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,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token price lookup with cascading fallback:
|
|
3
|
+
* DexScreener → CoinGecko → Alchemy (if key exists)
|
|
4
|
+
*
|
|
5
|
+
* In-memory cache with 60-second TTL. No DB writes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isSolanaChain, normalizeAddress, getNativeCurrency } from './address';
|
|
9
|
+
import { getAlchemyKey, ALCHEMY_PATHS } from './config';
|
|
10
|
+
import { getEthToUsd, getSolToUsd } from './prices';
|
|
11
|
+
|
|
12
|
+
export interface PriceResult {
|
|
13
|
+
priceUsd: string;
|
|
14
|
+
source: string;
|
|
15
|
+
cached: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CacheEntry {
|
|
19
|
+
priceUsd: string;
|
|
20
|
+
source: string;
|
|
21
|
+
fetchedAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CACHE_TTL_MS = 60_000; // 60 seconds
|
|
25
|
+
|
|
26
|
+
const priceCache = new Map<string, CacheEntry>();
|
|
27
|
+
|
|
28
|
+
/** Clear cache — exposed for tests */
|
|
29
|
+
export function clearPriceCache(): void {
|
|
30
|
+
priceCache.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// CoinGecko platform ID mapping
|
|
34
|
+
const COINGECKO_PLATFORMS: Record<string, string> = {
|
|
35
|
+
base: 'base',
|
|
36
|
+
ethereum: 'ethereum',
|
|
37
|
+
solana: 'solana',
|
|
38
|
+
polygon: 'polygon-pos',
|
|
39
|
+
arbitrum: 'arbitrum-one',
|
|
40
|
+
optimism: 'optimistic-ethereum',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get USD price for a token. Returns null if no source has a price.
|
|
45
|
+
*/
|
|
46
|
+
export async function getTokenPrice(address: string, chain: string): Promise<PriceResult | null> {
|
|
47
|
+
// Native token shortcut — use existing cached prices from cron
|
|
48
|
+
if (address === 'native') {
|
|
49
|
+
return getNativePrice(chain);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const normalized = normalizeAddress(address, chain);
|
|
53
|
+
const cacheKey = `${chain}:${normalized}`;
|
|
54
|
+
|
|
55
|
+
// Check cache
|
|
56
|
+
const cached = priceCache.get(cacheKey);
|
|
57
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
58
|
+
return { priceUsd: cached.priceUsd, source: cached.source, cached: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Cascading fallback
|
|
62
|
+
let result = await fetchDexScreener(normalized, chain);
|
|
63
|
+
if (!result) result = await fetchCoinGecko(normalized, chain);
|
|
64
|
+
if (!result) result = await fetchAlchemy(normalized, chain);
|
|
65
|
+
|
|
66
|
+
if (!result) return null;
|
|
67
|
+
|
|
68
|
+
// Cache the result
|
|
69
|
+
priceCache.set(cacheKey, {
|
|
70
|
+
priceUsd: result.priceUsd,
|
|
71
|
+
source: result.source,
|
|
72
|
+
fetchedAt: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return { ...result, cached: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Batch price lookup — efficient for portfolio valuation.
|
|
80
|
+
* Uses CoinGecko batch (comma-separated per platform) first,
|
|
81
|
+
* then DexScreener in parallel for misses, then Alchemy batch.
|
|
82
|
+
*/
|
|
83
|
+
export async function getTokenPrices(
|
|
84
|
+
tokens: { address: string; chain: string }[],
|
|
85
|
+
): Promise<Map<string, PriceResult>> {
|
|
86
|
+
const results = new Map<string, PriceResult>();
|
|
87
|
+
if (tokens.length === 0) return results;
|
|
88
|
+
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const misses: { address: string; chain: string; normalized: string; cacheKey: string }[] = [];
|
|
91
|
+
|
|
92
|
+
// 1. Check cache
|
|
93
|
+
for (const { address, chain } of tokens) {
|
|
94
|
+
const normalized = normalizeAddress(address, chain);
|
|
95
|
+
const cacheKey = `${chain}:${normalized}`;
|
|
96
|
+
const cached = priceCache.get(cacheKey);
|
|
97
|
+
if (cached && now - cached.fetchedAt < CACHE_TTL_MS) {
|
|
98
|
+
results.set(cacheKey, { priceUsd: cached.priceUsd, source: cached.source, cached: true });
|
|
99
|
+
} else {
|
|
100
|
+
misses.push({ address, chain, normalized, cacheKey });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (misses.length === 0) return results;
|
|
105
|
+
|
|
106
|
+
// 2. CoinGecko batch — group by platform, comma-separate addresses
|
|
107
|
+
const remaining = await batchCoinGecko(misses, results);
|
|
108
|
+
|
|
109
|
+
// 3. DexScreener in parallel for CoinGecko misses
|
|
110
|
+
const afterDex = await batchDexScreener(remaining, results);
|
|
111
|
+
|
|
112
|
+
// 4. Alchemy batch for remaining misses
|
|
113
|
+
await batchAlchemy(afterDex, results);
|
|
114
|
+
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** CoinGecko batch: one API call per platform with comma-separated addresses */
|
|
119
|
+
async function batchCoinGecko(
|
|
120
|
+
tokens: { address: string; chain: string; normalized: string; cacheKey: string }[],
|
|
121
|
+
results: Map<string, PriceResult>,
|
|
122
|
+
): Promise<typeof tokens> {
|
|
123
|
+
// Group by platform
|
|
124
|
+
const byPlatform = new Map<string, typeof tokens>();
|
|
125
|
+
const unsupported: typeof tokens = [];
|
|
126
|
+
|
|
127
|
+
for (const t of tokens) {
|
|
128
|
+
const platformId = COINGECKO_PLATFORMS[t.chain];
|
|
129
|
+
if (!platformId) {
|
|
130
|
+
unsupported.push(t);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const group = byPlatform.get(platformId) || [];
|
|
134
|
+
group.push(t);
|
|
135
|
+
byPlatform.set(platformId, group);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const remaining = [...unsupported];
|
|
139
|
+
|
|
140
|
+
await Promise.all(
|
|
141
|
+
Array.from(byPlatform.entries()).map(async ([platformId, group]) => {
|
|
142
|
+
const addresses = group.map((t) => t.normalized).join(',');
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(
|
|
145
|
+
`https://api.coingecko.com/api/v3/simple/token_price/${platformId}?contract_addresses=${addresses}&vs_currencies=usd`,
|
|
146
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
147
|
+
);
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
remaining.push(...group);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
|
|
154
|
+
for (const t of group) {
|
|
155
|
+
const price = data[t.normalized.toLowerCase()]?.usd;
|
|
156
|
+
if (price !== undefined && price !== null) {
|
|
157
|
+
const entry = { priceUsd: price.toString(), source: 'coingecko' as const };
|
|
158
|
+
results.set(t.cacheKey, { ...entry, cached: false });
|
|
159
|
+
priceCache.set(t.cacheKey, { ...entry, fetchedAt: Date.now() });
|
|
160
|
+
} else {
|
|
161
|
+
remaining.push(t);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
remaining.push(...group);
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return remaining;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** DexScreener: one API call per token, run in parallel */
|
|
174
|
+
async function batchDexScreener(
|
|
175
|
+
tokens: { address: string; chain: string; normalized: string; cacheKey: string }[],
|
|
176
|
+
results: Map<string, PriceResult>,
|
|
177
|
+
): Promise<typeof tokens> {
|
|
178
|
+
if (tokens.length === 0) return [];
|
|
179
|
+
|
|
180
|
+
const remaining: typeof tokens = [];
|
|
181
|
+
|
|
182
|
+
await Promise.all(
|
|
183
|
+
tokens.map(async (t) => {
|
|
184
|
+
const result = await fetchDexScreener(t.normalized, t.chain);
|
|
185
|
+
if (result) {
|
|
186
|
+
results.set(t.cacheKey, { ...result, cached: false });
|
|
187
|
+
priceCache.set(t.cacheKey, { ...result, fetchedAt: Date.now() });
|
|
188
|
+
} else {
|
|
189
|
+
remaining.push(t);
|
|
190
|
+
}
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return remaining;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Alchemy batch: one API call with multiple addresses (EVM only) */
|
|
198
|
+
async function batchAlchemy(
|
|
199
|
+
tokens: { address: string; chain: string; normalized: string; cacheKey: string }[],
|
|
200
|
+
results: Map<string, PriceResult>,
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
if (tokens.length === 0) return;
|
|
203
|
+
|
|
204
|
+
const apiKey = await getAlchemyKey();
|
|
205
|
+
if (!apiKey) return;
|
|
206
|
+
|
|
207
|
+
// Filter to EVM only
|
|
208
|
+
const evmTokens = tokens.filter((t) => !isSolanaChain(t.chain) && ALCHEMY_PATHS[t.chain]);
|
|
209
|
+
if (evmTokens.length === 0) return;
|
|
210
|
+
|
|
211
|
+
const addressPayload = evmTokens.map((t) => ({
|
|
212
|
+
network: ALCHEMY_PATHS[t.chain].path,
|
|
213
|
+
address: t.normalized,
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetch(
|
|
218
|
+
`https://api.g.alchemy.com/prices/v1/${apiKey}/tokens/by-address`,
|
|
219
|
+
{
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({ addresses: addressPayload }),
|
|
223
|
+
signal: AbortSignal.timeout(5000),
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
if (!res.ok) return;
|
|
227
|
+
const data = await res.json();
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < evmTokens.length; i++) {
|
|
230
|
+
const priceEntry = data.data?.[i]?.prices?.find(
|
|
231
|
+
(p: any) => p.currency === 'usd' || p.currency === 'USD',
|
|
232
|
+
);
|
|
233
|
+
if (priceEntry?.value) {
|
|
234
|
+
const entry = { priceUsd: priceEntry.value, source: 'alchemy' as const };
|
|
235
|
+
results.set(evmTokens[i].cacheKey, { ...entry, cached: false });
|
|
236
|
+
priceCache.set(evmTokens[i].cacheKey, { ...entry, fetchedAt: Date.now() });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Alchemy failed — prices just won't be available for these tokens
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function getNativePrice(chain: string): Promise<PriceResult | null> {
|
|
245
|
+
const currency = getNativeCurrency(chain);
|
|
246
|
+
const price = currency === 'SOL' ? await getSolToUsd() : await getEthToUsd();
|
|
247
|
+
if (price === null) return null;
|
|
248
|
+
return { priceUsd: price.toString(), source: 'cache', cached: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* DexScreener: free, 300 req/min, no key needed
|
|
253
|
+
*/
|
|
254
|
+
async function fetchDexScreener(address: string, chain: string): Promise<{ priceUsd: string; source: string } | null> {
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(
|
|
257
|
+
`https://api.dexscreener.com/latest/dex/tokens/${address}`,
|
|
258
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
259
|
+
);
|
|
260
|
+
if (!res.ok) return null;
|
|
261
|
+
const data = await res.json();
|
|
262
|
+
|
|
263
|
+
const pairs = data.pairs
|
|
264
|
+
?.filter((p: any) => p.chainId === chain)
|
|
265
|
+
?.sort((a: any, b: any) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0));
|
|
266
|
+
|
|
267
|
+
const best = pairs?.[0];
|
|
268
|
+
if (!best?.priceUsd) return null;
|
|
269
|
+
|
|
270
|
+
return { priceUsd: best.priceUsd, source: 'dexscreener' };
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* CoinGecko: free tier, 5-15 req/min, no key needed
|
|
278
|
+
*/
|
|
279
|
+
async function fetchCoinGecko(address: string, chain: string): Promise<{ priceUsd: string; source: string } | null> {
|
|
280
|
+
const platformId = COINGECKO_PLATFORMS[chain];
|
|
281
|
+
if (!platformId) return null;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(
|
|
285
|
+
`https://api.coingecko.com/api/v3/simple/token_price/${platformId}?contract_addresses=${address}&vs_currencies=usd`,
|
|
286
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
287
|
+
);
|
|
288
|
+
if (!res.ok) return null;
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
|
|
291
|
+
const key = address.toLowerCase();
|
|
292
|
+
const price = data[key]?.usd;
|
|
293
|
+
if (price === undefined || price === null) return null;
|
|
294
|
+
|
|
295
|
+
return { priceUsd: price.toString(), source: 'coingecko' };
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Alchemy: requires API key, EVM only
|
|
303
|
+
*/
|
|
304
|
+
async function fetchAlchemy(address: string, chain: string): Promise<{ priceUsd: string; source: string } | null> {
|
|
305
|
+
// Skip for Solana chains — Alchemy price API is EVM only
|
|
306
|
+
if (isSolanaChain(chain)) return null;
|
|
307
|
+
|
|
308
|
+
const apiKey = await getAlchemyKey();
|
|
309
|
+
if (!apiKey) return null;
|
|
310
|
+
|
|
311
|
+
const alchemyConfig = ALCHEMY_PATHS[chain];
|
|
312
|
+
if (!alchemyConfig) return null;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const res = await fetch(
|
|
316
|
+
`https://api.g.alchemy.com/prices/v1/${apiKey}/tokens/by-address`,
|
|
317
|
+
{
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
addresses: [{ network: alchemyConfig.path, address }],
|
|
322
|
+
}),
|
|
323
|
+
signal: AbortSignal.timeout(5000),
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
if (!res.ok) return null;
|
|
327
|
+
const data = await res.json();
|
|
328
|
+
|
|
329
|
+
const priceEntry = data.data?.[0]?.prices?.find(
|
|
330
|
+
(p: any) => p.currency === 'usd' || p.currency === 'USD',
|
|
331
|
+
);
|
|
332
|
+
if (!priceEntry?.value) return null;
|
|
333
|
+
|
|
334
|
+
return { priceUsd: priceEntry.value, source: 'alchemy' };
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price helpers — read cached native currency prices from DB.
|
|
3
|
+
* Prices are written by the cron server's native-price job.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { prisma } from './db';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get cached ETH→USD price. Returns null if no cached price exists.
|
|
10
|
+
*/
|
|
11
|
+
export async function getEthToUsd(): Promise<number | null> {
|
|
12
|
+
try {
|
|
13
|
+
const row = await prisma.nativePrice.findUnique({ where: { currency: 'ETH' } });
|
|
14
|
+
if (!row) return null;
|
|
15
|
+
const price = parseFloat(row.priceUsd);
|
|
16
|
+
return isNaN(price) ? null : price;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get cached SOL→USD price. Returns null if no cached price exists.
|
|
24
|
+
*/
|
|
25
|
+
export async function getSolToUsd(): Promise<number | null> {
|
|
26
|
+
try {
|
|
27
|
+
const row = await prisma.nativePrice.findUnique({ where: { currency: 'SOL' } });
|
|
28
|
+
if (!row) return null;
|
|
29
|
+
const price = parseFloat(row.priceUsd);
|
|
30
|
+
return isNaN(price) ? null : price;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
export type ProjectScopeCode =
|
|
7
|
+
| 'PROJECT_SCOPE_MISSING_AURA'
|
|
8
|
+
| 'PROJECT_SCOPE_INVALID_AURA'
|
|
9
|
+
| 'PROJECT_SCOPE_DENIED'
|
|
10
|
+
| 'PROJECT_SCOPE_OVERRIDE_USED';
|
|
11
|
+
|
|
12
|
+
export type ProjectScopeMode = 'auto' | 'strict' | 'off';
|
|
13
|
+
|
|
14
|
+
export interface ScopeCandidate {
|
|
15
|
+
id?: string;
|
|
16
|
+
name: string;
|
|
17
|
+
vaultName: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ScopeDecision {
|
|
21
|
+
allowed: boolean;
|
|
22
|
+
code: ProjectScopeCode | null;
|
|
23
|
+
remediation: string;
|
|
24
|
+
projectScopeMode: ProjectScopeMode;
|
|
25
|
+
normalizedIdentity: { vaultName: string | null; credentialName: string };
|
|
26
|
+
projectRoot: string | null;
|
|
27
|
+
auraFingerprint: string | null;
|
|
28
|
+
allowedCandidates: ScopeCandidate[];
|
|
29
|
+
overrideUsed?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ParsedRef {
|
|
33
|
+
vaultName: string | null;
|
|
34
|
+
credentialName: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalize(str: string): string {
|
|
38
|
+
return str.trim().toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isTruthyFlag(value: string | undefined): boolean {
|
|
42
|
+
if (!value) return false;
|
|
43
|
+
const normalized = value.trim().toLowerCase();
|
|
44
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function shouldEmitProjectScopeEvent(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
48
|
+
return isTruthyFlag(env.AURA_PROJECT_SCOPE_DEBUG);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseReference(ref: string): ParsedRef {
|
|
52
|
+
if (ref.startsWith('@')) {
|
|
53
|
+
const parts = ref.slice(1).split('/');
|
|
54
|
+
if (parts.length < 3 || !parts[0] || !parts[1]) {
|
|
55
|
+
throw new Error(`Invalid vault reference: ${ref}`);
|
|
56
|
+
}
|
|
57
|
+
return { vaultName: parts[0], credentialName: parts[1] };
|
|
58
|
+
}
|
|
59
|
+
const parts = ref.split('/');
|
|
60
|
+
if (parts.length < 2 || !parts[0]) {
|
|
61
|
+
throw new Error(`Invalid reference: ${ref}`);
|
|
62
|
+
}
|
|
63
|
+
return { vaultName: null, credentialName: parts[0] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseAuraAllowlist(auraPath: string): ParsedRef[] {
|
|
67
|
+
const content = fs.readFileSync(auraPath, 'utf-8');
|
|
68
|
+
const refs: ParsedRef[] = [];
|
|
69
|
+
|
|
70
|
+
for (const rawLine of content.split('\n')) {
|
|
71
|
+
const line = rawLine.trim();
|
|
72
|
+
if (!line || line.startsWith('#')) continue;
|
|
73
|
+
const idx = line.indexOf('=');
|
|
74
|
+
if (idx === -1) throw new Error(`Invalid line: ${line}`);
|
|
75
|
+
const ref = line.slice(idx + 1).trim();
|
|
76
|
+
refs.push(parseReference(ref));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return refs;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeProjectScopeMode(raw: unknown): ProjectScopeMode {
|
|
83
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
84
|
+
if (value === 'strict') return 'strict';
|
|
85
|
+
if (value === 'off') return 'off';
|
|
86
|
+
return 'auto';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findNearestAura(startDir: string): string | null {
|
|
90
|
+
let current = startDir;
|
|
91
|
+
while (true) {
|
|
92
|
+
const candidate = path.join(current, '.aura');
|
|
93
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
94
|
+
const parent = path.dirname(current);
|
|
95
|
+
if (parent === current) return null;
|
|
96
|
+
current = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveGitRoot(startDir: string): string | null {
|
|
101
|
+
try {
|
|
102
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
103
|
+
cwd: startDir,
|
|
104
|
+
encoding: 'utf-8',
|
|
105
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
106
|
+
}).trim();
|
|
107
|
+
return root || null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveAuraPath(opts: { cwd: string; projectRootOverride?: string }): { projectRoot: string | null; auraPath: string | null } {
|
|
114
|
+
const explicit = opts.projectRootOverride || process.env.AURA_PROJECT_ROOT;
|
|
115
|
+
if (explicit) {
|
|
116
|
+
const root = path.resolve(explicit);
|
|
117
|
+
return { projectRoot: root, auraPath: path.join(root, '.aura') };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const gitRoot = resolveGitRoot(opts.cwd);
|
|
121
|
+
if (gitRoot) {
|
|
122
|
+
return { projectRoot: gitRoot, auraPath: path.join(gitRoot, '.aura') };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nearestAura = findNearestAura(opts.cwd);
|
|
126
|
+
if (nearestAura) {
|
|
127
|
+
return { projectRoot: path.dirname(nearestAura), auraPath: nearestAura };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { projectRoot: null, auraPath: null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fingerprint(content: string): string {
|
|
134
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isAllowed(candidate: ScopeCandidate, refs: ParsedRef[]): boolean {
|
|
138
|
+
const candName = normalize(candidate.name);
|
|
139
|
+
const candVault = candidate.vaultName ? normalize(candidate.vaultName) : null;
|
|
140
|
+
return refs.some((ref) => {
|
|
141
|
+
if (normalize(ref.credentialName) !== candName) return false;
|
|
142
|
+
if (!ref.vaultName) return true;
|
|
143
|
+
return candVault === normalize(ref.vaultName);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function evaluateProjectScopeAccess(input: {
|
|
148
|
+
surface: 'cli_vault_get' | 'cli_env' | 'mcp_get_secret';
|
|
149
|
+
requested: { vaultName: string | null; credentialName: string };
|
|
150
|
+
candidates: ScopeCandidate[];
|
|
151
|
+
actor?: string;
|
|
152
|
+
cwd?: string;
|
|
153
|
+
projectRootOverride?: string;
|
|
154
|
+
projectScopeMode?: ProjectScopeMode;
|
|
155
|
+
}): ScopeDecision {
|
|
156
|
+
const projectScopeMode = normalizeProjectScopeMode(input.projectScopeMode || process.env.AURA_PROJECT_SCOPE_MODE || 'auto');
|
|
157
|
+
const normalizedIdentity = {
|
|
158
|
+
vaultName: input.requested.vaultName,
|
|
159
|
+
credentialName: input.requested.credentialName,
|
|
160
|
+
};
|
|
161
|
+
const bypass = process.env.AURA_PROJECT_SCOPE_BYPASS === '1';
|
|
162
|
+
|
|
163
|
+
if (bypass) {
|
|
164
|
+
return {
|
|
165
|
+
allowed: true,
|
|
166
|
+
code: 'PROJECT_SCOPE_OVERRIDE_USED',
|
|
167
|
+
remediation: 'Unset AURA_PROJECT_SCOPE_BYPASS to restore strict project scoping.',
|
|
168
|
+
projectScopeMode,
|
|
169
|
+
normalizedIdentity,
|
|
170
|
+
projectRoot: null,
|
|
171
|
+
auraFingerprint: null,
|
|
172
|
+
allowedCandidates: input.candidates,
|
|
173
|
+
overrideUsed: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (projectScopeMode === 'off') {
|
|
178
|
+
return {
|
|
179
|
+
allowed: true,
|
|
180
|
+
code: null,
|
|
181
|
+
remediation: '',
|
|
182
|
+
projectScopeMode,
|
|
183
|
+
normalizedIdentity,
|
|
184
|
+
projectRoot: null,
|
|
185
|
+
auraFingerprint: null,
|
|
186
|
+
allowedCandidates: input.candidates,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const cwd = input.cwd || process.cwd();
|
|
191
|
+
const { projectRoot, auraPath } = resolveAuraPath({ cwd, projectRootOverride: input.projectRootOverride });
|
|
192
|
+
if (!auraPath || !fs.existsSync(auraPath)) {
|
|
193
|
+
if (projectScopeMode === 'auto') {
|
|
194
|
+
return {
|
|
195
|
+
allowed: true,
|
|
196
|
+
code: null,
|
|
197
|
+
remediation: '',
|
|
198
|
+
projectScopeMode,
|
|
199
|
+
normalizedIdentity,
|
|
200
|
+
projectRoot,
|
|
201
|
+
auraFingerprint: null,
|
|
202
|
+
allowedCandidates: input.candidates,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
allowed: false,
|
|
207
|
+
code: 'PROJECT_SCOPE_MISSING_AURA',
|
|
208
|
+
remediation: 'Add a .aura file in the project root (or set AURA_PROJECT_ROOT / --project-root).',
|
|
209
|
+
projectScopeMode,
|
|
210
|
+
normalizedIdentity,
|
|
211
|
+
projectRoot,
|
|
212
|
+
auraFingerprint: null,
|
|
213
|
+
allowedCandidates: [],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let refs: ParsedRef[];
|
|
218
|
+
let auraFingerprint: string;
|
|
219
|
+
try {
|
|
220
|
+
const content = fs.readFileSync(auraPath, 'utf-8');
|
|
221
|
+
auraFingerprint = fingerprint(content);
|
|
222
|
+
refs = parseAuraAllowlist(auraPath);
|
|
223
|
+
} catch {
|
|
224
|
+
return {
|
|
225
|
+
allowed: false,
|
|
226
|
+
code: 'PROJECT_SCOPE_INVALID_AURA',
|
|
227
|
+
remediation: 'Fix .aura syntax (ENV=@vault/name/field or ENV=name/field) and retry.',
|
|
228
|
+
projectScopeMode,
|
|
229
|
+
normalizedIdentity,
|
|
230
|
+
projectRoot,
|
|
231
|
+
auraFingerprint: null,
|
|
232
|
+
allowedCandidates: [],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const allowedCandidates = input.candidates.filter((candidate) => isAllowed(candidate, refs));
|
|
237
|
+
if (allowedCandidates.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
allowed: false,
|
|
240
|
+
code: 'PROJECT_SCOPE_DENIED',
|
|
241
|
+
remediation: `Add '${input.requested.credentialName}' to .aura (or use an allowed credential).`,
|
|
242
|
+
projectScopeMode,
|
|
243
|
+
normalizedIdentity,
|
|
244
|
+
projectRoot,
|
|
245
|
+
auraFingerprint,
|
|
246
|
+
allowedCandidates: [],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!input.requested.vaultName && allowedCandidates.length > 1) {
|
|
251
|
+
return {
|
|
252
|
+
allowed: false,
|
|
253
|
+
code: 'PROJECT_SCOPE_DENIED',
|
|
254
|
+
remediation: `Credential '${input.requested.credentialName}' is mapped in multiple vaults. Re-run with explicit --vault.`,
|
|
255
|
+
projectScopeMode,
|
|
256
|
+
normalizedIdentity,
|
|
257
|
+
projectRoot,
|
|
258
|
+
auraFingerprint,
|
|
259
|
+
allowedCandidates: [],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
allowed: true,
|
|
265
|
+
code: null,
|
|
266
|
+
remediation: '',
|
|
267
|
+
projectScopeMode,
|
|
268
|
+
normalizedIdentity,
|
|
269
|
+
projectRoot,
|
|
270
|
+
auraFingerprint,
|
|
271
|
+
allowedCandidates,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function emitProjectScopeEvent(input: {
|
|
276
|
+
actor?: string;
|
|
277
|
+
surface: 'cli_vault_get' | 'cli_env' | 'mcp_get_secret';
|
|
278
|
+
requestedCredential: { vaultName: string | null; credentialName: string };
|
|
279
|
+
decision: ScopeDecision;
|
|
280
|
+
}): void {
|
|
281
|
+
if (!shouldEmitProjectScopeEvent()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const event = {
|
|
286
|
+
actor: input.actor || 'unknown',
|
|
287
|
+
surface: input.surface,
|
|
288
|
+
projectScopeMode: input.decision.projectScopeMode,
|
|
289
|
+
projectRoot: input.decision.projectRoot,
|
|
290
|
+
auraFingerprint: input.decision.auraFingerprint,
|
|
291
|
+
requestedCredential: input.requestedCredential,
|
|
292
|
+
code: input.decision.code,
|
|
293
|
+
timestamp: new Date().toISOString(),
|
|
294
|
+
overrideUsed: Boolean(input.decision.overrideUsed),
|
|
295
|
+
};
|
|
296
|
+
console.warn(`[project-scope] ${JSON.stringify(event)}`);
|
|
297
|
+
}
|