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,1272 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { AlertTriangle, Check, Copy, KeyRound, Loader2, RefreshCw, ShieldAlert, ShieldCheck, Trash2 } from 'lucide-react';
|
|
5
|
+
import { Button, FilterDropdown, TextInput } from '@/components/design-system';
|
|
6
|
+
import { api, Api } from '@/lib/api';
|
|
7
|
+
import { decryptCredentialPayload, getVaultPublicKeyBase64 } from '@/lib/vault-crypto';
|
|
8
|
+
import type { AgentToken, HumanAction } from '@/hooks/useAgentActions';
|
|
9
|
+
import type { VaultInfo } from '@/components/vault/types';
|
|
10
|
+
|
|
11
|
+
const BUILTIN_PROFILE_OPTIONS = [
|
|
12
|
+
{ id: 'strict', label: 'Strict', description: 'Agent vault only, read-only, strong field redaction. 15 min TTL.' },
|
|
13
|
+
{ id: 'dev', label: 'Dev (recommended)', description: 'All vaults, read/write, moderate redaction. 1 hour TTL.' },
|
|
14
|
+
{ id: 'admin', label: 'Admin (dangerous)', description: 'Full access, no redaction. Use with caution.' },
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
const TOKEN_PERMISSION_OPTIONS = [
|
|
18
|
+
{ value: 'wallet:list', label: 'wallet:list' },
|
|
19
|
+
{ value: 'wallet:create:hot', label: 'wallet:create:hot' },
|
|
20
|
+
{ value: 'wallet:create:temp', label: 'wallet:create:temp' },
|
|
21
|
+
{ value: 'wallet:rename', label: 'wallet:rename' },
|
|
22
|
+
{ value: 'wallet:export', label: 'wallet:export' },
|
|
23
|
+
{ value: 'wallet:tx:add', label: 'wallet:tx:add' },
|
|
24
|
+
{ value: 'wallet:asset:add', label: 'wallet:asset:add' },
|
|
25
|
+
{ value: 'wallet:asset:remove', label: 'wallet:asset:remove' },
|
|
26
|
+
{ value: 'send:hot', label: 'send:hot' },
|
|
27
|
+
{ value: 'send:temp', label: 'send:temp' },
|
|
28
|
+
{ value: 'swap', label: 'swap' },
|
|
29
|
+
{ value: 'fund', label: 'fund' },
|
|
30
|
+
{ value: 'launch', label: 'launch' },
|
|
31
|
+
{ value: 'apikey:get', label: 'apikey:get' },
|
|
32
|
+
{ value: 'apikey:set', label: 'apikey:set' },
|
|
33
|
+
{ value: 'workspace:modify', label: 'workspace:modify' },
|
|
34
|
+
{ value: 'strategy:read', label: 'strategy:read' },
|
|
35
|
+
{ value: 'strategy:manage', label: 'strategy:manage' },
|
|
36
|
+
{ value: 'app:storage', label: 'app:storage' },
|
|
37
|
+
{ value: 'app:storage:all', label: 'app:storage:all' },
|
|
38
|
+
{ value: 'app:accesskey', label: 'app:accesskey' },
|
|
39
|
+
{ value: 'action:create', label: 'action:create' },
|
|
40
|
+
{ value: 'action:read', label: 'action:read' },
|
|
41
|
+
{ value: 'action:resolve', label: 'action:resolve' },
|
|
42
|
+
{ value: 'adapter:manage', label: 'adapter:manage' },
|
|
43
|
+
{ value: 'addressbook:write', label: 'addressbook:write' },
|
|
44
|
+
{ value: 'bookmark:write', label: 'bookmark:write' },
|
|
45
|
+
{ value: 'secret:read', label: 'secret:read' },
|
|
46
|
+
{ value: 'secret:write', label: 'secret:write' },
|
|
47
|
+
{ value: 'totp:read', label: 'totp:read' },
|
|
48
|
+
{ value: 'trade:all', label: 'trade:all' },
|
|
49
|
+
{ value: 'wallet:write', label: 'wallet:write' },
|
|
50
|
+
{ value: 'extension:*', label: 'extension:*' },
|
|
51
|
+
{ value: 'admin:*', label: 'admin:* (dangerous)' },
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
const PROFILE_DESCRIPTIONS: Record<string, string> = {
|
|
55
|
+
strict: 'Read-only, agent vault only. Hidden: password, cvv, privateKey, seedPhrase, refresh_token. 15 min TTL, 50 reads max.',
|
|
56
|
+
dev: 'Read/write, all vaults. Hidden: cvv, seedPhrase, privateKey, refresh_token. 1 hour TTL, 500 reads max.',
|
|
57
|
+
admin: 'Full access, no field redaction. 1 hour TTL, unlimited reads.',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function describeProfileId(id: string): string {
|
|
61
|
+
return PROFILE_DESCRIPTIONS[id] || `Custom profile: ${id}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type IssueMode = 'profile' | 'permissions';
|
|
65
|
+
|
|
66
|
+
const ISSUE_MODE_OPTIONS: Array<{ value: IssueMode; label: string }> = [
|
|
67
|
+
{ value: 'profile', label: 'Profile' },
|
|
68
|
+
{ value: 'permissions', label: 'Permissions' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const PROFILE_STORAGE_KEY = 'aura:api-keys:profiles:v1';
|
|
72
|
+
|
|
73
|
+
const BUILTIN_PROFILE_TEMPLATES: Record<string, { ttlSeconds: number; maxReads?: number; scope: string[]; excludeFields: string[] }> = {
|
|
74
|
+
strict: { ttlSeconds: 900, maxReads: 50, scope: ['secret:read'], excludeFields: ['password', 'cvv', 'privateKey', 'seedPhrase', 'refresh_token'] },
|
|
75
|
+
dev: { ttlSeconds: 3600, maxReads: 500, scope: ['wallet:list', 'secret:read', 'secret:write', 'action:create', 'action:read', 'action:resolve'], excludeFields: ['cvv', 'seedPhrase', 'privateKey', 'refresh_token'] },
|
|
76
|
+
admin: { ttlSeconds: 3600, scope: ['admin:*'], excludeFields: [] },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface ProfileOverrides {
|
|
80
|
+
ttlSeconds?: number;
|
|
81
|
+
maxReads?: number;
|
|
82
|
+
scope?: string[];
|
|
83
|
+
readScopes?: string[];
|
|
84
|
+
writeScopes?: string[];
|
|
85
|
+
excludeFields?: string[];
|
|
86
|
+
vaultReadScopes?: string[];
|
|
87
|
+
vaultWriteScopes?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface LocalProfileDraft {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
profile: string;
|
|
94
|
+
profileVersion: 'v1';
|
|
95
|
+
overrides: ProfileOverrides | null;
|
|
96
|
+
updatedAt: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface PolicyPreviewResponse {
|
|
100
|
+
version: 'v1';
|
|
101
|
+
profile?: { id: string; version: string; displayName?: string };
|
|
102
|
+
effectivePolicy: {
|
|
103
|
+
permissions: string[];
|
|
104
|
+
credentialAccess: {
|
|
105
|
+
read: string[];
|
|
106
|
+
write: string[];
|
|
107
|
+
excludeFields: string[];
|
|
108
|
+
maxReads: number | null;
|
|
109
|
+
};
|
|
110
|
+
ttlSeconds: number;
|
|
111
|
+
maxReads: number | null;
|
|
112
|
+
rateBudget: {
|
|
113
|
+
state: 'none' | 'inherited' | 'explicit';
|
|
114
|
+
requests: number | null;
|
|
115
|
+
windowSeconds: number | null;
|
|
116
|
+
source: 'none' | 'profile' | 'override';
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
warnings: string[];
|
|
120
|
+
overrideDelta: string[];
|
|
121
|
+
denyExamples?: Array<{ code: string; message: string }>;
|
|
122
|
+
effectivePolicyHash: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface IssueResponse {
|
|
126
|
+
success: boolean;
|
|
127
|
+
encryptedToken?: string;
|
|
128
|
+
warnings?: string[];
|
|
129
|
+
profile?: { id: string; version: string; displayName?: string };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface ApiKeysConsoleProps {
|
|
133
|
+
requests: HumanAction[];
|
|
134
|
+
activeTokens: AgentToken[];
|
|
135
|
+
inactiveTokens: AgentToken[];
|
|
136
|
+
actionLoading: string | null;
|
|
137
|
+
onResolveAction: (id: string, approved: boolean) => Promise<{ success: boolean; message?: string }>;
|
|
138
|
+
onRevokeToken: (tokenHash: string) => Promise<boolean>;
|
|
139
|
+
vaults?: VaultInfo[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function shortHash(value: string): string {
|
|
143
|
+
if (value.length <= 14) return value;
|
|
144
|
+
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function generateSuggestedAgentId(): string {
|
|
148
|
+
const seed = Math.random().toString(36).slice(2, 8);
|
|
149
|
+
return `agent:local:${seed}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizePreviewPayload(raw: unknown): PolicyPreviewResponse {
|
|
153
|
+
const data = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
|
|
154
|
+
const effectivePolicy = (data.effectivePolicy && typeof data.effectivePolicy === 'object'
|
|
155
|
+
? data.effectivePolicy
|
|
156
|
+
: {}) as Record<string, unknown>;
|
|
157
|
+
const credentialAccess = (effectivePolicy.credentialAccess && typeof effectivePolicy.credentialAccess === 'object'
|
|
158
|
+
? effectivePolicy.credentialAccess
|
|
159
|
+
: {}) as Record<string, unknown>;
|
|
160
|
+
const rateBudget = (effectivePolicy.rateBudget && typeof effectivePolicy.rateBudget === 'object'
|
|
161
|
+
? effectivePolicy.rateBudget
|
|
162
|
+
: {}) as Record<string, unknown>;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
version: 'v1',
|
|
166
|
+
profile: data.profile && typeof data.profile === 'object'
|
|
167
|
+
? data.profile as PolicyPreviewResponse['profile']
|
|
168
|
+
: undefined,
|
|
169
|
+
effectivePolicy: {
|
|
170
|
+
permissions: Array.isArray(effectivePolicy.permissions) ? effectivePolicy.permissions.filter((v): v is string => typeof v === 'string') : [],
|
|
171
|
+
credentialAccess: {
|
|
172
|
+
read: Array.isArray(credentialAccess.read) ? credentialAccess.read.filter((v): v is string => typeof v === 'string') : [],
|
|
173
|
+
write: Array.isArray(credentialAccess.write) ? credentialAccess.write.filter((v): v is string => typeof v === 'string') : [],
|
|
174
|
+
excludeFields: Array.isArray(credentialAccess.excludeFields) ? credentialAccess.excludeFields.filter((v): v is string => typeof v === 'string') : [],
|
|
175
|
+
maxReads: typeof credentialAccess.maxReads === 'number' ? credentialAccess.maxReads : null,
|
|
176
|
+
},
|
|
177
|
+
ttlSeconds: typeof effectivePolicy.ttlSeconds === 'number' ? effectivePolicy.ttlSeconds : 0,
|
|
178
|
+
maxReads: typeof effectivePolicy.maxReads === 'number' ? effectivePolicy.maxReads : null,
|
|
179
|
+
rateBudget: {
|
|
180
|
+
state: rateBudget.state === 'inherited' || rateBudget.state === 'explicit' ? rateBudget.state : 'none',
|
|
181
|
+
requests: typeof rateBudget.requests === 'number' ? rateBudget.requests : null,
|
|
182
|
+
windowSeconds: typeof rateBudget.windowSeconds === 'number' ? rateBudget.windowSeconds : null,
|
|
183
|
+
source: rateBudget.source === 'profile' || rateBudget.source === 'override' ? rateBudget.source : 'none',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
warnings: Array.isArray(data.warnings) ? data.warnings.filter((v): v is string => typeof v === 'string') : [],
|
|
187
|
+
overrideDelta: Array.isArray(data.overrideDelta) ? data.overrideDelta.filter((v): v is string => typeof v === 'string') : [],
|
|
188
|
+
denyExamples: Array.isArray(data.denyExamples)
|
|
189
|
+
? data.denyExamples
|
|
190
|
+
.filter((row): row is Record<string, unknown> => Boolean(row) && typeof row === 'object')
|
|
191
|
+
.map((row) => ({ code: String(row.code || 'UNKNOWN'), message: String(row.message || '') }))
|
|
192
|
+
: [],
|
|
193
|
+
effectivePolicyHash: typeof data.effectivePolicyHash === 'string' ? data.effectivePolicyHash : 'n/a',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseMetadata(raw?: string): Record<string, unknown> {
|
|
198
|
+
if (!raw) return {};
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
201
|
+
if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>;
|
|
202
|
+
return {};
|
|
203
|
+
} catch {
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseCsv(value: string): string[] | undefined {
|
|
209
|
+
const parts = value
|
|
210
|
+
.split(',')
|
|
211
|
+
.map((part) => part.trim())
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
return parts.length > 0 ? parts : undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeStringList(values: string[]): string[] {
|
|
217
|
+
return Array.from(
|
|
218
|
+
new Set(
|
|
219
|
+
values
|
|
220
|
+
.map((value) => value.trim().normalize('NFKC').toLowerCase())
|
|
221
|
+
.filter(Boolean),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildOverrides(input: {
|
|
227
|
+
ttlSeconds: string;
|
|
228
|
+
maxReads: string;
|
|
229
|
+
scope: string[];
|
|
230
|
+
excludeFields: string;
|
|
231
|
+
}): { overrides: ProfileOverrides | null; error: string | null } {
|
|
232
|
+
const next: ProfileOverrides = {};
|
|
233
|
+
|
|
234
|
+
if (input.ttlSeconds.trim().length > 0) {
|
|
235
|
+
const ttl = Number(input.ttlSeconds.trim());
|
|
236
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
237
|
+
return { overrides: null, error: 'TTL override must be a positive number.' };
|
|
238
|
+
}
|
|
239
|
+
next.ttlSeconds = ttl;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (input.maxReads.trim().length > 0) {
|
|
243
|
+
const maxReads = Number(input.maxReads.trim());
|
|
244
|
+
if (!Number.isFinite(maxReads) || maxReads <= 0) {
|
|
245
|
+
return { overrides: null, error: 'Max reads override must be a positive number.' };
|
|
246
|
+
}
|
|
247
|
+
next.maxReads = maxReads;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const scope = normalizeStringList(input.scope);
|
|
251
|
+
const excludeFields = parseCsv(input.excludeFields);
|
|
252
|
+
|
|
253
|
+
if (scope.length > 0) next.scope = scope;
|
|
254
|
+
if (excludeFields) next.excludeFields = excludeFields;
|
|
255
|
+
|
|
256
|
+
return { overrides: Object.keys(next).length > 0 ? next : null, error: null };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const ApiKeysConsole: React.FC<ApiKeysConsoleProps> = ({
|
|
260
|
+
requests,
|
|
261
|
+
activeTokens,
|
|
262
|
+
inactiveTokens,
|
|
263
|
+
actionLoading,
|
|
264
|
+
onResolveAction,
|
|
265
|
+
onRevokeToken,
|
|
266
|
+
vaults = [],
|
|
267
|
+
}) => {
|
|
268
|
+
const [profiles, setProfiles] = useState<LocalProfileDraft[]>([]);
|
|
269
|
+
const [editingProfileId, setEditingProfileId] = useState<string | null>(null);
|
|
270
|
+
|
|
271
|
+
const [profileName, setProfileName] = useState('');
|
|
272
|
+
const [profileBase, setProfileBase] = useState<string>('dev');
|
|
273
|
+
const [profileTtlSeconds, setProfileTtlSeconds] = useState('');
|
|
274
|
+
const [profileMaxReads, setProfileMaxReads] = useState('');
|
|
275
|
+
const [profileScopes, setProfileScopes] = useState<string[]>([]);
|
|
276
|
+
const [profileScopeCandidate, setProfileScopeCandidate] = useState<string>('secret:read');
|
|
277
|
+
const [profileExcludeFields, setProfileExcludeFields] = useState('');
|
|
278
|
+
const [profileVaultScopes, setProfileVaultScopes] = useState<string[]>([]);
|
|
279
|
+
|
|
280
|
+
const [profileError, setProfileError] = useState<string | null>(null);
|
|
281
|
+
const [profileNotice, setProfileNotice] = useState<string | null>(null);
|
|
282
|
+
|
|
283
|
+
const [issueAgentId, setIssueAgentId] = useState<string>(() => generateSuggestedAgentId());
|
|
284
|
+
const [issueMode, setIssueMode] = useState<IssueMode>('profile');
|
|
285
|
+
const [issueProfileSource, setIssueProfileSource] = useState<string>('builtin:dev');
|
|
286
|
+
const [issuePermissionCandidate, setIssuePermissionCandidate] = useState<string>('secret:read');
|
|
287
|
+
const [issuePermissions, setIssuePermissions] = useState<string[]>(['secret:read', 'secret:write']);
|
|
288
|
+
const [issueVaultScopes, setIssueVaultScopes] = useState<string[]>([]);
|
|
289
|
+
const [issuing, setIssuing] = useState(false);
|
|
290
|
+
const [issueError, setIssueError] = useState<string | null>(null);
|
|
291
|
+
const [issuedToken, setIssuedToken] = useState<string | null>(null);
|
|
292
|
+
const [issuedMeta, setIssuedMeta] = useState<string | null>(null);
|
|
293
|
+
const [copied, setCopied] = useState(false);
|
|
294
|
+
|
|
295
|
+
const [previewLoading, setPreviewLoading] = useState(false);
|
|
296
|
+
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
297
|
+
const [preview, setPreview] = useState<PolicyPreviewResponse | null>(null);
|
|
298
|
+
|
|
299
|
+
const [requestNotice, setRequestNotice] = useState<string | null>(null);
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
try {
|
|
303
|
+
const raw = localStorage.getItem(PROFILE_STORAGE_KEY);
|
|
304
|
+
if (!raw) return;
|
|
305
|
+
const parsed = JSON.parse(raw) as LocalProfileDraft[];
|
|
306
|
+
if (!Array.isArray(parsed)) return;
|
|
307
|
+
const safe = parsed
|
|
308
|
+
.filter((entry) =>
|
|
309
|
+
entry
|
|
310
|
+
&& typeof entry.id === 'string'
|
|
311
|
+
&& typeof entry.name === 'string'
|
|
312
|
+
&& typeof entry.profile === 'string'
|
|
313
|
+
&& entry.profileVersion === 'v1'
|
|
314
|
+
&& typeof entry.updatedAt === 'number'
|
|
315
|
+
)
|
|
316
|
+
.map((entry) => {
|
|
317
|
+
const scope = normalizeStringList([
|
|
318
|
+
...(entry.overrides?.scope || []),
|
|
319
|
+
...(entry.overrides?.readScopes || []),
|
|
320
|
+
...(entry.overrides?.writeScopes || []),
|
|
321
|
+
]);
|
|
322
|
+
const overrides = entry.overrides
|
|
323
|
+
? {
|
|
324
|
+
...entry.overrides,
|
|
325
|
+
...(scope.length > 0 ? { scope } : {}),
|
|
326
|
+
readScopes: undefined,
|
|
327
|
+
writeScopes: undefined,
|
|
328
|
+
}
|
|
329
|
+
: null;
|
|
330
|
+
return {
|
|
331
|
+
...entry,
|
|
332
|
+
overrides,
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
setProfiles(safe);
|
|
336
|
+
} catch {
|
|
337
|
+
setProfiles([]);
|
|
338
|
+
}
|
|
339
|
+
}, []);
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(profiles));
|
|
343
|
+
}, [profiles]);
|
|
344
|
+
|
|
345
|
+
const resetProfileForm = useCallback(() => {
|
|
346
|
+
setEditingProfileId(null);
|
|
347
|
+
setProfileName('');
|
|
348
|
+
setProfileBase('dev');
|
|
349
|
+
setProfileTtlSeconds('');
|
|
350
|
+
setProfileMaxReads('');
|
|
351
|
+
setProfileScopes([]);
|
|
352
|
+
setProfileScopeCandidate('secret:read');
|
|
353
|
+
setProfileExcludeFields('');
|
|
354
|
+
setProfileVaultScopes([]);
|
|
355
|
+
setProfileError(null);
|
|
356
|
+
setProfileNotice(null);
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
const applyBaseTemplate = useCallback((baseProfile: string) => {
|
|
360
|
+
const template = BUILTIN_PROFILE_TEMPLATES[baseProfile];
|
|
361
|
+
if (!template) return;
|
|
362
|
+
setProfileTtlSeconds(String(template.ttlSeconds));
|
|
363
|
+
setProfileMaxReads(typeof template.maxReads === 'number' ? String(template.maxReads) : '');
|
|
364
|
+
setProfileScopes(template.scope);
|
|
365
|
+
setProfileScopeCandidate(template.scope[0] || 'secret:read');
|
|
366
|
+
setProfileExcludeFields(template.excludeFields.join(', '));
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
const vaultScopeOptions = useMemo(() => {
|
|
370
|
+
const options: Array<{ value: string; label: string }> = [
|
|
371
|
+
{ value: 'vault:*', label: 'All Vaults (vault:*)' },
|
|
372
|
+
];
|
|
373
|
+
for (const vault of vaults) {
|
|
374
|
+
const name = vault.name || vault.id;
|
|
375
|
+
options.push({ value: `vault:${vault.id}`, label: `vault:${name}` });
|
|
376
|
+
}
|
|
377
|
+
return options;
|
|
378
|
+
}, [vaults]);
|
|
379
|
+
|
|
380
|
+
const pendingAuthRequests = useMemo(
|
|
381
|
+
() => requests.filter((request) => request.status === 'pending' && (request.type === 'auth' || request.type === 'permission_update')),
|
|
382
|
+
[requests],
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const selectableProfileSources = useMemo(() => {
|
|
386
|
+
const builtin = BUILTIN_PROFILE_OPTIONS.map((option) => ({
|
|
387
|
+
value: `builtin:${option.id}`,
|
|
388
|
+
label: `${option.label} — ${option.description}`,
|
|
389
|
+
}));
|
|
390
|
+
const custom = profiles.map((profile) => ({
|
|
391
|
+
value: `draft:${profile.id}`,
|
|
392
|
+
label: `Custom · ${profile.name}`,
|
|
393
|
+
}));
|
|
394
|
+
return [...builtin, ...custom];
|
|
395
|
+
}, [profiles]);
|
|
396
|
+
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
if (!selectableProfileSources.some((option) => option.value === issueProfileSource)) {
|
|
399
|
+
setIssueProfileSource('builtin:dev');
|
|
400
|
+
}
|
|
401
|
+
}, [issueProfileSource, selectableProfileSources]);
|
|
402
|
+
|
|
403
|
+
const resolveIssueProfile = useCallback((): { profile: string; profileVersion: string; profileOverrides?: ProfileOverrides } => {
|
|
404
|
+
if (issueProfileSource.startsWith('draft:')) {
|
|
405
|
+
const profileId = issueProfileSource.slice(6);
|
|
406
|
+
const draft = profiles.find((entry) => entry.id === profileId);
|
|
407
|
+
if (draft) {
|
|
408
|
+
const scope = normalizeStringList([
|
|
409
|
+
...(draft.overrides?.scope || []),
|
|
410
|
+
...(draft.overrides?.readScopes || []),
|
|
411
|
+
...(draft.overrides?.writeScopes || []),
|
|
412
|
+
]);
|
|
413
|
+
const profileOverrides = draft.overrides
|
|
414
|
+
? {
|
|
415
|
+
...draft.overrides,
|
|
416
|
+
...(scope.length > 0 ? { scope } : {}),
|
|
417
|
+
}
|
|
418
|
+
: undefined;
|
|
419
|
+
return {
|
|
420
|
+
profile: draft.profile,
|
|
421
|
+
profileVersion: draft.profileVersion,
|
|
422
|
+
...(profileOverrides ? { profileOverrides } : {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const profile = issueProfileSource.startsWith('builtin:') ? issueProfileSource.slice(8) : 'dev';
|
|
427
|
+
return { profile, profileVersion: 'v1' };
|
|
428
|
+
}, [issueProfileSource, profiles]);
|
|
429
|
+
|
|
430
|
+
const addIssuePermission = useCallback((permission: string) => {
|
|
431
|
+
const normalized = normalizeStringList([permission])[0];
|
|
432
|
+
if (!normalized) return;
|
|
433
|
+
setIssuePermissions((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
|
|
434
|
+
}, []);
|
|
435
|
+
|
|
436
|
+
const removeIssuePermission = useCallback((permission: string) => {
|
|
437
|
+
setIssuePermissions((prev) => prev.filter((entry) => entry !== permission));
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
440
|
+
const handleSaveProfile = useCallback(() => {
|
|
441
|
+
setProfileError(null);
|
|
442
|
+
setProfileNotice(null);
|
|
443
|
+
const trimmedName = profileName.trim();
|
|
444
|
+
if (!trimmedName) {
|
|
445
|
+
setProfileError('Profile name is required.');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const { overrides: baseOverrides, error } = buildOverrides({
|
|
450
|
+
ttlSeconds: profileTtlSeconds,
|
|
451
|
+
maxReads: profileMaxReads,
|
|
452
|
+
scope: profileScopes,
|
|
453
|
+
excludeFields: profileExcludeFields,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (error) {
|
|
457
|
+
setProfileError(error);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const overrides: ProfileOverrides | null = profileVaultScopes.length > 0
|
|
462
|
+
? { ...(baseOverrides || {}), readScopes: profileVaultScopes, writeScopes: profileVaultScopes }
|
|
463
|
+
: baseOverrides;
|
|
464
|
+
|
|
465
|
+
const existingNameConflict = profiles.some((profile) =>
|
|
466
|
+
profile.name.toLowerCase() === trimmedName.toLowerCase()
|
|
467
|
+
&& profile.id !== editingProfileId
|
|
468
|
+
);
|
|
469
|
+
if (existingNameConflict) {
|
|
470
|
+
setProfileError('A profile with this name already exists.');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const payload: LocalProfileDraft = {
|
|
475
|
+
id: editingProfileId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
|
|
476
|
+
name: trimmedName,
|
|
477
|
+
profile: profileBase,
|
|
478
|
+
profileVersion: 'v1',
|
|
479
|
+
overrides,
|
|
480
|
+
updatedAt: Date.now(),
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
setProfiles((prev) => {
|
|
484
|
+
if (editingProfileId) {
|
|
485
|
+
return prev.map((entry) => (entry.id === editingProfileId ? payload : entry));
|
|
486
|
+
}
|
|
487
|
+
return [payload, ...prev];
|
|
488
|
+
});
|
|
489
|
+
setProfileNotice(editingProfileId ? 'Profile updated.' : 'Profile created.');
|
|
490
|
+
if (!editingProfileId) {
|
|
491
|
+
setIssueProfileSource(`draft:${payload.id}`);
|
|
492
|
+
}
|
|
493
|
+
setEditingProfileId(payload.id);
|
|
494
|
+
}, [
|
|
495
|
+
editingProfileId,
|
|
496
|
+
profileBase,
|
|
497
|
+
profileExcludeFields,
|
|
498
|
+
profileMaxReads,
|
|
499
|
+
profileName,
|
|
500
|
+
profileScopes,
|
|
501
|
+
profileTtlSeconds,
|
|
502
|
+
profileVaultScopes,
|
|
503
|
+
profiles,
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
const handleEditProfile = useCallback((profile: LocalProfileDraft) => {
|
|
507
|
+
setEditingProfileId(profile.id);
|
|
508
|
+
setProfileName(profile.name);
|
|
509
|
+
setProfileBase(profile.profile);
|
|
510
|
+
setProfileTtlSeconds(profile.overrides?.ttlSeconds ? String(profile.overrides.ttlSeconds) : '');
|
|
511
|
+
setProfileMaxReads(profile.overrides?.maxReads ? String(profile.overrides.maxReads) : '');
|
|
512
|
+
const legacyScopes = [
|
|
513
|
+
...(profile.overrides?.scope || []),
|
|
514
|
+
...(profile.overrides?.readScopes || []),
|
|
515
|
+
...(profile.overrides?.writeScopes || []),
|
|
516
|
+
];
|
|
517
|
+
const normalizedScopes = normalizeStringList(legacyScopes);
|
|
518
|
+
setProfileScopes(normalizedScopes);
|
|
519
|
+
setProfileScopeCandidate(normalizedScopes[0] || 'secret:read');
|
|
520
|
+
setProfileExcludeFields((profile.overrides?.excludeFields || []).join(', '));
|
|
521
|
+
setProfileVaultScopes(profile.overrides?.readScopes || profile.overrides?.vaultReadScopes || []);
|
|
522
|
+
setProfileError(null);
|
|
523
|
+
setProfileNotice(null);
|
|
524
|
+
}, []);
|
|
525
|
+
|
|
526
|
+
const addProfileScope = useCallback((scope: string) => {
|
|
527
|
+
const normalized = normalizeStringList([scope])[0];
|
|
528
|
+
if (!normalized) return;
|
|
529
|
+
setProfileScopes((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
|
|
530
|
+
}, []);
|
|
531
|
+
|
|
532
|
+
const removeProfileScope = useCallback((scope: string) => {
|
|
533
|
+
setProfileScopes((prev) => prev.filter((entry) => entry !== scope));
|
|
534
|
+
}, []);
|
|
535
|
+
|
|
536
|
+
const handleDeleteProfile = useCallback((id: string) => {
|
|
537
|
+
setProfiles((prev) => prev.filter((profile) => profile.id !== id));
|
|
538
|
+
if (editingProfileId === id) {
|
|
539
|
+
resetProfileForm();
|
|
540
|
+
}
|
|
541
|
+
if (issueProfileSource === `draft:${id}`) {
|
|
542
|
+
setIssueProfileSource('builtin:dev');
|
|
543
|
+
}
|
|
544
|
+
}, [editingProfileId, issueProfileSource, resetProfileForm]);
|
|
545
|
+
|
|
546
|
+
const handlePreview = useCallback(async () => {
|
|
547
|
+
setPreviewError(null);
|
|
548
|
+
setPreview(null);
|
|
549
|
+
if (issueMode === 'permissions') {
|
|
550
|
+
setPreviewError('Policy preview is currently available for profile-based issuance only.');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
setPreviewLoading(true);
|
|
554
|
+
try {
|
|
555
|
+
const selection = resolveIssueProfile();
|
|
556
|
+
const data = await api.post<PolicyPreviewResponse>(Api.Wallet, '/actions/token/preview', selection);
|
|
557
|
+
setPreview(normalizePreviewPayload(data));
|
|
558
|
+
} catch (error) {
|
|
559
|
+
setPreviewError(error instanceof Error ? error.message : 'Failed to preview profile policy.');
|
|
560
|
+
} finally {
|
|
561
|
+
setPreviewLoading(false);
|
|
562
|
+
}
|
|
563
|
+
}, [issueMode, resolveIssueProfile]);
|
|
564
|
+
|
|
565
|
+
const handleIssueApiKey = useCallback(async () => {
|
|
566
|
+
setIssueError(null);
|
|
567
|
+
setIssuedToken(null);
|
|
568
|
+
setIssuedMeta(null);
|
|
569
|
+
if (!issueAgentId.trim()) {
|
|
570
|
+
setIssueError('Agent ID is required.');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (issueMode === 'permissions' && issuePermissions.length === 0) {
|
|
574
|
+
setIssueError('Select at least one permission for manual issuance.');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const pubkey = getVaultPublicKeyBase64();
|
|
578
|
+
if (!pubkey) {
|
|
579
|
+
setIssueError('Vault keypair is unavailable. Re-unlock and retry.');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
setIssuing(true);
|
|
584
|
+
try {
|
|
585
|
+
let result: IssueResponse;
|
|
586
|
+
if (issueMode === 'permissions') {
|
|
587
|
+
const credentialAccess = issueVaultScopes.length > 0
|
|
588
|
+
? { read: issueVaultScopes, write: issueVaultScopes }
|
|
589
|
+
: undefined;
|
|
590
|
+
result = await api.post<IssueResponse>(Api.Wallet, '/actions/token', {
|
|
591
|
+
agentId: issueAgentId.trim(),
|
|
592
|
+
pubkey,
|
|
593
|
+
permissions: issuePermissions,
|
|
594
|
+
...(credentialAccess ? { credentialAccess } : {}),
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
const profilePayload = resolveIssueProfile();
|
|
598
|
+
if (issueVaultScopes.length > 0) {
|
|
599
|
+
profilePayload.profileOverrides = {
|
|
600
|
+
...(profilePayload.profileOverrides || {}),
|
|
601
|
+
readScopes: issueVaultScopes,
|
|
602
|
+
writeScopes: issueVaultScopes,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
result = await api.post<IssueResponse>(Api.Wallet, '/actions/token', {
|
|
606
|
+
agentId: issueAgentId.trim(),
|
|
607
|
+
pubkey,
|
|
608
|
+
...profilePayload,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (!result.success || !result.encryptedToken) {
|
|
612
|
+
throw new Error('Token issuance failed.');
|
|
613
|
+
}
|
|
614
|
+
const token = await decryptCredentialPayload(result.encryptedToken);
|
|
615
|
+
setIssuedToken(token);
|
|
616
|
+
if (issueMode === 'permissions') {
|
|
617
|
+
setIssuedMeta(
|
|
618
|
+
`${issueAgentId.trim()} · permissions:${issuePermissions.join(',')}${result.warnings?.length ? ' · warnings present' : ''}`,
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
const selection = resolveIssueProfile();
|
|
622
|
+
setIssuedMeta(
|
|
623
|
+
`${issueAgentId.trim()} · ${selection.profile}@${selection.profileVersion}${result.warnings?.length ? ' · warnings present' : ''}`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {
|
|
627
|
+
setIssueError(error instanceof Error ? error.message : 'Token issuance failed.');
|
|
628
|
+
} finally {
|
|
629
|
+
setIssuing(false);
|
|
630
|
+
}
|
|
631
|
+
}, [issueAgentId, issueMode, issuePermissions, issueVaultScopes, resolveIssueProfile]);
|
|
632
|
+
|
|
633
|
+
const handleCopyToken = useCallback(async () => {
|
|
634
|
+
if (!issuedToken || !navigator.clipboard) return;
|
|
635
|
+
await navigator.clipboard.writeText(issuedToken);
|
|
636
|
+
setCopied(true);
|
|
637
|
+
setTimeout(() => setCopied(false), 1500);
|
|
638
|
+
}, [issuedToken]);
|
|
639
|
+
|
|
640
|
+
const handleResolveRequest = useCallback(async (id: string, approved: boolean) => {
|
|
641
|
+
const result = await onResolveAction(id, approved);
|
|
642
|
+
if (result.success) {
|
|
643
|
+
setRequestNotice(approved ? `Approved request ${id.slice(0, 8)}.` : `Rejected request ${id.slice(0, 8)}.`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
setRequestNotice(result.message || 'Failed to resolve request.');
|
|
647
|
+
}, [onResolveAction]);
|
|
648
|
+
|
|
649
|
+
const managedActiveTokens = useMemo(
|
|
650
|
+
() => activeTokens.filter((token) => !token.isAdmin),
|
|
651
|
+
[activeTokens],
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
return (
|
|
655
|
+
<div className="h-full overflow-y-auto px-5 py-4 font-mono">
|
|
656
|
+
<div className="mb-4 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
|
|
657
|
+
<div className="text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">API KEYS</div>
|
|
658
|
+
<div className="mt-1 text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
|
|
659
|
+
Auth management surface for profile templates + permissions-based issuance + pending auth approvals.
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
|
664
|
+
<section className="order-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
|
|
665
|
+
<div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">ISSUE API KEY</div>
|
|
666
|
+
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
|
667
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
|
|
668
|
+
<TextInput
|
|
669
|
+
label="Agent ID"
|
|
670
|
+
value={issueAgentId}
|
|
671
|
+
onChange={(event) => setIssueAgentId(event.target.value)}
|
|
672
|
+
placeholder="agent:local:dev"
|
|
673
|
+
compact
|
|
674
|
+
/>
|
|
675
|
+
<Button
|
|
676
|
+
variant="secondary"
|
|
677
|
+
size="sm"
|
|
678
|
+
onClick={() => setIssueAgentId(generateSuggestedAgentId())}
|
|
679
|
+
icon={<RefreshCw size={10} />}
|
|
680
|
+
title="Regenerate Agent ID"
|
|
681
|
+
className="mb-[1px]"
|
|
682
|
+
>
|
|
683
|
+
NEW ID
|
|
684
|
+
</Button>
|
|
685
|
+
</div>
|
|
686
|
+
|
|
687
|
+
<FilterDropdown
|
|
688
|
+
label="Issuance Mode"
|
|
689
|
+
options={ISSUE_MODE_OPTIONS}
|
|
690
|
+
value={issueMode}
|
|
691
|
+
onChange={(value) => setIssueMode(value as IssueMode)}
|
|
692
|
+
compact
|
|
693
|
+
/>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
{issueMode === 'profile' ? (
|
|
697
|
+
<div className="mt-2">
|
|
698
|
+
<FilterDropdown
|
|
699
|
+
label="Profile Source"
|
|
700
|
+
options={selectableProfileSources}
|
|
701
|
+
value={issueProfileSource}
|
|
702
|
+
onChange={setIssueProfileSource}
|
|
703
|
+
compact
|
|
704
|
+
/>
|
|
705
|
+
{(() => {
|
|
706
|
+
const builtinId = issueProfileSource.startsWith('builtin:') ? issueProfileSource.slice(8) : null;
|
|
707
|
+
const desc = builtinId ? PROFILE_DESCRIPTIONS[builtinId] : null;
|
|
708
|
+
return desc ? (
|
|
709
|
+
<div className="mt-1 px-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">{desc}</div>
|
|
710
|
+
) : (
|
|
711
|
+
<div className="mt-1 px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
712
|
+
Profile mode resolves permissions/scopes/ttl from the selected profile.
|
|
713
|
+
</div>
|
|
714
|
+
);
|
|
715
|
+
})()}
|
|
716
|
+
</div>
|
|
717
|
+
) : (
|
|
718
|
+
<div className="mt-2 space-y-1">
|
|
719
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
|
|
720
|
+
<FilterDropdown
|
|
721
|
+
label="Permissions"
|
|
722
|
+
options={TOKEN_PERMISSION_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
|
|
723
|
+
value={issuePermissionCandidate}
|
|
724
|
+
onChange={setIssuePermissionCandidate}
|
|
725
|
+
compact
|
|
726
|
+
/>
|
|
727
|
+
<Button
|
|
728
|
+
variant="secondary"
|
|
729
|
+
size="sm"
|
|
730
|
+
onClick={() => addIssuePermission(issuePermissionCandidate)}
|
|
731
|
+
>
|
|
732
|
+
ADD
|
|
733
|
+
</Button>
|
|
734
|
+
</div>
|
|
735
|
+
{issuePermissions.length === 0 ? (
|
|
736
|
+
<div className="px-1 text-[8px] text-[var(--color-danger,#ef4444)]">
|
|
737
|
+
No permissions selected.
|
|
738
|
+
</div>
|
|
739
|
+
) : (
|
|
740
|
+
<div className="flex flex-wrap gap-1">
|
|
741
|
+
{issuePermissions.map((permission) => (
|
|
742
|
+
<span
|
|
743
|
+
key={`perm-${permission}`}
|
|
744
|
+
className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
|
|
745
|
+
>
|
|
746
|
+
{permission}
|
|
747
|
+
<Button
|
|
748
|
+
variant="ghost"
|
|
749
|
+
size="sm"
|
|
750
|
+
onClick={() => removeIssuePermission(permission)}
|
|
751
|
+
className="h-4 px-1 text-[8px]"
|
|
752
|
+
>
|
|
753
|
+
X
|
|
754
|
+
</Button>
|
|
755
|
+
</span>
|
|
756
|
+
))}
|
|
757
|
+
</div>
|
|
758
|
+
)}
|
|
759
|
+
<div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
760
|
+
Permissions mode issues exactly the selected permissions.
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
)}
|
|
764
|
+
|
|
765
|
+
{vaultScopeOptions.length > 1 && (
|
|
766
|
+
<div className="mt-2 space-y-1">
|
|
767
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
|
|
768
|
+
<FilterDropdown
|
|
769
|
+
label="Vault Scope"
|
|
770
|
+
options={vaultScopeOptions}
|
|
771
|
+
value={vaultScopeOptions[0]?.value || 'vault:*'}
|
|
772
|
+
onChange={(value) => {
|
|
773
|
+
if (!issueVaultScopes.includes(value)) {
|
|
774
|
+
setIssueVaultScopes((prev) => [...prev, value]);
|
|
775
|
+
}
|
|
776
|
+
}}
|
|
777
|
+
compact
|
|
778
|
+
/>
|
|
779
|
+
<Button
|
|
780
|
+
variant="secondary"
|
|
781
|
+
size="sm"
|
|
782
|
+
onClick={() => setIssueVaultScopes([])}
|
|
783
|
+
>
|
|
784
|
+
CLEAR
|
|
785
|
+
</Button>
|
|
786
|
+
</div>
|
|
787
|
+
{issueVaultScopes.length === 0 ? (
|
|
788
|
+
<div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
789
|
+
No vault restriction — uses profile/system defaults.
|
|
790
|
+
</div>
|
|
791
|
+
) : (
|
|
792
|
+
<div className="flex flex-wrap gap-1">
|
|
793
|
+
{issueVaultScopes.map((scope) => (
|
|
794
|
+
<span
|
|
795
|
+
key={`vs-${scope}`}
|
|
796
|
+
className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
|
|
797
|
+
>
|
|
798
|
+
{scope}
|
|
799
|
+
<Button
|
|
800
|
+
variant="ghost"
|
|
801
|
+
size="sm"
|
|
802
|
+
onClick={() => setIssueVaultScopes((prev) => prev.filter((s) => s !== scope))}
|
|
803
|
+
className="h-4 px-1 text-[8px]"
|
|
804
|
+
>
|
|
805
|
+
X
|
|
806
|
+
</Button>
|
|
807
|
+
</span>
|
|
808
|
+
))}
|
|
809
|
+
</div>
|
|
810
|
+
)}
|
|
811
|
+
</div>
|
|
812
|
+
)}
|
|
813
|
+
|
|
814
|
+
<div className="mt-2 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-2 py-2 text-[9px] text-[var(--color-text-muted,#6b7280)]">
|
|
815
|
+
Generate a key in one click. Preview is optional and only shows what permissions/scopes will be issued.
|
|
816
|
+
</div>
|
|
817
|
+
|
|
818
|
+
{issueError && (
|
|
819
|
+
<div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{issueError}</div>
|
|
820
|
+
)}
|
|
821
|
+
|
|
822
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
823
|
+
<Button
|
|
824
|
+
size="sm"
|
|
825
|
+
onClick={() => { void handleIssueApiKey(); }}
|
|
826
|
+
loading={issuing}
|
|
827
|
+
icon={!issuing ? <KeyRound size={11} /> : undefined}
|
|
828
|
+
>
|
|
829
|
+
ISSUE API KEY
|
|
830
|
+
</Button>
|
|
831
|
+
<Button
|
|
832
|
+
size="sm"
|
|
833
|
+
variant="secondary"
|
|
834
|
+
onClick={() => { void handlePreview(); }}
|
|
835
|
+
loading={previewLoading}
|
|
836
|
+
disabled={issueMode === 'permissions'}
|
|
837
|
+
icon={!previewLoading ? <RefreshCw size={11} /> : undefined}
|
|
838
|
+
>
|
|
839
|
+
PREVIEW POLICY
|
|
840
|
+
</Button>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
{previewError && (
|
|
844
|
+
<div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{previewError}</div>
|
|
845
|
+
)}
|
|
846
|
+
|
|
847
|
+
{preview && (
|
|
848
|
+
<div className="mt-3 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
|
|
849
|
+
<div className="text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
|
|
850
|
+
HASH {shortHash(preview.effectivePolicyHash)}
|
|
851
|
+
</div>
|
|
852
|
+
{preview.profile && (
|
|
853
|
+
<div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
854
|
+
profile: {preview.profile.id}@{preview.profile.version}
|
|
855
|
+
</div>
|
|
856
|
+
)}
|
|
857
|
+
<div className="mt-1 text-[9px] text-[var(--color-text-muted,#6b7280)]">
|
|
858
|
+
permissions: {preview.effectivePolicy.permissions.length} · ttl: {preview.effectivePolicy.ttlSeconds}s · max reads: {preview.effectivePolicy.maxReads ?? 'unlimited'}
|
|
859
|
+
</div>
|
|
860
|
+
<div className="mt-2 grid grid-cols-1 gap-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
|
|
861
|
+
<div>perms: {preview.effectivePolicy.permissions.join(', ') || '(none)'}</div>
|
|
862
|
+
<div>read scope: {preview.effectivePolicy.credentialAccess.read.join(', ') || '(none)'}</div>
|
|
863
|
+
<div>write scope: {preview.effectivePolicy.credentialAccess.write.join(', ') || '(none)'}</div>
|
|
864
|
+
<div>excluded fields: {preview.effectivePolicy.credentialAccess.excludeFields.join(', ') || '(none)'}</div>
|
|
865
|
+
<div>
|
|
866
|
+
rate budget: {preview.effectivePolicy.rateBudget.state} ({preview.effectivePolicy.rateBudget.requests ?? 'n/a'} / {preview.effectivePolicy.rateBudget.windowSeconds ?? 'n/a'}s, source={preview.effectivePolicy.rateBudget.source})
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
{preview.overrideDelta.length > 0 && (
|
|
871
|
+
<div className="mt-2 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-1.5 text-[8px] text-[var(--color-text-muted,#6b7280)]">
|
|
872
|
+
<div className="mb-1 tracking-widest text-[var(--color-text-faint,#9ca3af)]">OVERRIDE DELTA</div>
|
|
873
|
+
{preview.overrideDelta.join(' · ')}
|
|
874
|
+
</div>
|
|
875
|
+
)}
|
|
876
|
+
|
|
877
|
+
{preview.denyExamples && preview.denyExamples.length > 0 && (
|
|
878
|
+
<div className="mt-2 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-1.5 text-[8px] text-[var(--color-text-muted,#6b7280)]">
|
|
879
|
+
<div className="mb-1 tracking-widest text-[var(--color-text-faint,#9ca3af)]">EXPECTED DENY EXAMPLES</div>
|
|
880
|
+
<ul className="space-y-0.5">
|
|
881
|
+
{preview.denyExamples.map((deny) => (
|
|
882
|
+
<li key={`${deny.code}-${deny.message.slice(0, 12)}`}>• {deny.code}: {deny.message}</li>
|
|
883
|
+
))}
|
|
884
|
+
</ul>
|
|
885
|
+
</div>
|
|
886
|
+
)}
|
|
887
|
+
|
|
888
|
+
{preview.warnings.length > 0 && (
|
|
889
|
+
<div className="mt-2 flex items-start gap-1.5 text-[8px] text-[var(--color-warning,#ff4d00)]">
|
|
890
|
+
<ShieldAlert size={10} className="mt-[1px]" />
|
|
891
|
+
<span>{preview.warnings.join(' | ')}</span>
|
|
892
|
+
</div>
|
|
893
|
+
)}
|
|
894
|
+
</div>
|
|
895
|
+
)}
|
|
896
|
+
|
|
897
|
+
{issuedToken && (
|
|
898
|
+
<div className="mt-3 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
|
|
899
|
+
<div className="mb-1 flex items-center justify-between">
|
|
900
|
+
<div className="text-[8px] tracking-widest text-[var(--color-success,#16a34a)]">API KEY ISSUED</div>
|
|
901
|
+
<Button
|
|
902
|
+
variant="ghost"
|
|
903
|
+
size="sm"
|
|
904
|
+
onClick={() => { void handleCopyToken(); }}
|
|
905
|
+
icon={<Copy size={10} />}
|
|
906
|
+
>
|
|
907
|
+
{copied ? 'COPIED' : 'COPY'}
|
|
908
|
+
</Button>
|
|
909
|
+
</div>
|
|
910
|
+
{issuedMeta && (
|
|
911
|
+
<div className="mb-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">{issuedMeta}</div>
|
|
912
|
+
)}
|
|
913
|
+
<code className="block max-h-20 overflow-y-auto break-all bg-[var(--color-background,#f4f4f5)] px-2 py-1 text-[9px] text-[var(--color-text,#0a0a0a)]">
|
|
914
|
+
{issuedToken}
|
|
915
|
+
</code>
|
|
916
|
+
</div>
|
|
917
|
+
)}
|
|
918
|
+
</section>
|
|
919
|
+
|
|
920
|
+
<section className="order-2 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
|
|
921
|
+
<div className="mb-2 flex items-center justify-between">
|
|
922
|
+
<div className="text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">PROFILE BUILDER</div>
|
|
923
|
+
{editingProfileId && (
|
|
924
|
+
<Button variant="ghost" size="sm" onClick={resetProfileForm}>
|
|
925
|
+
NEW
|
|
926
|
+
</Button>
|
|
927
|
+
)}
|
|
928
|
+
</div>
|
|
929
|
+
|
|
930
|
+
<div className="grid grid-cols-1 gap-2">
|
|
931
|
+
<TextInput
|
|
932
|
+
label="Profile Name"
|
|
933
|
+
value={profileName}
|
|
934
|
+
onChange={(event) => setProfileName(event.target.value)}
|
|
935
|
+
placeholder="e.g. CI deploy scoped"
|
|
936
|
+
compact
|
|
937
|
+
/>
|
|
938
|
+
|
|
939
|
+
<FilterDropdown
|
|
940
|
+
label="Load from template"
|
|
941
|
+
options={BUILTIN_PROFILE_OPTIONS.map((profile) => ({ value: profile.id, label: `${profile.label} — ${profile.description}` }))}
|
|
942
|
+
value={profileBase}
|
|
943
|
+
onChange={(next) => {
|
|
944
|
+
setProfileBase(next);
|
|
945
|
+
applyBaseTemplate(next);
|
|
946
|
+
}}
|
|
947
|
+
compact
|
|
948
|
+
/>
|
|
949
|
+
{profileBase && PROFILE_DESCRIPTIONS[profileBase] && (
|
|
950
|
+
<div className="px-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
|
|
951
|
+
{PROFILE_DESCRIPTIONS[profileBase]}
|
|
952
|
+
</div>
|
|
953
|
+
)}
|
|
954
|
+
|
|
955
|
+
<div className="grid grid-cols-2 gap-2">
|
|
956
|
+
<TextInput
|
|
957
|
+
label="TTL Override"
|
|
958
|
+
value={profileTtlSeconds}
|
|
959
|
+
onChange={(event) => setProfileTtlSeconds(event.target.value)}
|
|
960
|
+
placeholder="seconds"
|
|
961
|
+
compact
|
|
962
|
+
/>
|
|
963
|
+
<TextInput
|
|
964
|
+
label="Max Reads"
|
|
965
|
+
value={profileMaxReads}
|
|
966
|
+
onChange={(event) => setProfileMaxReads(event.target.value)}
|
|
967
|
+
placeholder="count"
|
|
968
|
+
compact
|
|
969
|
+
/>
|
|
970
|
+
</div>
|
|
971
|
+
|
|
972
|
+
<div className="space-y-1">
|
|
973
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
|
|
974
|
+
<FilterDropdown
|
|
975
|
+
label="Scope"
|
|
976
|
+
options={TOKEN_PERMISSION_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
|
|
977
|
+
value={profileScopeCandidate}
|
|
978
|
+
onChange={setProfileScopeCandidate}
|
|
979
|
+
compact
|
|
980
|
+
/>
|
|
981
|
+
<Button
|
|
982
|
+
variant="secondary"
|
|
983
|
+
size="sm"
|
|
984
|
+
onClick={() => addProfileScope(profileScopeCandidate)}
|
|
985
|
+
>
|
|
986
|
+
ADD
|
|
987
|
+
</Button>
|
|
988
|
+
</div>
|
|
989
|
+
{profileScopes.length === 0 ? (
|
|
990
|
+
<div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
991
|
+
Using base profile scope defaults.
|
|
992
|
+
</div>
|
|
993
|
+
) : (
|
|
994
|
+
<div className="flex flex-wrap gap-1">
|
|
995
|
+
{profileScopes.map((scope) => (
|
|
996
|
+
<span
|
|
997
|
+
key={`scope-${scope}`}
|
|
998
|
+
className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
|
|
999
|
+
>
|
|
1000
|
+
{scope}
|
|
1001
|
+
<Button
|
|
1002
|
+
variant="ghost"
|
|
1003
|
+
size="sm"
|
|
1004
|
+
onClick={() => removeProfileScope(scope)}
|
|
1005
|
+
className="h-4 px-1 text-[8px]"
|
|
1006
|
+
>
|
|
1007
|
+
X
|
|
1008
|
+
</Button>
|
|
1009
|
+
</span>
|
|
1010
|
+
))}
|
|
1011
|
+
</div>
|
|
1012
|
+
)}
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1016
|
+
Scope uses the same permission vocabulary as Issue API Key (for example: <code>secret:read</code>, <code>secret:write</code>, <code>wallet:create:hot</code>, <code>send:hot</code>).
|
|
1017
|
+
</div>
|
|
1018
|
+
|
|
1019
|
+
<TextInput
|
|
1020
|
+
label="Exclude Fields (CSV)"
|
|
1021
|
+
value={profileExcludeFields}
|
|
1022
|
+
onChange={(event) => setProfileExcludeFields(event.target.value)}
|
|
1023
|
+
placeholder="password, seedPhrase"
|
|
1024
|
+
compact
|
|
1025
|
+
/>
|
|
1026
|
+
|
|
1027
|
+
{vaultScopeOptions.length > 1 && (
|
|
1028
|
+
<div className="space-y-1">
|
|
1029
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
|
|
1030
|
+
<FilterDropdown
|
|
1031
|
+
label="Vault Read/Write Scope"
|
|
1032
|
+
options={vaultScopeOptions}
|
|
1033
|
+
value={vaultScopeOptions[0]?.value || 'vault:*'}
|
|
1034
|
+
onChange={(value) => {
|
|
1035
|
+
if (!profileVaultScopes.includes(value)) {
|
|
1036
|
+
setProfileVaultScopes((prev) => [...prev, value]);
|
|
1037
|
+
}
|
|
1038
|
+
}}
|
|
1039
|
+
compact
|
|
1040
|
+
/>
|
|
1041
|
+
<Button
|
|
1042
|
+
variant="secondary"
|
|
1043
|
+
size="sm"
|
|
1044
|
+
onClick={() => setProfileVaultScopes([])}
|
|
1045
|
+
>
|
|
1046
|
+
CLEAR
|
|
1047
|
+
</Button>
|
|
1048
|
+
</div>
|
|
1049
|
+
{profileVaultScopes.length === 0 ? (
|
|
1050
|
+
<div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1051
|
+
Using base profile vault defaults.
|
|
1052
|
+
</div>
|
|
1053
|
+
) : (
|
|
1054
|
+
<div className="flex flex-wrap gap-1">
|
|
1055
|
+
{profileVaultScopes.map((scope) => (
|
|
1056
|
+
<span
|
|
1057
|
+
key={`pvs-${scope}`}
|
|
1058
|
+
className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
|
|
1059
|
+
>
|
|
1060
|
+
{scope}
|
|
1061
|
+
<Button
|
|
1062
|
+
variant="ghost"
|
|
1063
|
+
size="sm"
|
|
1064
|
+
onClick={() => setProfileVaultScopes((prev) => prev.filter((s) => s !== scope))}
|
|
1065
|
+
className="h-4 px-1 text-[8px]"
|
|
1066
|
+
>
|
|
1067
|
+
X
|
|
1068
|
+
</Button>
|
|
1069
|
+
</span>
|
|
1070
|
+
))}
|
|
1071
|
+
</div>
|
|
1072
|
+
)}
|
|
1073
|
+
<div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1074
|
+
Restricts which vaults this profile can read/write credentials from. Select specific vaults or leave empty for profile defaults.
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
)}
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
{profileError && (
|
|
1081
|
+
<div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{profileError}</div>
|
|
1082
|
+
)}
|
|
1083
|
+
{profileNotice && (
|
|
1084
|
+
<div className="mt-2 text-[9px] text-[var(--color-success,#16a34a)]">{profileNotice}</div>
|
|
1085
|
+
)}
|
|
1086
|
+
|
|
1087
|
+
<div className="mt-3 flex gap-2">
|
|
1088
|
+
<Button size="sm" onClick={handleSaveProfile} icon={<ShieldCheck size={11} />}>
|
|
1089
|
+
{editingProfileId ? 'UPDATE PROFILE' : 'CREATE PROFILE'}
|
|
1090
|
+
</Button>
|
|
1091
|
+
{editingProfileId && (
|
|
1092
|
+
<Button variant="secondary" size="sm" onClick={resetProfileForm}>
|
|
1093
|
+
CANCEL
|
|
1094
|
+
</Button>
|
|
1095
|
+
)}
|
|
1096
|
+
</div>
|
|
1097
|
+
|
|
1098
|
+
<div className="mt-4 border-t border-[var(--color-border,#d4d4d8)] pt-3">
|
|
1099
|
+
<div className="mb-2 text-[9px] font-bold tracking-widest text-[var(--color-text-muted,#6b7280)]">
|
|
1100
|
+
SAVED PROFILES ({profiles.length})
|
|
1101
|
+
</div>
|
|
1102
|
+
{profiles.length === 0 ? (
|
|
1103
|
+
<div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No custom profiles saved yet.</div>
|
|
1104
|
+
) : (
|
|
1105
|
+
<div className="space-y-2">
|
|
1106
|
+
{profiles.map((profile) => (
|
|
1107
|
+
<div key={profile.id} className="flex items-center justify-between border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-2 py-1.5">
|
|
1108
|
+
<Button
|
|
1109
|
+
variant="ghost"
|
|
1110
|
+
size="sm"
|
|
1111
|
+
onClick={() => handleEditProfile(profile)}
|
|
1112
|
+
className="h-auto min-h-0 w-full justify-start px-0 py-0 text-left hover:bg-transparent"
|
|
1113
|
+
>
|
|
1114
|
+
<span className="block">
|
|
1115
|
+
<span className="block text-[10px] text-[var(--color-text,#0a0a0a)]">{profile.name}</span>
|
|
1116
|
+
<span className="block text-[8px] text-[var(--color-text-faint,#9ca3af)]">{profile.profile}@{profile.profileVersion}</span>
|
|
1117
|
+
</span>
|
|
1118
|
+
</Button>
|
|
1119
|
+
<Button
|
|
1120
|
+
variant="ghost"
|
|
1121
|
+
size="sm"
|
|
1122
|
+
onClick={() => handleDeleteProfile(profile.id)}
|
|
1123
|
+
icon={<Trash2 size={10} />}
|
|
1124
|
+
>
|
|
1125
|
+
{''}
|
|
1126
|
+
</Button>
|
|
1127
|
+
</div>
|
|
1128
|
+
))}
|
|
1129
|
+
</div>
|
|
1130
|
+
)}
|
|
1131
|
+
</div>
|
|
1132
|
+
</section>
|
|
1133
|
+
</div>
|
|
1134
|
+
|
|
1135
|
+
<div className="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-2">
|
|
1136
|
+
<section className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
|
|
1137
|
+
<div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">PENDING AUTH REQUESTS</div>
|
|
1138
|
+
{requestNotice && (
|
|
1139
|
+
<div className="mb-2 text-[9px] text-[var(--color-text-muted,#6b7280)]">{requestNotice}</div>
|
|
1140
|
+
)}
|
|
1141
|
+
{pendingAuthRequests.length === 0 ? (
|
|
1142
|
+
<div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No pending auth requests.</div>
|
|
1143
|
+
) : (
|
|
1144
|
+
<div className="space-y-2">
|
|
1145
|
+
{pendingAuthRequests.map((request) => {
|
|
1146
|
+
const metadata = parseMetadata(request.metadata);
|
|
1147
|
+
const agentId = typeof metadata.agentId === 'string' ? metadata.agentId : 'unknown-agent';
|
|
1148
|
+
const profileObj = typeof metadata.profile === 'object' && metadata.profile && 'id' in metadata.profile
|
|
1149
|
+
? metadata.profile as Record<string, unknown>
|
|
1150
|
+
: null;
|
|
1151
|
+
const profileLabel = profileObj
|
|
1152
|
+
? `${String(profileObj.id)}@${String(profileObj.version || 'v1')}`
|
|
1153
|
+
: 'n/a';
|
|
1154
|
+
const profileDesc = profileObj ? describeProfileId(String(profileObj.id)) : null;
|
|
1155
|
+
const perms = Array.isArray(metadata.permissions) ? metadata.permissions as string[] : [];
|
|
1156
|
+
const resolving = actionLoading === `resolve-${request.id}`;
|
|
1157
|
+
return (
|
|
1158
|
+
<div key={request.id} className="rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
|
|
1159
|
+
<div className="text-[9px] text-[var(--color-text,#0a0a0a)]">
|
|
1160
|
+
{agentId} · {request.type}
|
|
1161
|
+
</div>
|
|
1162
|
+
<div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1163
|
+
{profileLabel} · {new Date(request.createdAt).toLocaleString()}
|
|
1164
|
+
</div>
|
|
1165
|
+
{profileDesc && (
|
|
1166
|
+
<div className="mt-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
|
|
1167
|
+
{profileDesc}
|
|
1168
|
+
</div>
|
|
1169
|
+
)}
|
|
1170
|
+
{perms.length > 0 && (
|
|
1171
|
+
<div className="mt-1 flex flex-wrap gap-1">
|
|
1172
|
+
{perms.slice(0, 6).map((p) => (
|
|
1173
|
+
<span key={p} className="border border-[var(--color-border,#d4d4d8)] px-1 py-0.5 text-[7px] text-[var(--color-text-muted,#6b7280)]">
|
|
1174
|
+
{String(p)}
|
|
1175
|
+
</span>
|
|
1176
|
+
))}
|
|
1177
|
+
{perms.length > 6 && (
|
|
1178
|
+
<span className="px-1 py-0.5 text-[7px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1179
|
+
+{perms.length - 6} more
|
|
1180
|
+
</span>
|
|
1181
|
+
)}
|
|
1182
|
+
</div>
|
|
1183
|
+
)}
|
|
1184
|
+
<div className="mt-2 flex gap-2">
|
|
1185
|
+
<Button
|
|
1186
|
+
size="sm"
|
|
1187
|
+
onClick={() => { void handleResolveRequest(request.id, true); }}
|
|
1188
|
+
loading={resolving}
|
|
1189
|
+
icon={!resolving ? <Check size={10} /> : undefined}
|
|
1190
|
+
>
|
|
1191
|
+
APPROVE
|
|
1192
|
+
</Button>
|
|
1193
|
+
<Button
|
|
1194
|
+
size="sm"
|
|
1195
|
+
variant="secondary"
|
|
1196
|
+
onClick={() => { void handleResolveRequest(request.id, false); }}
|
|
1197
|
+
disabled={resolving}
|
|
1198
|
+
icon={<AlertTriangle size={10} />}
|
|
1199
|
+
>
|
|
1200
|
+
REJECT
|
|
1201
|
+
</Button>
|
|
1202
|
+
</div>
|
|
1203
|
+
</div>
|
|
1204
|
+
);
|
|
1205
|
+
})}
|
|
1206
|
+
</div>
|
|
1207
|
+
)}
|
|
1208
|
+
</section>
|
|
1209
|
+
|
|
1210
|
+
<section className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
|
|
1211
|
+
<div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">ISSUED API KEYS</div>
|
|
1212
|
+
|
|
1213
|
+
<div className="mb-2 text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
|
|
1214
|
+
ACTIVE ({managedActiveTokens.length})
|
|
1215
|
+
</div>
|
|
1216
|
+
{managedActiveTokens.length === 0 ? (
|
|
1217
|
+
<div className="mb-3 text-[9px] text-[var(--color-text-faint,#9ca3af)]">No active non-admin tokens.</div>
|
|
1218
|
+
) : (
|
|
1219
|
+
<div className="mb-3 space-y-2">
|
|
1220
|
+
{managedActiveTokens.map((token) => {
|
|
1221
|
+
const revoking = actionLoading === `revoke-${token.tokenHash}`;
|
|
1222
|
+
return (
|
|
1223
|
+
<div key={token.tokenHash} className="rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
|
|
1224
|
+
<div className="flex items-center justify-between">
|
|
1225
|
+
<div className="text-[9px] text-[var(--color-text,#0a0a0a)]">{token.agentId}</div>
|
|
1226
|
+
<div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">{shortHash(token.tokenHash)}</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
<div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1229
|
+
perms: {token.permissions.length} · expires: {new Date(token.expiresAt).toLocaleString()}
|
|
1230
|
+
</div>
|
|
1231
|
+
<div className="mt-2">
|
|
1232
|
+
<Button
|
|
1233
|
+
size="sm"
|
|
1234
|
+
variant="secondary"
|
|
1235
|
+
onClick={() => { void onRevokeToken(token.tokenHash); }}
|
|
1236
|
+
disabled={revoking}
|
|
1237
|
+
icon={revoking ? <Loader2 size={10} className="animate-spin" /> : <Trash2 size={10} />}
|
|
1238
|
+
>
|
|
1239
|
+
REVOKE
|
|
1240
|
+
</Button>
|
|
1241
|
+
</div>
|
|
1242
|
+
</div>
|
|
1243
|
+
);
|
|
1244
|
+
})}
|
|
1245
|
+
</div>
|
|
1246
|
+
)}
|
|
1247
|
+
|
|
1248
|
+
<div className="mb-2 text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
|
|
1249
|
+
INACTIVE ({inactiveTokens.length})
|
|
1250
|
+
</div>
|
|
1251
|
+
{inactiveTokens.length === 0 ? (
|
|
1252
|
+
<div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No inactive tokens.</div>
|
|
1253
|
+
) : (
|
|
1254
|
+
<div className="space-y-1.5">
|
|
1255
|
+
{inactiveTokens.slice(0, 8).map((token) => (
|
|
1256
|
+
<div key={`${token.agentId}-${token.tokenHash}`} className="flex items-center justify-between rounded border border-[var(--color-border,#d4d4d8)] px-2 py-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
|
|
1257
|
+
<span>{token.agentId}</span>
|
|
1258
|
+
<span>{shortHash(token.tokenHash)}</span>
|
|
1259
|
+
</div>
|
|
1260
|
+
))}
|
|
1261
|
+
{inactiveTokens.length > 8 && (
|
|
1262
|
+
<div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">
|
|
1263
|
+
+{inactiveTokens.length - 8} more inactive tokens
|
|
1264
|
+
</div>
|
|
1265
|
+
)}
|
|
1266
|
+
</div>
|
|
1267
|
+
)}
|
|
1268
|
+
</section>
|
|
1269
|
+
</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
);
|
|
1272
|
+
};
|