From dcd88e2f6405fd70b5c452b4dffe5863b72bbda8 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 20 Mar 2026 23:08:43 +0000 Subject: [PATCH] Extract API token manager state owner --- .../v6/internal/subsystems/api-contracts.md | 102 ++-- .../v6/internal/subsystems/registry.json | 437 +++++++------- .../internal/subsystems/security-privacy.md | 44 +- .../components/Settings/APITokenManager.tsx | 548 ++---------------- .../monitoredSystemModelGuardrails.test.ts | 46 +- .../__tests__/settingsArchitecture.test.ts | 19 + .../Settings/apiTokenManagerModel.ts | 232 ++++++++ .../Settings/useAPITokenManagerState.ts | 355 ++++++++++++ .../canonical_completion_guard_test.py | 36 +- .../release_control/subsystem_lookup_test.py | 5 +- 10 files changed, 1022 insertions(+), 802 deletions(-) create mode 100644 frontend-modern/src/components/Settings/apiTokenManagerModel.ts create mode 100644 frontend-modern/src/components/Settings/useAPITokenManagerState.ts diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 9b46ba96c4..715d2cf627 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -28,20 +28,22 @@ Own canonical runtime payload shapes between backend and frontend. 5. `frontend-modern/src/types/api.ts` 6. `frontend-modern/src/api/responseUtils.ts` 7. `frontend-modern/src/components/Settings/APITokenManager.tsx` -8. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx` -9. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` -10. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` -11. `frontend-modern/src/components/Settings/UnifiedAgents.tsx` -12. `frontend-modern/src/components/Settings/NodeModal.tsx` -13. `frontend-modern/src/components/Settings/nodeModalModel.ts` -14. `frontend-modern/src/components/Settings/useNodeModalState.ts` -15. `frontend-modern/src/utils/agentInstallCommand.ts` -16. `frontend-modern/src/api/nodes.ts` -17. `frontend-modern/src/api/license.ts` -18. `frontend-modern/src/api/monitoredSystemLedger.ts` -19. `frontend-modern/src/api/resources.ts` -20. `frontend-modern/src/api/monitoring.ts` -21. `internal/api/monitored_system_ledger.go` +8. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` +9. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx` +10. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` +11. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` +12. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` +13. `frontend-modern/src/components/Settings/UnifiedAgents.tsx` +14. `frontend-modern/src/components/Settings/NodeModal.tsx` +15. `frontend-modern/src/components/Settings/nodeModalModel.ts` +16. `frontend-modern/src/components/Settings/useNodeModalState.ts` +17. `frontend-modern/src/utils/agentInstallCommand.ts` +18. `frontend-modern/src/api/nodes.ts` +19. `frontend-modern/src/api/license.ts` +20. `frontend-modern/src/api/monitoredSystemLedger.ts` +21. `frontend-modern/src/api/resources.ts` +22. `frontend-modern/src/api/monitoring.ts` +23. `internal/api/monitored_system_ledger.go` ## Shared Boundaries @@ -53,31 +55,33 @@ Own canonical runtime payload shapes between backend and frontend. 6. `frontend-modern/src/api/security.ts` shared with `security-privacy`: the security frontend client is both a security/privacy control surface and a canonical API payload contract boundary. 7. `frontend-modern/src/api/updates.ts` shared with `deployment-installability`: the updates frontend client is both a deployment-installability control surface and a canonical API payload contract boundary. 8. `frontend-modern/src/components/Settings/APITokenManager.tsx` shared with `security-privacy`: the API token settings surface is both a security/privacy control surface and a canonical API payload contract boundary. -9. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx` shared with `agent-lifecycle`: the infrastructure operations controller is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. -10. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` shared with `agent-lifecycle`: the pure infrastructure operations inventory/install model is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. -11. `frontend-modern/src/components/Settings/NodeModal.tsx` shared with `agent-lifecycle`: the node setup modal render shell is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -12. `frontend-modern/src/components/Settings/nodeModalModel.ts` shared with `agent-lifecycle`: the pure node setup modal model is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -13. `frontend-modern/src/components/Settings/UnifiedAgents.tsx` shared with `agent-lifecycle`: the UnifiedAgents module is a compatibility shim for the canonical infrastructure operations controller and remains on the same shared agent lifecycle and API contract boundary while the old module path exists. -14. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` shared with `agent-lifecycle`: the shared infrastructure operations state hook is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. -15. `frontend-modern/src/components/Settings/useNodeModalState.ts` shared with `agent-lifecycle`: the node setup modal state hook is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -16. `frontend-modern/src/utils/agentInstallCommand.ts` shared with `agent-lifecycle`: the shared frontend install-command helper is both an agent lifecycle control surface and a canonical API/install transport contract boundary. -17. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary. -18. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. -19. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. -20. `internal/api/ai_intelligence_handlers.go` shared with `ai-runtime`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. -21. `internal/api/config_setup_handlers.go` shared with `agent-lifecycle`: auto-register and setup handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. -22. `internal/api/licensing_bridge.go` shared with `cloud-paid`: commercial licensing bridge handlers carry both API payload contract and cloud-paid entitlement boundary ownership. -23. `internal/api/licensing_handlers.go` shared with `cloud-paid`: commercial licensing handlers carry both API payload contract and cloud-paid entitlement boundary ownership. -24. `internal/api/notifications.go` shared with `notifications`: notification handlers are both a notification delivery control surface and a canonical API payload contract boundary. -25. `internal/api/payments_webhook_handlers.go` shared with `cloud-paid`: commercial payment webhook handlers carry both API payload contract and cloud-paid billing boundary ownership. -26. `internal/api/public_signup_handlers.go` shared with `cloud-paid`: hosted signup handlers carry both API payload contract and cloud-paid hosted provisioning boundary ownership. -27. `internal/api/resources.go` shared with `unified-resources`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary. -28. `internal/api/security.go` shared with `security-privacy`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary. -29. `internal/api/security_tokens.go` shared with `security-privacy`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary. -30. `internal/api/slo.go` shared with `performance-and-scalability`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary. -31. `internal/api/system_settings.go` shared with `security-privacy`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary. -32. `internal/api/unified_agent.go` shared with `agent-lifecycle`: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. -33. `internal/api/updates.go` shared with `deployment-installability`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary. +9. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` shared with `security-privacy`: the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary. +10. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx` shared with `agent-lifecycle`: the infrastructure operations controller is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. +11. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` shared with `agent-lifecycle`: the pure infrastructure operations inventory/install model is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. +12. `frontend-modern/src/components/Settings/NodeModal.tsx` shared with `agent-lifecycle`: the node setup modal render shell is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +13. `frontend-modern/src/components/Settings/nodeModalModel.ts` shared with `agent-lifecycle`: the pure node setup modal model is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +14. `frontend-modern/src/components/Settings/UnifiedAgents.tsx` shared with `agent-lifecycle`: the UnifiedAgents module is a compatibility shim for the canonical infrastructure operations controller and remains on the same shared agent lifecycle and API contract boundary while the old module path exists. +15. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` shared with `security-privacy`: the API token settings state hook is both a security/privacy control surface and a canonical API payload contract boundary. +16. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` shared with `agent-lifecycle`: the shared infrastructure operations state hook is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. +17. `frontend-modern/src/components/Settings/useNodeModalState.ts` shared with `agent-lifecycle`: the node setup modal state hook is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +18. `frontend-modern/src/utils/agentInstallCommand.ts` shared with `agent-lifecycle`: the shared frontend install-command helper is both an agent lifecycle control surface and a canonical API/install transport contract boundary. +19. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary. +20. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. +21. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. +22. `internal/api/ai_intelligence_handlers.go` shared with `ai-runtime`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. +23. `internal/api/config_setup_handlers.go` shared with `agent-lifecycle`: auto-register and setup handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. +24. `internal/api/licensing_bridge.go` shared with `cloud-paid`: commercial licensing bridge handlers carry both API payload contract and cloud-paid entitlement boundary ownership. +25. `internal/api/licensing_handlers.go` shared with `cloud-paid`: commercial licensing handlers carry both API payload contract and cloud-paid entitlement boundary ownership. +26. `internal/api/notifications.go` shared with `notifications`: notification handlers are both a notification delivery control surface and a canonical API payload contract boundary. +27. `internal/api/payments_webhook_handlers.go` shared with `cloud-paid`: commercial payment webhook handlers carry both API payload contract and cloud-paid billing boundary ownership. +28. `internal/api/public_signup_handlers.go` shared with `cloud-paid`: hosted signup handlers carry both API payload contract and cloud-paid hosted provisioning boundary ownership. +29. `internal/api/resources.go` shared with `unified-resources`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary. +30. `internal/api/security.go` shared with `security-privacy`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary. +31. `internal/api/security_tokens.go` shared with `security-privacy`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary. +32. `internal/api/slo.go` shared with `performance-and-scalability`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary. +33. `internal/api/system_settings.go` shared with `security-privacy`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary. +34. `internal/api/unified_agent.go` shared with `agent-lifecycle`: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. +35. `internal/api/updates.go` shared with `deployment-installability`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary. ## Extension Points 1. Add or change payload fields through handler + contract tests together @@ -100,7 +104,7 @@ Own canonical runtime payload shapes between backend and frontend. and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` cards' infrastructure resource-link default, so the Patrol page, resource drawer, and problem-resource dashboard panels inherit the canonical resource-filter path construction instead of rebuilding infrastructure URLs inline 8. Route frontend API-client parsed error propagation, API-error-status fallback handling, allowed-status handling, custom status-specific error handling, command-trigger success envelope handling, shared response parsing pipelines, missing-resource lookup handling, metadata CRUD routing, stream event consumption, response status, collection normalization, scalar payload coercion, and structured error normalization through canonical shared helpers under `frontend-modern/src/api/` -9. Add or change API token scope, assignment, and revocation presentation through `frontend-modern/src/components/Settings/APITokenManager.tsx` +9. Add or change API token scope, assignment, and revocation presentation through `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, and `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` 10. Add or change infrastructure operations token generation, lookup, assignment, the pure unified-agent inventory/install model, and reporting/install presentation through `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, and `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` 11. Keep `internal/api/session_store.go` on a fail-closed auth-persistence boundary: persisted OIDC refresh tokens may only round-trip through encrypted-at-rest session payloads, and any missing-crypto or invalid-ciphertext path must drop the token instead of preserving plaintext-at-rest session state. 12. Keep tenant AI handler wiring on canonical provider ownership: `internal/api/ai_handlers.go` may wire tenant `ReadState` and tenant-scoped unified-resource providers into AI services, but it must not revive tenant snapshot-provider bridges once Patrol can initialize and verify from those canonical providers directly. @@ -1342,14 +1346,16 @@ magic-link payload semantics, while `internal/hosted/provisioner.go` owns the shared org bootstrap and rollback mechanics that the hosted signup handler invokes. The API token settings surface now also follows the same explicit ownership -rule. Changes to `frontend-modern/src/components/Settings/APITokenManager.tsx` -must carry this contract and the dedicated API-token management proof file -instead of remaining an unowned consumer of token scope labels, token -assignment visibility, and revoke-state presentation. -That shared `APITokenManager.tsx` boundary must also stay under explicit proof -routing on both sides instead of relying only on broad settings-surface -coverage on the security side: token settings changes must continue to carry -the direct `api-token-management-surface` API-contract proof together with the +rule. Changes to `frontend-modern/src/components/Settings/APITokenManager.tsx`, +`frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, and +`frontend-modern/src/components/Settings/useAPITokenManagerState.ts` must +carry this contract and the dedicated API-token management proof file instead +of remaining an unowned consumer of token scope labels, token assignment +visibility, and revoke-state presentation. +That shared API-token boundary must also stay under explicit proof routing on +both sides instead of relying only on broad settings-surface coverage on the +security side: token settings changes must continue to carry the direct +`api-token-management-surface` API-contract proof together with the security-side surface proof. That same token surface, together with `frontend-modern/src/api/security.ts`, `internal/api/security.go`, `internal/api/security_tokens.go`, and diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 72819765b0..587f861cf3 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -97,6 +97,14 @@ "security-privacy" ] }, + { + "path": "frontend-modern/src/components/Settings/apiTokenManagerModel.ts", + "rationale": "the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary", + "subsystems": [ + "api-contracts", + "security-privacy" + ] + }, { "path": "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx", "rationale": "the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary", @@ -161,6 +169,14 @@ "api-contracts" ] }, + { + "path": "frontend-modern/src/components/Settings/useAPITokenManagerState.ts", + "rationale": "the API token settings state hook is both a security/privacy control surface and a canonical API payload contract boundary", + "subsystems": [ + "api-contracts", + "security-privacy" + ] + }, { "path": "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx", "rationale": "the shared infrastructure operations state hook is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary", @@ -441,32 +457,32 @@ "internal/hostagent/send_report_test.go" ] }, - { - "id": "unified-agent-installer-runtime", - "label": "unified agent installer runtime proof", - "match_prefixes": [], - "match_files": [ - "scripts/install.sh" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "scripts/installtests/install_sh_test.go" - ] - }, - { - "id": "windows-agent-installer-runtime", - "label": "windows agent installer lifecycle proof", - "match_prefixes": [], - "match_files": [ - "scripts/install.ps1" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "scripts/installtests/install_ps1_test.go" - ] - }, + { + "id": "unified-agent-installer-runtime", + "label": "unified agent installer runtime proof", + "match_prefixes": [], + "match_files": [ + "scripts/install.sh" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "scripts/installtests/install_sh_test.go" + ] + }, + { + "id": "windows-agent-installer-runtime", + "label": "windows agent installer lifecycle proof", + "match_prefixes": [], + "match_files": [ + "scripts/install.ps1" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "scripts/installtests/install_ps1_test.go" + ] + }, { "id": "agent-profiles-surface", "label": "agent profile management proof", @@ -513,47 +529,47 @@ ] }, { - "id": "node-setup-settings-surface", - "label": "node setup/install command proof", - "match_prefixes": [], - "match_files": [ - "frontend-modern/src/components/Settings/NodeModal.tsx", - "frontend-modern/src/components/Settings/nodeModalModel.ts", - "frontend-modern/src/components/Settings/useNodeModalState.ts" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "frontend-modern/src/components/Settings/__tests__/NodeModal.guardrails.test.ts" - ] - }, - { - "id": "setup-completion-install-surface", - "label": "setup completion install handoff proof", - "match_prefixes": [], - "match_files": [ - "frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts" - ] - }, - { - "id": "deploy-fallback-install-surface", - "label": "deploy fallback install command proof", - "match_prefixes": [], - "match_files": [ - "frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "frontend-modern/src/components/Infrastructure/__tests__/DeployStepComponents.test.tsx" - ] - }, - { + "id": "node-setup-settings-surface", + "label": "node setup/install command proof", + "match_prefixes": [], + "match_files": [ + "frontend-modern/src/components/Settings/NodeModal.tsx", + "frontend-modern/src/components/Settings/nodeModalModel.ts", + "frontend-modern/src/components/Settings/useNodeModalState.ts" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/Settings/__tests__/NodeModal.guardrails.test.ts" + ] + }, + { + "id": "setup-completion-install-surface", + "label": "setup completion install handoff proof", + "match_prefixes": [], + "match_files": [ + "frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts" + ] + }, + { + "id": "deploy-fallback-install-surface", + "label": "deploy fallback install command proof", + "match_prefixes": [], + "match_files": [ + "frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/Infrastructure/__tests__/DeployStepComponents.test.tsx" + ] + }, + { "id": "unified-agent-settings-surface", "label": "unified agent settings lifecycle proof", "match_prefixes": [], @@ -753,11 +769,13 @@ ], "owned_files": [ "frontend-modern/src/components/Settings/APITokenManager.tsx", + "frontend-modern/src/components/Settings/apiTokenManagerModel.ts", "frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx", "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx", "frontend-modern/src/components/Settings/NodeModal.tsx", "frontend-modern/src/components/Settings/nodeModalModel.ts", "frontend-modern/src/components/Settings/UnifiedAgents.tsx", + "frontend-modern/src/components/Settings/useAPITokenManagerState.ts", "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx", "frontend-modern/src/components/Settings/useNodeModalState.ts", "frontend-modern/src/types/api.ts", @@ -883,12 +901,15 @@ "label": "API token management surface proof", "match_prefixes": [], "match_files": [ - "frontend-modern/src/components/Settings/APITokenManager.tsx" + "frontend-modern/src/components/Settings/APITokenManager.tsx", + "frontend-modern/src/components/Settings/apiTokenManagerModel.ts", + "frontend-modern/src/components/Settings/useAPITokenManagerState.ts" ], "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ - "frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx" + "frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx", + "frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts" ] }, { @@ -1873,89 +1894,89 @@ "internal/unifiedresources/code_standards_test.go" ], "require_explicit_path_policy_coverage": true, - "path_policies": [ - { - "id": "discovery-provider-runtime", - "label": "monitoring discovery provider proof", - "match_prefixes": [], - "match_files": [ - "internal/monitoring/kubernetes_agents.go", - "internal/monitoring/monitor_discovery_helpers.go", - "internal/monitoring/monitor_pbs_pmg.go", - "internal/monitoring/poll_providers.go", - "internal/monitoring/truenas_poller.go" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "internal/monitoring/kubernetes_agents_test.go", - "internal/monitoring/monitor_host_agents_test.go", - "internal/monitoring/monitor_pbs_coverage_test.go", - "internal/monitoring/monitor_pmg_test.go", - "internal/monitoring/monitor_polling_test.go", - "internal/monitoring/truenas_poller_test.go" - ] - }, - { - "id": "metrics-hot-path", - "label": "monitoring metrics hot-path proof", - "match_prefixes": [], - "match_files": [ - "internal/monitoring/monitor_metrics.go" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "internal/monitoring/monitor_metrics_chart_batch_bench_test.go", - "internal/monitoring/monitor_metrics_slo_test.go" - ] - }, - { - "id": "metrics-history-runtime", - "label": "monitoring metrics history proof", - "match_prefixes": [], - "match_files": [ - "internal/monitoring/metrics.go", - "internal/monitoring/metrics_history.go", - "internal/monitoring/mock_metrics_history.go" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "internal/monitoring/metrics_history_concurrency_test.go", - "internal/monitoring/metrics_history_memory_regression_test.go", - "internal/monitoring/metrics_history_test.go", - "internal/monitoring/metrics_test.go", - "internal/monitoring/mock_metrics_history_test.go" - ] - }, - { - "id": "memory-source-runtime", - "label": "monitoring memory-source proof", - "match_prefixes": [], - "match_files": [ - "internal/api/diagnostics.go", - "internal/monitoring/guest_memory_sources.go", - "internal/monitoring/memory_source_catalog.go", - "internal/monitoring/monitor_polling_node.go", - "internal/monitoring/monitor_polling_vm.go", - "internal/monitoring/node_memory_sources.go" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "internal/api/diagnostics_memory_test.go", - "internal/monitoring/diagnostic_snapshots_test.go", - "internal/monitoring/memory_source_catalog_test.go", - "internal/monitoring/memory_trust_characterization_test.go", - "internal/monitoring/node_memory_sources_test.go" - ] - }, - { - "id": "container-entrypoint-runtime", - "label": "container entrypoint runtime proof", - "match_prefixes": [], - "match_files": [ + "path_policies": [ + { + "id": "discovery-provider-runtime", + "label": "monitoring discovery provider proof", + "match_prefixes": [], + "match_files": [ + "internal/monitoring/kubernetes_agents.go", + "internal/monitoring/monitor_discovery_helpers.go", + "internal/monitoring/monitor_pbs_pmg.go", + "internal/monitoring/poll_providers.go", + "internal/monitoring/truenas_poller.go" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/monitoring/kubernetes_agents_test.go", + "internal/monitoring/monitor_host_agents_test.go", + "internal/monitoring/monitor_pbs_coverage_test.go", + "internal/monitoring/monitor_pmg_test.go", + "internal/monitoring/monitor_polling_test.go", + "internal/monitoring/truenas_poller_test.go" + ] + }, + { + "id": "metrics-hot-path", + "label": "monitoring metrics hot-path proof", + "match_prefixes": [], + "match_files": [ + "internal/monitoring/monitor_metrics.go" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/monitoring/monitor_metrics_chart_batch_bench_test.go", + "internal/monitoring/monitor_metrics_slo_test.go" + ] + }, + { + "id": "metrics-history-runtime", + "label": "monitoring metrics history proof", + "match_prefixes": [], + "match_files": [ + "internal/monitoring/metrics.go", + "internal/monitoring/metrics_history.go", + "internal/monitoring/mock_metrics_history.go" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/monitoring/metrics_history_concurrency_test.go", + "internal/monitoring/metrics_history_memory_regression_test.go", + "internal/monitoring/metrics_history_test.go", + "internal/monitoring/metrics_test.go", + "internal/monitoring/mock_metrics_history_test.go" + ] + }, + { + "id": "memory-source-runtime", + "label": "monitoring memory-source proof", + "match_prefixes": [], + "match_files": [ + "internal/api/diagnostics.go", + "internal/monitoring/guest_memory_sources.go", + "internal/monitoring/memory_source_catalog.go", + "internal/monitoring/monitor_polling_node.go", + "internal/monitoring/monitor_polling_vm.go", + "internal/monitoring/node_memory_sources.go" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/api/diagnostics_memory_test.go", + "internal/monitoring/diagnostic_snapshots_test.go", + "internal/monitoring/memory_source_catalog_test.go", + "internal/monitoring/memory_trust_characterization_test.go", + "internal/monitoring/node_memory_sources_test.go" + ] + }, + { + "id": "container-entrypoint-runtime", + "label": "container entrypoint runtime proof", + "match_prefixes": [], + "match_files": [ "docker-entrypoint.sh" ], "allow_same_subsystem_tests": false, @@ -2363,9 +2384,11 @@ "docs/PRIVACY.md", "frontend-modern/src/api/security.ts", "frontend-modern/src/components/Settings/APITokenManager.tsx", + "frontend-modern/src/components/Settings/apiTokenManagerModel.ts", "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx", "frontend-modern/src/components/Settings/SecurityAuthPanel.tsx", "frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx", + "frontend-modern/src/components/Settings/useAPITokenManagerState.ts", "frontend-modern/src/components/Settings/useSystemSettingsState.ts", "internal/api/security.go", "internal/api/security_tokens.go", @@ -2416,9 +2439,11 @@ "match_prefixes": [], "match_files": [ "frontend-modern/src/components/Settings/APITokenManager.tsx", + "frontend-modern/src/components/Settings/apiTokenManagerModel.ts", "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx", "frontend-modern/src/components/Settings/SecurityAuthPanel.tsx", - "frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx" + "frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx", + "frontend-modern/src/components/Settings/useAPITokenManagerState.ts" ], "allow_same_subsystem_tests": false, "test_prefixes": [], @@ -2644,10 +2669,10 @@ "internal/unifiedresources/code_standards_test.go" ], "require_explicit_path_policy_coverage": true, - "path_policies": [ - { - "id": "identity-canonicalization", - "label": "canonical identity proof", + "path_policies": [ + { + "id": "identity-canonicalization", + "label": "canonical identity proof", "match_prefixes": [], "match_files": [ "internal/unifiedresources/canonical_identity.go", @@ -2687,9 +2712,9 @@ "internal/unifiedresources/views_test.go" ] }, - { - "id": "resource-consumers", - "label": "unified resource consumer proof", + { + "id": "resource-consumers", + "label": "unified resource consumer proof", "match_prefixes": [], "match_files": [ "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts", @@ -2724,52 +2749,52 @@ "frontend-modern/src/stores/__tests__/websocket-unified.test.ts", "frontend-modern/src/types/__tests__/resource.test.ts", "internal/unifiedresources/code_standards_test.go" - ] - }, - { - "id": "metrics-target-runtime", - "label": "unified resource metrics target proof", - "match_prefixes": [], - "match_files": [ - "internal/unifiedresources/metrics.go", - "internal/unifiedresources/metrics_targets.go" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "internal/unifiedresources/metrics_targets_test.go", - "internal/unifiedresources/metrics_test.go" - ] - }, - { - "id": "platform-registry-runtime", - "label": "unified resource platform registry proof", - "match_prefixes": [], - "match_files": [ - "internal/unifiedresources/kubernetes_capabilities.go", - "internal/unifiedresources/pbs_rollups.go", - "internal/unifiedresources/registry.go", - "internal/unifiedresources/resolve.go", - "internal/unifiedresources/resolved_host_set.go", - "internal/unifiedresources/snapshot_source_filter.go", - "internal/unifiedresources/store.go" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "internal/unifiedresources/kubernetes_registry_test.go", - "internal/unifiedresources/pbs_pmg_registry_test.go", - "internal/unifiedresources/registry_merge_policy_test.go", - "internal/unifiedresources/registry_test.go", - "internal/unifiedresources/resolve_test.go", - "internal/unifiedresources/resolved_host_set_test.go", - "internal/unifiedresources/snapshot_source_filter_test.go", - "internal/unifiedresources/store_test.go" - ] - }, - { - "id": "unified-resource-runtime-support", - "label": "unified resource runtime support proof", + ] + }, + { + "id": "metrics-target-runtime", + "label": "unified resource metrics target proof", + "match_prefixes": [], + "match_files": [ + "internal/unifiedresources/metrics.go", + "internal/unifiedresources/metrics_targets.go" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/unifiedresources/metrics_targets_test.go", + "internal/unifiedresources/metrics_test.go" + ] + }, + { + "id": "platform-registry-runtime", + "label": "unified resource platform registry proof", + "match_prefixes": [], + "match_files": [ + "internal/unifiedresources/kubernetes_capabilities.go", + "internal/unifiedresources/pbs_rollups.go", + "internal/unifiedresources/registry.go", + "internal/unifiedresources/resolve.go", + "internal/unifiedresources/resolved_host_set.go", + "internal/unifiedresources/snapshot_source_filter.go", + "internal/unifiedresources/store.go" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/unifiedresources/kubernetes_registry_test.go", + "internal/unifiedresources/pbs_pmg_registry_test.go", + "internal/unifiedresources/registry_merge_policy_test.go", + "internal/unifiedresources/registry_test.go", + "internal/unifiedresources/resolve_test.go", + "internal/unifiedresources/resolved_host_set_test.go", + "internal/unifiedresources/snapshot_source_filter_test.go", + "internal/unifiedresources/store_test.go" + ] + }, + { + "id": "unified-resource-runtime-support", + "label": "unified resource runtime support proof", "match_prefixes": [ "internal/unifiedresources/" ], diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 4569df7272..fa99e72046 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -25,32 +25,36 @@ token-management visibility, and privacy controls to operators. 2. `docs/PRIVACY.md` 3. `frontend-modern/src/api/security.ts` 4. `frontend-modern/src/components/Settings/APITokenManager.tsx` -5. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` -6. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` -7. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` -8. `frontend-modern/src/components/Settings/useSystemSettingsState.ts` -9. `internal/api/security.go` -10. `internal/api/security_tokens.go` -11. `internal/api/system_settings.go` -12. `internal/telemetry/telemetry.go` +5. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` +6. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` +7. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` +8. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` +9. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` +10. `frontend-modern/src/components/Settings/useSystemSettingsState.ts` +11. `internal/api/security.go` +12. `internal/api/security_tokens.go` +13. `internal/api/system_settings.go` +14. `internal/telemetry/telemetry.go` ## Shared Boundaries 1. `frontend-modern/src/api/security.ts` shared with `api-contracts`: the security frontend client is both a security/privacy control surface and a canonical API payload contract boundary. 2. `frontend-modern/src/components/Settings/APITokenManager.tsx` shared with `api-contracts`: the API token settings surface is both a security/privacy control surface and a canonical API payload contract boundary. -3. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` shared with `frontend-primitives`: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. -4. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `frontend-primitives`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. -5. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `frontend-primitives`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. -6. `internal/api/security.go` shared with `api-contracts`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary. -7. `internal/api/security_tokens.go` shared with `api-contracts`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary. -8. `internal/api/system_settings.go` shared with `api-contracts`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary. +3. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` shared with `api-contracts`: the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary. +4. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` shared with `frontend-primitives`: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. +5. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `frontend-primitives`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. +6. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `frontend-primitives`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. +7. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` shared with `api-contracts`: the API token settings state hook is both a security/privacy control surface and a canonical API payload contract boundary. +8. `internal/api/security.go` shared with `api-contracts`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary. +9. `internal/api/security_tokens.go` shared with `api-contracts`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary. +10. `internal/api/system_settings.go` shared with `api-contracts`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary. ## Extension Points 1. Change privacy disclosures or outbound-data guarantees through `docs/PRIVACY.md` and `internal/telemetry/telemetry.go` together. 2. Change security policy, hardening guidance, or supported auth boundaries through `SECURITY.md`. 3. Change telemetry/privacy settings state handling through `frontend-modern/src/components/Settings/useSystemSettingsState.ts`. -4. Change security/auth/token transport behavior through the shared `frontend-modern/src/api/security.ts`, `frontend-modern/src/components/Settings/APITokenManager.tsx`, `internal/api/security.go`, `internal/api/security_tokens.go`, and `internal/api/system_settings.go` boundary. +4. Change security/auth/token transport behavior through the shared `frontend-modern/src/api/security.ts`, `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, `frontend-modern/src/components/Settings/useAPITokenManagerState.ts`, `internal/api/security.go`, `internal/api/security_tokens.go`, and `internal/api/system_settings.go` boundary. 5. Change security/privacy settings presentation through the shared `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`, `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx`, and `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` boundary. ## Forbidden Paths @@ -212,7 +216,9 @@ migration input. Once a legacy plaintext license file can be read, canonical persistence must rewrite encrypted-at-rest storage immediately on load instead of allowing plaintext licensing state to remain on the runtime primary path. That same shared token-settings boundary must stay under explicit proof routing -on both sides: `frontend-modern/src/components/Settings/APITokenManager.tsx` -must continue to carry the direct `security-settings-surfaces` proof path -together with the API-contract token-management proof instead of borrowing -coverage only from broader settings-shell or API ownership. +on both sides: `frontend-modern/src/components/Settings/APITokenManager.tsx`, +`frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, and +`frontend-modern/src/components/Settings/useAPITokenManagerState.ts` must +continue to carry the direct `security-settings-surfaces` proof path together +with the API-contract token-management proof instead of borrowing coverage only +from broader settings-shell or API ownership. diff --git a/frontend-modern/src/components/Settings/APITokenManager.tsx b/frontend-modern/src/components/Settings/APITokenManager.tsx index 712d75dbbf..57b283df77 100644 --- a/frontend-modern/src/components/Settings/APITokenManager.tsx +++ b/frontend-modern/src/components/Settings/APITokenManager.tsx @@ -1,48 +1,10 @@ -import { - Component, - For, - Show, - createEffect, - createMemo, - createSignal, - onCleanup, - onMount, -} from 'solid-js'; -import { unwrap } from 'solid-js/store'; -import { SecurityAPI, type APITokenRecord } from '@/api/security'; -import { notificationStore } from '@/stores/notifications'; -import { formatRelativeTime } from '@/utils/format'; -import { useWebSocket } from '@/App'; -import { getPulseBaseUrl } from '@/utils/url'; -import { showTokenReveal, useTokenRevealState } from '@/stores/tokenReveal'; -import { logger } from '@/utils/logger'; -import { - getAPITokenGenerateErrorMessage, - getAPITokensLoadErrorMessage, - getAPITokenRevokeErrorMessage, -} from '@/utils/apiTokenPresentation'; +import { Component, For, Show } from 'solid-js'; import { Card } from '@/components/shared/Card'; import { SectionHeader } from '@/components/shared/SectionHeader'; import { PulseDataGrid } from '@/components/shared/PulseDataGrid'; -import { useResources } from '@/hooks/useResources'; -import type { Resource } from '@/types/resource'; -import { - getActionableAgentIdFromResource, - getActionableDockerRuntimeIdFromResource, - hasAgentFacet as resourceHasAgentFacet, -} from '@/utils/agentResources'; -import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity'; import BadgeCheck from 'lucide-solid/icons/badge-check'; -import { - API_SCOPE_LABELS, - API_SCOPE_OPTIONS, - DOCKER_MANAGE_SCOPE, - DOCKER_REPORT_SCOPE, - AGENT_REPORT_SCOPE, - MONITORING_READ_SCOPE, - SETTINGS_READ_SCOPE, - SETTINGS_WRITE_SCOPE, -} from '@/constants/apiScopes'; +import { MONITORING_READ_SCOPE } from '@/constants/apiScopes'; +import { useAPITokenManagerState } from './useAPITokenManagerState'; interface APITokenManagerProps { currentTokenHint?: string; @@ -51,439 +13,45 @@ interface APITokenManagerProps { canManage?: boolean; } -const SCOPES_DOC_URL = - 'https://github.com/rcourtman/Pulse/blob/main/docs/CONFIGURATION.md#token-scopes'; -const WILDCARD_SCOPE = '*'; - export const APITokenManager: Component = (props) => { - const { markDockerRuntimesTokenRevoked, markAgentsTokenRevoked } = useWebSocket(); - const { byType, resources } = useResources(); - - const dockerRuntimeResources = createMemo(() => byType('docker-host')); - const hasAgentScopeResource = (resource: Resource): boolean => { - if (resource.type === 'docker-host') return false; - return ( - resource.type === 'agent' || - resource.type === 'pbs' || - resource.type === 'pmg' || - resource.type === 'truenas' || - resourceHasAgentFacet(resource) - ); - }; - const agentCapableResources = createMemo(() => - resources().filter((resource) => hasAgentScopeResource(resource)), - ); - - const readPlatformData = (resource: Resource): Record | undefined => { - return resource.platformData - ? (unwrap(resource.platformData) as Record) - : undefined; - }; - - const readPlatformString = (value: unknown): string | undefined => { - return typeof value === 'string' && value.length > 0 ? value : undefined; - }; - - const readPlatformNumber = (value: unknown): number | undefined => { - return typeof value === 'number' && Number.isFinite(value) ? value : undefined; - }; - - const readNestedPlatformField = ( - platformData: Record | undefined, - field: string, - ): unknown => { - if (!platformData) return undefined; - if (field in platformData) return platformData[field]; - const agent = platformData.agent; - if (agent && typeof agent === 'object' && field in (agent as Record)) { - return (agent as Record)[field]; - } - const docker = platformData.docker; - if (docker && typeof docker === 'object' && field in (docker as Record)) { - return (docker as Record)[field]; - } - return undefined; - }; - - const tokenIdForResource = (resource: Resource): string | undefined => { - const platformData = readPlatformData(resource); - return readPlatformString(readNestedPlatformField(platformData, 'tokenId')); - }; - - const agentActionIdForResource = (resource: Resource): string => { - return getActionableAgentIdFromResource(resource) || resource.id; - }; - - const dockerActionIdForResource = (resource: Resource): string => { - return getActionableDockerRuntimeIdFromResource(resource) || resource.id; - }; - - const revokedTokenIdForResource = (resource: Resource): string | undefined => { - const platformData = readPlatformData(resource); - return readPlatformString(readNestedPlatformField(platformData, 'revokedTokenId')); - }; - - const tokenRevokedAtForResource = (resource: Resource): number | undefined => { - const platformData = readPlatformData(resource); - return readPlatformNumber(readNestedPlatformField(platformData, 'tokenRevokedAt')); - }; - - const dockerTokenUsage = createMemo(() => { - type UsageRuntime = { id: string; label: string }; - const usage = new Map(); - for (const resource of dockerRuntimeResources()) { - const tokenId = tokenIdForResource(resource); - if (!tokenId) continue; - const runtimeId = dockerActionIdForResource(resource); - const label = getPreferredResourceDisplayName(resource); - const previous = usage.get(tokenId); - if (previous) { - if (previous.runtimes.some((runtime) => runtime.id === runtimeId)) continue; - usage.set(tokenId, { - count: previous.count + 1, - runtimes: [...previous.runtimes, { id: runtimeId, label }], - }); - } else { - usage.set(tokenId, { - count: 1, - runtimes: [{ id: runtimeId, label }], - }); - } - } - return usage; - }); - const agentTokenUsage = createMemo(() => { - type UsageAgent = { id: string; label: string }; - const usage = new Map(); - for (const resource of agentCapableResources()) { - const tokenId = tokenIdForResource(resource); - if (!tokenId) continue; - const agentId = agentActionIdForResource(resource); - const label = getPreferredResourceDisplayName(resource); - const previous = usage.get(tokenId); - if (previous) { - if (previous.agents.some((agent) => agent.id === agentId)) continue; - usage.set(tokenId, { - count: previous.count + 1, - agents: [...previous.agents, { id: agentId, label }], - }); - } else { - usage.set(tokenId, { count: 1, agents: [{ id: agentId, label }] }); - } - } - return usage; - }); - - const [tokens, setTokens] = createSignal([]); - const [tokensLoaded, setTokensLoaded] = createSignal(false); - const sortedTokens = createMemo(() => - [...tokens()].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()), - ); - const totalTokens = createMemo(() => sortedTokens().length); - const wildcardCount = createMemo( - () => - sortedTokens().filter((token) => { - const scopes = token.scopes; - return !scopes || scopes.length === 0 || scopes.includes('*'); - }).length, - ); - const scopedTokenCount = createMemo(() => totalTokens() - wildcardCount()); - const hasWildcardTokens = createMemo(() => wildcardCount() > 0); - - const [loading, setLoading] = createSignal(true); - const [isGenerating, setIsGenerating] = createSignal(false); - const [newTokenValue, setNewTokenValue] = createSignal(null); - const [newTokenRecord, setNewTokenRecord] = createSignal(null); - const [nameInput, setNameInput] = createSignal(''); - const tokenRevealState = useTokenRevealState(); - const [selectedScopes, setSelectedScopes] = createSignal([]); - const canManage = () => props.canManage !== false; - - type ScopeGroup = (typeof API_SCOPE_OPTIONS)[number]['group']; - type ScopeOption = (typeof API_SCOPE_OPTIONS)[number]; - const scopeGroupOrder: ScopeGroup[] = ['Monitoring', 'Agents', 'Settings']; - const scopeGroups = createMemo<[ScopeGroup, ScopeOption[]][]>(() => { - const grouped: Record = { - Monitoring: [], - Agents: [], - Settings: [], - }; - for (const option of API_SCOPE_OPTIONS) { - grouped[option.group].push(option); - } - return scopeGroupOrder - .map((group) => [group, grouped[group]] as [ScopeGroup, ScopeOption[]]) - .filter(([, options]) => options.length > 0); - }); - - const isFullAccessSelected = () => - selectedScopes().length === 0 || selectedScopes().includes(WILDCARD_SCOPE); - - const scopePresets: { label: string; scopes: string[]; description: string }[] = [ - { - label: 'Kiosk / Dashboard', - scopes: [MONITORING_READ_SCOPE], - description: - 'Read-only access for wall displays. Use ?token=xxx&kiosk=1 in the URL to hide navigation and filters.', - }, - { - label: 'Agent', - scopes: [AGENT_REPORT_SCOPE], - description: 'Allow the Pulse agent to submit OS, CPU, and disk metrics.', - }, - { - label: 'Container report', - scopes: [DOCKER_REPORT_SCOPE], - description: - 'Permits container agents (Docker or Podman) to stream runtime and container telemetry only.', - }, - { - label: 'Container manage', - scopes: [DOCKER_REPORT_SCOPE, DOCKER_MANAGE_SCOPE], - description: 'Extends container reporting with lifecycle actions (restart, stop, etc.).', - }, - { - label: 'Settings read', - scopes: [SETTINGS_READ_SCOPE], - description: 'Read configuration snapshots and diagnostics without modifying anything.', - }, - { - label: 'Settings admin', - scopes: [SETTINGS_READ_SCOPE, SETTINGS_WRITE_SCOPE], - description: 'Full settings read/write – equivalent to automation with admin privileges.', - }, - ]; - - const presetMatchesSelection = (presetScopes: string[]) => { - const selection = [...selectedScopes()].filter((scope) => scope !== WILDCARD_SCOPE).sort(); - const target = [...presetScopes].sort(); - if (target.length === 0) { - return isFullAccessSelected(); - } - if (selection.length !== target.length) { - return false; - } - return target.every((scope) => selection.includes(scope)); - }; - - const applyScopePreset = (scopes: string[]) => { - const unique = Array.from(new Set(scopes)).filter(Boolean); - setSelectedScopes(unique); - }; - const clearScopes = () => setSelectedScopes([]); - - let createSectionRef: HTMLDivElement | undefined; - const [createHighlight, setCreateHighlight] = createSignal(false); - let highlightTimer: number | undefined; - const focusCreateSection = () => { - if (!createSectionRef) return; - createSectionRef.scrollIntoView({ behavior: 'smooth', block: 'start' }); - setCreateHighlight(true); - window.clearTimeout(highlightTimer); - highlightTimer = window.setTimeout(() => setCreateHighlight(false), 1600); - }; - onCleanup(() => { - if (highlightTimer) window.clearTimeout(highlightTimer); - }); - - const loadTokens = async () => { - setLoading(true); - setTokensLoaded(false); - try { - const list = await SecurityAPI.listTokens(); - setTokens(list); - setTokensLoaded(true); - } catch (err) { - logger.error('Failed to load API tokens', err); - notificationStore.error(getAPITokensLoadErrorMessage()); - } finally { - setLoading(false); - } - }; - - onMount(() => { - void loadTokens(); - }); - - createEffect(() => { - if (!tokensLoaded()) return; - const activeTokenIds = new Set(tokens().map((token) => token.id)); - const pendingRuntimesByToken = new Map(); - - for (const resource of dockerRuntimeResources()) { - const tokenId = tokenIdForResource(resource); - if (!tokenId) continue; - if (activeTokenIds.has(tokenId)) continue; - if (revokedTokenIdForResource(resource) === tokenId) continue; - - if (!pendingRuntimesByToken.has(tokenId)) { - pendingRuntimesByToken.set(tokenId, []); - } - const runtimeIds = pendingRuntimesByToken.get(tokenId)!; - const runtimeId = dockerActionIdForResource(resource); - if (!runtimeIds.includes(runtimeId)) { - runtimeIds.push(runtimeId); - } - } - - pendingRuntimesByToken.forEach((runtimeIds, tokenId) => { - if (runtimeIds.length === 0) return; - markDockerRuntimesTokenRevoked(tokenId, runtimeIds); - }); - - const pendingAgentsByToken = new Map(); - for (const resource of agentCapableResources()) { - const tokenId = tokenIdForResource(resource); - if (!tokenId) continue; - if (activeTokenIds.has(tokenId)) continue; - if (revokedTokenIdForResource(resource) === tokenId && tokenRevokedAtForResource(resource)) - continue; - - if (!pendingAgentsByToken.has(tokenId)) { - pendingAgentsByToken.set(tokenId, []); - } - const agentIds = pendingAgentsByToken.get(tokenId)!; - const agentId = agentActionIdForResource(resource); - if (!agentIds.includes(agentId)) { - agentIds.push(agentId); - } - } - - pendingAgentsByToken.forEach((agentIds, tokenId) => { - if (agentIds.length === 0) return; - markAgentsTokenRevoked(tokenId, agentIds); - }); - }); - - const handleGenerate = async () => { - if (!canManage()) return; - setIsGenerating(true); - try { - const trimmedName = nameInput().trim() || undefined; - const scopeSelection = [...selectedScopes()].sort(); - const scopePayload = scopeSelection.length > 0 ? scopeSelection : undefined; - const { token, record } = await SecurityAPI.createToken(trimmedName, scopePayload); - - setTokens((prev) => [record, ...prev]); - setNewTokenRecord(record); - setNewTokenValue(token); - setNameInput(''); - - showTokenReveal({ - token, - record, - source: 'security', - note: 'Copy this token now. You can reopen this dialog from Security → API tokens while this page stays open.', - }); - notificationStore.success( - 'New API token generated. Copy it below while it is still visible.', - ); - props.onTokensChanged?.(); - } catch (err) { - logger.error('Failed to generate API token', err); - notificationStore.error(getAPITokenGenerateErrorMessage(err)); - } finally { - setIsGenerating(false); - } - }; - - const tokenHint = (record: APITokenRecord | null | undefined) => { - if (!record) return '—'; - if (record.prefix && record.suffix) { - return `${record.prefix}…${record.suffix}`; - } - if (record.prefix) { - return `${record.prefix}…`; - } - return '—'; - }; - - const tokenNameForDialog = (record: APITokenRecord) => { - if (record.name?.trim()) return record.name.trim(); - if (record.prefix && record.suffix) return `${record.prefix}…${record.suffix}`; - if (record.prefix) return `${record.prefix}…`; - return 'untitled token'; - }; - - const handleDelete = async (record: APITokenRecord) => { - if (!canManage()) return; - const dockerUsage = dockerTokenUsage().get(record.id); - const agentUsage = agentTokenUsage().get(record.id); - const displayName = tokenNameForDialog(record); - - const affectedRuntimeIds = dockerUsage ? dockerUsage.runtimes.map((runtime) => runtime.id) : []; - const affectedAgentIds = agentUsage ? agentUsage.agents.map((agent) => agent.id) : []; - let revokeMessage: string | undefined; - const messageChunks: string[] = []; - if (dockerUsage) { - const runtimeListPreview = dockerUsage.runtimes - .slice(0, 5) - .map((runtime) => runtime.label) - .join(', '); - const extraCount = dockerUsage.runtimes.length - 5; - const usageSummary = - extraCount > 0 ? `${runtimeListPreview}, +${extraCount} more` : runtimeListPreview; - const runtimeCountLabel = - dockerUsage.count === 1 ? 'container runtime' : `${dockerUsage.count} container runtimes`; - messageChunks.push(`${runtimeCountLabel}: ${usageSummary}`); - } - if (agentUsage) { - const agentListPreview = agentUsage.agents - .slice(0, 5) - .map((agent) => agent.label) - .join(', '); - const agentExtra = agentUsage.agents.length - 5; - const agentSummary = - agentExtra > 0 ? `${agentListPreview}, +${agentExtra} more` : agentListPreview; - const agentCountLabel = agentUsage.count === 1 ? 'agent' : `${agentUsage.count} agents`; - messageChunks.push(`${agentCountLabel}: ${agentSummary}`); - } - if (messageChunks.length > 0) { - revokeMessage = `Token "${displayName}" was previously used by ${messageChunks.join(' • ')}. Update those agents with a new token.`; - } - - try { - await SecurityAPI.deleteToken(record.id); - setTokens((prev) => prev.filter((token) => token.id !== record.id)); - notificationStore.success( - revokeMessage ? `Token revoked: ${revokeMessage}` : 'Token revoked', - ); - props.onTokensChanged?.(); - if (affectedRuntimeIds.length > 0) { - markDockerRuntimesTokenRevoked(record.id, affectedRuntimeIds); - } - if (affectedAgentIds.length > 0) { - markAgentsTokenRevoked(record.id, affectedAgentIds); - } - - const current = newTokenRecord(); - if (current && current.id === record.id) { - setNewTokenValue(null); - setNewTokenRecord(null); - } - } catch (err) { - logger.error('Failed to revoke API token', err); - notificationStore.error(getAPITokenRevokeErrorMessage()); - } - }; - - const isRevealActiveForCurrentToken = () => { - const active = tokenRevealState(); - if (!active) return false; - return newTokenValue() !== null && active.token === newTokenValue(); - }; - - const reopenTokenDialog = () => { - const token = newTokenValue(); - const record = newTokenRecord(); - if (!token || !record) return; - showTokenReveal({ - token, - record, - source: 'security', - note: 'Copy this token now. Close the dialog once you have stored it safely.', - }); - }; + const { + API_SCOPE_LABELS, + API_TOKEN_SCOPES_DOC_URL, + agentTokenUsage, + applyScopePreset, + canManage, + clearScopes, + copyNewMonitoringKioskLink, + createHighlight, + dismissNewToken, + dockerTokenUsage, + focusCreateSection, + formatRelativeTime, + handleDelete, + handleGenerate, + hasWildcardTokens, + isFullAccessSelected, + isGenerating, + isRevealActiveForCurrentToken, + loading, + nameInput, + newMonitoringKioskLink, + newTokenRecord, + newTokenValue, + reopenTokenDialog, + scopedTokenCount, + scopeGroups, + scopePresets, + selectedScopes, + setCreateSectionRef, + setNameInput, + sortedTokens, + tokenHint, + toggleScope, + totalTokens, + wildcardCount, + presetMatchesSelection, + } = useAPITokenManagerState(props); return (
@@ -626,8 +194,7 @@ export const APITokenManager: Component = (props) => {