diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 63b9f13b3d..9838397212 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,15 +1,18 @@ { "version": 1, - "base_sha": "db7e26deac2a77dd5eff1dfb5bf2f1546683d5d6", - "verified_at": "2026-08-29T10:03:51Z", + "base_sha": "0df9747bd7ef97d768d5320e1952116ad4e675ea", + "verified_at": "2026-08-29T13:46:17Z", "result": "passed", "changed_paths": [ "frontend-modern/src/api/settings.ts" ], "content_sha256": { - "frontend-modern/src/api/settings.ts": "e54e7adfb7c66c8626e2cb0abcf20c1c7903b3d26e2f0050750a97a89811eab2" + "frontend-modern/src/api/settings.ts": "6b3b67e14054d6b93a0eeb92107426384b4d2badeec7c52798b7c00cf6f5e920" }, - "routes": ["/settings/general"], + "routes": [ + "/settings/system-general", + "/docs/PRIVACY" + ], "viewports": [ { "width": 1280, @@ -21,14 +24,17 @@ } ], "states": [ - "authenticated General settings with outbound telemetry locked off by PULSE_TELEMETRY", - "current heartbeat preview rendered from the schema-v13 backend at desktop width", - "current heartbeat preview rendered from the schema-v13 backend at narrow width" + "authenticated General settings with outbound telemetry disabled by PULSE_TELEMETRY=false", + "schema-14 heartbeat payload preview expanded at desktop width", + "schema-14 heartbeat payload preview expanded without document overflow at narrow width", + "rotating pseudonymous install ID refreshed after operator confirmation", + "shipped privacy document opened from the telemetry disclosure" ], "interactions": [ - "opened General settings and selected Preview payload", - "confirmed the preview explains that telemetry is disabled while still showing the exact payload Pulse would send", - "confirmed schema_version 13 plus service_health_observed, service_health_healthy, and service_health_cohort in the rendered payload at 1280x800", - "confirmed the same service-health payload fields remained visible and readable at 390x844" + "opened General settings at 1280x800 and selected Preview payload", + "confirmed schema_version 14 and representative alert severity, resolution-time, and persistence-health aggregates in the rendered payload", + "resized the current interaction to 390x844, confirmed the payload stayed visible, and verified no horizontal document overflow", + "selected Reset ID at narrow width, accepted the confirmation dialog, and confirmed the rendered install ID rotated", + "opened Full details and confirmed the shipped /docs/PRIVACY document rendered in a browser popup" ] } diff --git a/frontend-modern/src/api/settings.ts b/frontend-modern/src/api/settings.ts index 39b19ebf01..69ea4cef25 100644 --- a/frontend-modern/src/api/settings.ts +++ b/frontend-modern/src/api/settings.ts @@ -88,6 +88,7 @@ export interface TelemetryPingPreview { alerts_fired_30d: number; alerts_acknowledged_30d: number; alerts_resolved_30d: number; + // Coarse lifecycle aggregates only: no alert text, resource identity, or tenant identity. active_alerts_info: number; active_alerts_warning: number; active_alerts_critical: number; diff --git a/internal/api/api_token_org_routes_integration_test.go b/internal/api/api_token_org_routes_integration_test.go index ddd7d5e19c..973aba17a4 100644 --- a/internal/api/api_token_org_routes_integration_test.go +++ b/internal/api/api_token_org_routes_integration_test.go @@ -166,8 +166,19 @@ func TestOrgScopedAPITokenCannotReadOtherOrgMembersWithSessionCookiePresent(t *t "X-Org-ID": "org-a", } assertMembersStatus("/api/orgs/org-a/members", headers, http.StatusOK) - body := assertMembersStatus("/api/orgs/org-b/members", headers, http.StatusForbidden) + if _, initialized := router.mtMonitor.PeekMonitor("org-b"); initialized { + t.Fatal("cross-org target monitor initialized before the denied request") + } + crossOrgHeaders := map[string]string{ + "Authorization": "Bearer " + created.Token, + "X-Pulse-Org-ID": "org-b", + "X-Org-ID": "org-b", + } + body := assertMembersStatus("/api/orgs/org-b/members", crossOrgHeaders, http.StatusForbidden) if !strings.Contains(body, "access_denied") { t.Fatalf("expected cross-org denial payload, got %q", body) } + if _, initialized := router.mtMonitor.PeekMonitor("org-b"); initialized { + t.Fatal("denied global token request initialized the cross-org target monitor") + } } diff --git a/internal/api/auth.go b/internal/api/auth.go index e50b27f442..bdcdfb58d2 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -1322,6 +1322,21 @@ func extractAndStoreAuthContext(cfg *config.Config, mtm *monitoring.MultiTenantM if cfg.HasAPITokens() || mtm != nil { // Determine which config to use for validation (Global vs Tenant) targetConfig := cfg + providedToken, explicitTokenProvided := explicitAPITokenFromRequest(r) + + // Global tokens are the common control-plane credential. Authenticate + // them before resolving the header-selected tenant monitor: resolving a + // cold tenant can initialize its complete monitoring runtime, even when + // the already-known global token is bound to another organization and + // tenant authorization will reject the request. Besides wasting work on + // an unauthorized request, that initialization can exceed an API client + // timeout and leave cleanup queued behind the same cold start. + if explicitTokenProvided { + if record, ok := validateGlobalAPITokenLocked(cfg, providedToken); ok { + attachAPITokenRecord(r, record) + return attachUserContext(r, apiTokenAuthenticatedUser(record)) + } + } if mtm != nil { // Check for Tenant ID in header or cookie @@ -1351,7 +1366,7 @@ func extractAndStoreAuthContext(cfg *config.Config, mtm *monitoring.MultiTenantM return nil, false } - if providedToken, provided := explicitAPITokenFromRequest(r); provided { + if explicitTokenProvided { if req, ok := validateToken(providedToken); ok { return req } diff --git a/internal/api/hosted_agent_install_command_test.go b/internal/api/hosted_agent_install_command_test.go index 576272491f..3ef53e758f 100644 --- a/internal/api/hosted_agent_install_command_test.go +++ b/internal/api/hosted_agent_install_command_test.go @@ -279,7 +279,6 @@ func TestHostedTenantAgentInstallTokenCannotReportToOtherTenant(t *testing.T) { require.True(t, ok) require.Len(t, monitorA.GetLiveHostsSnapshot(), 1) - monitorB, ok := mtm.PeekMonitor("client-b") - require.True(t, ok) - require.Empty(t, monitorB.GetLiveHostsSnapshot(), "retargeted client-a token must not write into client-b") + _, ok = mtm.PeekMonitor("client-b") + require.False(t, ok, "retargeted client-a token must be denied before client-b runtime initialization") } diff --git a/tests/integration/tests/19-telemetry-disclosure.spec.ts b/tests/integration/tests/19-telemetry-disclosure.spec.ts index 6e14eeb363..165bb8156b 100644 --- a/tests/integration/tests/19-telemetry-disclosure.spec.ts +++ b/tests/integration/tests/19-telemetry-disclosure.spec.ts @@ -61,6 +61,10 @@ async function readTelemetryPreview(page: Page) { return JSON.parse((await preview.textContent()) ?? "{}") as { install_id: string; event: string; + schema_version: number; + active_alerts_warning: number; + alerts_resolution_under_15m_30d: number; + alert_active_state_persistence_degraded_tenants: number; }; } @@ -75,6 +79,7 @@ test.describe("Telemetry disclosure", () => { "Desktop-only telemetry disclosure coverage", ); + await page.setViewportSize({ width: 1280, height: 800 }); await page.goto("/settings/system-general", { waitUntil: "domcontentloaded", }); @@ -121,6 +126,26 @@ test.describe("Telemetry disclosure", () => { const initialPreview = await readTelemetryPreview(page); expect(initialPreview.event).toBe("heartbeat"); expect(initialPreview.install_id).toBeTruthy(); + expect(initialPreview.schema_version).toBe(14); + expect(initialPreview.active_alerts_warning).toBeGreaterThanOrEqual(0); + expect( + initialPreview.alerts_resolution_under_15m_30d, + ).toBeGreaterThanOrEqual(0); + expect( + initialPreview.alert_active_state_persistence_degraded_tenants, + ).toBeGreaterThanOrEqual(0); + + await page.setViewportSize({ width: 390, height: 844 }); + await expect( + page.locator('pre[aria-label="Telemetry payload preview"]'), + ).toBeVisible(); + expect( + await page.evaluate( + () => + document.documentElement.scrollWidth <= + document.documentElement.clientWidth, + ), + ).toBe(true); page.once("dialog", (dialog) => dialog.accept()); await page.getByRole("button", { name: "Reset ID" }).click();