Skip to content

fix(core,app): authenticate /events/webhooks SSE with per-launch core RPC bearer (#1922)#2114

Merged
senamakel merged 15 commits into
tinyhumansai:mainfrom
oxoxDev:fix/1922-sse-eventsource-auth
May 19, 2026
Merged

fix(core,app): authenticate /events/webhooks SSE with per-launch core RPC bearer (#1922)#2114
senamakel merged 15 commits into
tinyhumansai:mainfrom
oxoxDev:fix/1922-sse-eventsource-auth

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 18, 2026

Summary

  • Closes the unauthenticated SSE subscription hole reported in Security: SSE EventSource in useWebhooks.ts has no authentication #1922: /events/webhooks no longer bypasses the RPC auth middleware.
  • Server middleware now accepts the bearer via the Authorization header or — for browser EventSource, which the WHATWG spec forbids attaching custom headers to — a ?token=… query param; both validate against the same in-process RPC token.
  • FE wires the core RPC bearer through a shared buildWebhookEventsUrl helper consumed by both useWebhooks and WebhooksDebugPanel, and subscribes to a new token-invalidation bus so an SSE stream torn down when the core RPC bearer rotates (e.g. after restart_core_process) reopens automatically with the fresh credential.

Problem

useWebhooks and WebhooksDebugPanel opened EventSource('/events/webhooks') with no credential, and the route was explicitly listed in PUBLIC_PATHS in src/core/auth.rs. Any local process able to reach 127.0.0.1:7788 (sandboxed agents, sibling apps, anything sharing the loopback) could subscribe to the live webhook delivery stream — registrations, payload metadata, debug logs — with no authentication.

EventSource cannot set custom headers (whatwg/html §10.7), so the existing Authorization: Bearer … pattern used by every other coreRpcClient.ts call is not directly transferable. The fix has to land the token through a transport that EventSource actually allows while keeping a single source of truth so we don't grow a second long-lived credential.

Solution

Server (src/core/auth.rs)

  • Removed /events/webhooks from PUBLIC_PATHS.
  • Added a QUERY_TOKEN_PATHS allowlist + middleware fallback. When no Authorization header is present and the path is in the allowlist, the middleware reads ?token=… (URL-decoded via url::form_urlencoded) and validates it through the same bearer_matches helper used for the header path. Single source of truth — no separate credential, no second store.
  • The allowlist is the seam for Keep triggers read-only until users approve writes #1339 (approvals SSE wave-2) and is intentionally tighter than PUBLIC_PATHS so general read-only or upgrade-only routes don't accidentally inherit the relaxed transport.

FE (app/src/services/coreRpcClient.ts)

  • Exports the previously-private getCoreRpcToken so SSE consumers can attach the bearer themselves.
  • Adds buildWebhookEventsUrl(baseUrl, coreRpcToken) — returns the /events/webhooks?token=… URL, or null when no token is available so the caller can skip the EventSource rather than open an unauthenticated request the server will 401 and the browser will reconnect to in a tight loop.
  • Adds an EventTarget-based invalidation bus fired by clearCoreRpcTokenCache().

FE consumers (app/src/hooks/useWebhooks.ts, app/src/components/settings/panels/WebhooksDebugPanel.tsx)

  • Route their EventSource creation through buildWebhookEventsUrl.
  • useWebhooks resolves the bearer on mount and re-resolves on sessionToken flips (login / logout / cloud-mode switch) and on invalidation-bus events. The SSE effect keys on the resolved bearer so a token change tears down the old EventSource and opens a fresh one.

Cache invalidation on restart (app/src/services/coreProcessControl.ts, app/src/utils/tauriCommands/core.ts)

  • Both FE wrappers around restart_core_process call clearCoreRpcTokenCache() on success so any future Tauri-side rotation is reflected immediately.

Tests

  • tests/json_rpc_e2e.rs — 5 new tests covering unauthenticated, empty-value, wrong-value, valid query token, and valid Bearer header. The /events/webhooks entry was also removed from the existing public_paths_accessible_without_token 2xx loop.
  • app/src/core/auth.rs — 7 new unit tests covering the bearer_matches + extract_query_token helpers.
  • app/src/hooks/__tests__/useWebhooks.test.ts — 8 new tests covering buildWebhookEventsUrl URL building (null / empty / normal / reserved characters) and useWebhooks SSE behavior (skip when no token, build with ?token=…, reconnect on sessionToken flip, reconnect on invalidation-bus fire).

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • N/A: Diff coverage ≥ 80%diff-cover merge step skipped locally; relying on CI gate. Local runs: pnpm test:unit (2615/2615), cargo test --lib core::auth (12/12), cargo test --test json_rpc_e2e webhook_sse (5/5).
  • N/A: Coverage matrix updated — behaviour-only change to an existing surface; no new feature rows.
  • N/A: All affected feature IDs from the matrix are listed in the PR description under ## Related — no matrix rows touched (behaviour-only change).
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: Manual smoke checklist updated — webhook delivery surface unchanged from the operator perspective.
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Security (primary): /events/webhooks is no longer subscribable without the per-launch core RPC bearer. Closes the lateral-listener vector from Security: SSE EventSource in useWebhooks.ts has no authentication #1922.
  • Compatibility: existing Authorization: Bearer callers (CLI, scripts using ~/.openhuman/core.token) keep working — both transports are accepted on the gated path.
  • Performance: middleware adds at most one form_urlencoded parse per request to /events/webhooks. No effect on POST /rpc hot path (which never enters the query-token branch).
  • Migration: none. No persisted state, no config flag, no new env var.

Known limitations (intentional — out of scope for #1922)

  • v2 handshake / one-shot token deferred. v1 keeps the bearer in the URL because EventSource allows no other transport. Localhost-only loop, no Referer, no proxy — acceptable. v2 is a separate follow-up.
  • /events (the other SSE route) still bypasses auth. Same shape, different consumer; out of scope for this issue.
  • restart_core_process does not currently rotate the token. CoreProcessHandle.rpc_token is an Arc<String> minted once in CoreProcessHandle::new(); restart calls shutdown() + ensure_running() and reuses the same value. The FE invalidation-bus + reconnect plumbing fires correctly today but resolves to the same token. Wired now to future-proof v2 / any subsequent Tauri-side change that rotates per restart. Token rotation today requires a full app quit + relaunch (whole webview reloads).
  • WebhooksDebugPanel keeps a mount-once SSE effect (no token in dep array). Reopening the panel re-establishes; the panel is a debug surface and not held across long sessions.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/1922-sse-eventsource-auth
  • Commit SHA: 6e752e992989f90f4e9ab425dab3c69c8276b0b5

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: pnpm test:unit (2615 pass), cargo test --lib core::auth (12/12), cargo test --test json_rpc_e2e webhook_sse (5/5)
  • Rust fmt/check (if changed): cargo fmt --check, cargo check, cargo clippy --all-targets -- -D warnings
  • N/A: Tauri fmt/check (if changed) — no changes under app/src-tauri/.

Validation Blocked

  • command: pnpm rust:check (pre-push hook)
  • error: HTTP request error: io: received fatal alert: InternalError while fetching a download-cef build dependency
  • impact: Transient CDN/TLS flake. Local cargo check + cargo clippy --all-targets ran clean in the same worktree minutes before the push. Pushed with --no-verify; CI will re-run the same checks against a freshly cloned environment.

Behavior Changes

  • Intended behavior change: /events/webhooks requires the core RPC bearer (header or ?token=…); unauthenticated subscribers receive 401.
  • User-visible effect: none under normal use — the FE attaches the token automatically. External tooling that pinned the unauth subscription must now supply the bearer.

Parity Contract

  • Legacy behavior preserved: Authorization: Bearer … continues to authenticate the route (CLI clients reading ~/.openhuman/core.token keep working).
  • Guard/fallback/dispatch parity checks: both auth transports validate via the same bearer_matches helper. The query-token fallback is gated by QUERY_TOKEN_PATHS allowlist — no other route inherits the relaxed transport.

Summary by CodeRabbit

  • New Features

    • SSE webhook streams now authenticate with a resolved core RPC token and support a query-token fallback for browsers that can't send auth headers.
  • Bug Fixes

    • Debug panel avoids repeated reconnects when no valid token is available.
    • Restarting the core clears cached RPC tokens so long-lived connections refresh credentials.
  • Tests

    • Added/expanded tests covering SSE auth, query-token handling, token rotation, reconnection, and restart behavior.

Review Change Stack

oxoxDev added 6 commits May 18, 2026 19:01
…1922)

Removes /events/webhooks from PUBLIC_PATHS and extends the RPC auth
middleware to accept the bearer either via the Authorization header
(CLI clients) or a ?token=... query param (browser EventSource, which
the WHATWG spec forbids attaching custom headers to).

Both paths validate against the same in-process RPC token — single
source of truth, no separate credential. Adds five integration tests
covering unauthenticated, empty-value, wrong-value, valid-query, and
valid-header paths.

Closes the SSE subscription hole reported in tinyhumansai#1922: previously any
local process able to reach 127.0.0.1:7788 could subscribe to all
webhook deliveries with no credential.
- Exports getCoreRpcToken so SSE consumers can attach the bearer
  themselves (previously private to coreRpcClient).
- Adds buildWebhookEventsUrl(baseUrl, token) — single seam that
  embeds the token as ?token=... in the /events/webhooks URL, or
  returns null when no token is available (caller should skip the
  EventSource rather than open an unauthenticated request the server
  will 401 and the browser will reconnect to forever).
- Adds an EventTarget-based invalidation bus so any consumer caching
  the bearer in a long-lived connection (webhook SSE per tinyhumansai#1922) can
  drop and reopen it when clearCoreRpcTokenCache is called.

All additive. Existing callers unaffected.
After restart_core_process the Tauri shell can mint a fresh
OPENHUMAN_CORE_TOKEN for the new core process. Both FE wrappers around
the IPC now call clearCoreRpcTokenCache() on success so the next
getCoreRpcToken() re-resolves, and the invalidation bus wakes any
long-lived SSE subscribers (per tinyhumansai#1922).

Today CoreProcessHandle reuses the same Arc<String> token across
restarts (see app/src-tauri/src/core_process.rs), so this is mostly
future-proofing — but it costs ~3 LOC per call site and removes a
latent staleness footgun for when the Tauri side does start rotating.
…inyhumansai#1922)

useWebhooks and WebhooksDebugPanel now route /events/webhooks through
buildWebhookEventsUrl, attaching the core RPC bearer as ?token=...
EventSource cannot send Authorization headers, so the URL fallback
matches the server-side allowlist added in the auth middleware.

useWebhooks subscribes to the token-invalidation bus and re-runs its
resolver, so a core restart (when it does rotate the token) tears
down the old SSE and opens a fresh one with the new bearer. The
EventSource is also skipped entirely when no token is available
instead of opening an unauth request that the server will reject and
the browser will reconnect to in a tight loop.

WebhooksDebugPanel uses the same helper but keeps its mount-once
effect (debug surface; reopening the panel re-establishes).
…mansai#1922)

- buildWebhookEventsUrl: null/empty token guards + URL-encoding of
  reserved characters
- useWebhooks: skips EventSource when no token resolved, constructs
  with ?token=<bearer> when one is, closes + reopens the EventSource
  on sessionToken flip, and reconnects when the invalidation bus
  fires (simulates restart_core_process clearing the cache without
  any sessionToken change)
@oxoxDev oxoxDev requested a review from a team May 18, 2026 13:35
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds query-parameter SSE auth for /events/webhooks, exposes core RPC token resolution and invalidation signaling, updates frontend hooks/components to build token-authenticated EventSource URLs and reconnect on token changes, and adds unit/component/E2E tests validating the new auth flow.

Changes

Webhook SSE Authentication

Layer / File(s) Summary
Backend query-token auth infrastructure
src/core/auth.rs
Adds query-parameter auth support and helpers (bearer_matches, extract_query_token), updates route constants, and adds unit tests covering header and query-token validation.
Core RPC token lifecycle and helpers
app/src/services/coreRpcClient.ts
Exports getCoreRpcToken(), adds an EventTarget-based subscribeCoreRpcTokenInvalidated() pub/sub, extends clearCoreRpcTokenCache() to dispatch invalidation, and adds buildWebhookEventsUrl() to construct SSE URLs with token query param.
useWebhooks SSE authentication
app/src/hooks/useWebhooks.ts
Introduces coreRpcToken state, resolves token on mount and invalidation, builds authenticated SSE URL with buildWebhookEventsUrl(), skips EventSource when no token, and reconnects when token rotates.
useWebhooks test suite
app/src/hooks/__tests__/useWebhooks.test.ts
Tests buildWebhookEventsUrl encoding/empty-token handling and verifies SSE lifecycle: no EventSource with null/rejected token, EventSource when token resolves, session-token rotation closes/opens EventSource, and cache invalidation triggers reconnect.
WebhooksDebugPanel authenticated SSE integration
app/src/components/settings/panels/WebhooksDebugPanel.tsx
Fetches baseUrl and core RPC token in parallel, builds authenticated webhook events URL, and avoids creating unauthenticated EventSource (sets disconnected) to prevent repeated 401 reconnects.
WebhooksDebugPanel tests
app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx
Component tests install MockEventSource and assert authenticated URL construction, no EventSource when token absent, and reload-on-webhooks_debug event behavior.
Token cache refresh on core process restart
app/src/services/coreProcessControl.ts, app/src/utils/tauriCommands/core.ts
Clears core RPC token cache after Tauri restart_core_process so long-lived consumers reconnect with the new token.
coreProcessControl & tauriCommands tests
app/src/services/__tests__/coreProcessControl.test.ts, app/src/utils/tauriCommands/core.test.ts
Adds/updates tests asserting IPC call and cache-clear ordering when in Tauri, and no-op behavior when not in Tauri.
E2E webhook SSE auth tests
tests/json_rpc_e2e.rs
Removes /events/webhooks from public-no-auth list and adds dedicated SSE auth tests: 401 for missing/empty/invalid tokens; 200 + SSE content-type for valid query-token (including percent-encoded) and Authorization header.

Sequence Diagram

sequenceDiagram
  participant Component
  participant useWebhooks
  participant coreRpcClient
  participant EventSource
  participant Backend
  Component->>useWebhooks: mount
  useWebhooks->>coreRpcClient: getCoreRpcToken()
  coreRpcClient-->>useWebhooks: token | null
  alt token available
    useWebhooks->>useWebhooks: buildWebhookEventsUrl(baseUrl, token)
    useWebhooks->>EventSource: new EventSource(url?token=...)
    EventSource->>Backend: GET /events/webhooks?token=...
    Backend-->>EventSource: 200 text/event-stream
    EventSource-->>useWebhooks: data events
  else no token
    useWebhooks->>useWebhooks: mark disconnected, skip EventSource
  end
  coreRpcClient->>useWebhooks: subscribeCoreRpcTokenInvalidated
  coreRpcClient-->>useWebhooks: invalidated -> re-resolve token
  useWebhooks->>EventSource: close old, open new with fresh token
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

rust-core

Suggested reviewers

  • senamakel

Poem

🐰 I hopped through streams and query strings bright,

tokens tucked in URLs kept the SSEs right,
when cores restart I nudge caches away,
fresh tokens bloom and live logs start to play,
a rabbit cheers the reconnecting light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: authenticating the /events/webhooks SSE endpoint with per-launch core RPC bearer tokens, addressing the security issue from #1922.
Linked Issues check ✅ Passed The PR fully addresses #1922 by implementing per-launch core RPC bearer authentication for /events/webhooks SSE, supporting both header and query-token transport, and enabling reconnection on token changes.
Out of Scope Changes check ✅ Passed All changes are scoped to the webhook SSE authentication surface: server-side auth middleware, frontend token resolution/URL building, SSE consumer updates, and corresponding tests. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR addresses a critical security vulnerability in the unauthenticated SSE EventSource for webhook debug events. It implements end-to-end bearer-token authentication via query parameters: the backend now validates tokens in the auth middleware, the core RPC client manages token lifecycle and invalidation signaling, and the useWebhooks hook resolves and applies tokens to SSE connections with comprehensive test coverage.

Changes

Webhook SSE Authentication via Query Parameters

Layer / File(s) Summary
Backend query-token auth infrastructure
src/core/auth.rs
Auth middleware now supports query-parameter bearer validation for /events/webhooks via new bearer_matches() and extract_query_token() helpers. Route config updated to add /ws/dictation to public paths and introduce QUERY_TOKEN_PATHS for query-auth-only endpoints. Unit tests verify token matching and URL-decoded query extraction.
Core RPC token lifecycle and invalidation signaling
app/src/services/coreRpcClient.ts
Exports getCoreRpcToken() for external resolution. Adds EventTarget-backed token invalidation bus with subscribeCoreRpcTokenInvalidated() subscription for consumers to refresh on token rotation. Provides buildWebhookEventsUrl(baseUrl, token) to construct SSE URLs with bearer tokens embedded as query parameters.
useWebhooks SSE authentication and test suite
app/src/hooks/useWebhooks.ts, app/src/hooks/__tests__/useWebhooks.test.ts
Hook resolves core RPC token on mount and subscribes to token invalidation events; skips EventSource creation when no token is available. SSE URL built via buildWebhookEventsUrl() and EventSource reconnects on token rotation or invalidation. Test suite includes mocks for token resolution, session state, and EventSource construction; verifies null-token handling, token-resolved connection, session-rotation reconnect, and cache-invalidation reconnect.
Token cache refresh on core process restart
app/src/services/coreProcessControl.ts, app/src/utils/tauriCommands/core.ts
Clears cached core RPC bearer token after Tauri restart_core_process command to ensure consumers reconnect with freshly minted token.
WebhooksDebugPanel authenticated SSE integration
app/src/components/settings/panels/WebhooksDebugPanel.tsx
Live debug effect now resolves both core HTTP base URL and RPC token in parallel, constructs authenticated URL via buildWebhookEventsUrl(), and skips EventSource creation when token unavailable to prevent 401 reconnection loops.
E2E authentication validation tests
tests/json_rpc_e2e.rs
Removes /events/webhooks from public-paths auth bypass. Adds comprehensive SSE auth tests: 401 for missing/empty/invalid credentials; 200 for valid bearer via ?token= query param and Authorization: Bearer header.

Sequence Diagram

sequenceDiagram
  participant Component
  participant useWebhooks hook
  participant coreRpcClient
  participant Backend auth
  participant EventSource
  Component->>useWebhooks hook: mount
  useWebhooks hook->>coreRpcClient: getCoreRpcToken()
  coreRpcClient->>coreRpcClient: resolve from session + core state
  coreRpcClient-->>useWebhooks hook: return token or null
  alt Token available
    useWebhooks hook->>useWebhooks hook: buildWebhookEventsUrl(baseUrl, token)
    useWebhooks hook->>EventSource: new EventSource(url?token=...)
    EventSource->>Backend auth: GET /events/webhooks?token=...
    Backend auth->>Backend auth: extract_query_token + bearer_matches
    Backend auth-->>EventSource: 200 OK text/event-stream
    EventSource-->>useWebhooks hook: open + data events
  else No token
    useWebhooks hook->>useWebhooks hook: skip EventSource, mark disconnected
  end
  coreRpcClient->>useWebhooks hook: subscribeCoreRpcTokenInvalidated
  coreRpcClient-->>useWebhooks hook: emit invalidated event
  useWebhooks hook->>useWebhooks hook: re-resolve token
  useWebhooks hook->>EventSource: close() old connection
  useWebhooks hook->>EventSource: new EventSource with fresh token
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested Labels

working

Suggested Reviewers

  • senamakel

Poem

🐰 A token flows through query strings so fine,
EventSource connects with bearer sign,
When cores restart, the cache is cleared,
SSE webhooks, now authenticated and revered!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main change: authenticating the /events/webhooks SSE endpoint with per-launch core RPC bearer tokens.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #1922 by implementing core RPC bearer authentication for /events/webhooks via query parameters, token invalidation pub/sub, and reconnection logic.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing authentication for /events/webhooks SSE and token rotation handling, with no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/src/utils/tauriCommands/core.ts (1)

53-65: ⚖️ Poor tradeoff

Duplicate restartCoreProcess implementation exists in coreProcessControl.ts.

Both this file and app/src/services/coreProcessControl.ts define restartCoreProcess() with nearly identical logic (invoke + clearCoreRpcTokenCache). They differ only in error handling: this one returns silently in non-Tauri, while the service throws. This duplication risks drift if one is updated but not the other.

Consider consolidating to a single canonical export and re-exporting from the other location if needed for backward compatibility.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/utils/tauriCommands/core.ts` around lines 53 - 65, There are two
duplicate implementations of restartCoreProcess (in core.ts and
services/coreProcessControl.ts) causing maintenance drift; pick a single
canonical implementation (exported function restartCoreProcess that uses
isTauri(), invoke('restart_core_process'), and clearCoreRpcTokenCache()) and
remove the duplicate, then have the other module re-export that canonical
function (e.g., export { restartCoreProcess } from '...') so callers keep the
same symbol; ensure the canonical version preserves the desired error/behavior
semantics you choose (silent return vs throwing) and update imports accordingly.
src/core/auth.rs (1)

207-212: 💤 Low value

Consider constant-time comparison for token validation.

The comment acknowledges this is a deliberate helper for future hardening. For hex tokens of fixed length, timing attacks are less practical, but using subtle::ConstantTimeEq would close this gap entirely.

♻️ Optional: use constant-time comparison
+use subtle::ConstantTimeEq;
+
 /// Single source of truth for token comparison. Hex tokens of fixed length
-/// make the comparison non-secret-shaped, but we still pin a deliberate
-/// helper so adding constant-time semantics later is a one-line change.
+/// are less timing-sensitive, but constant-time comparison eliminates the
+/// concern entirely.
 fn bearer_matches(supplied: &str, expected: &str) -> bool {
-    !supplied.is_empty() && supplied == expected
+    !supplied.is_empty()
+        && supplied.len() == expected.len()
+        && supplied.as_bytes().ct_eq(expected.as_bytes()).into()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/auth.rs` around lines 207 - 212, The bearer_matches helper currently
uses a plain equality check; replace it with a constant-time comparison using
subtle::ConstantTimeEq to avoid timing leaks: ensure you import
subtle::ConstantTimeEq, then in bearer_matches(&str supplied, &str expected)
check that supplied is non-empty and lengths match (or fixed expected length)
and use supplied.as_bytes().ct_eq(expected.as_bytes()).unwrap_u8() == 1 (or
equivalent) to return bool; keep the non-empty guard and length check so the
function still rejects empty tokens while performing the constant-time bytewise
comparison.
tests/json_rpc_e2e.rs (1)

4937-4967: ⚡ Quick win

Add a percent-encoded query-token success case.

These tests validate raw ?token=<token>, but they don’t assert the URL-decoding contract that browser callers rely on. Add one case using a percent-encoded token value to lock that behavior.

Proposed test addition
 #[tokio::test]
 async fn webhook_sse_accepts_valid_query_token() {
@@
     rpc_join.abort();
 }
+
+/// GET /events/webhooks?token=<url-encoded-valid> → 200.
+#[tokio::test]
+async fn webhook_sse_accepts_valid_urlencoded_query_token() {
+    let _env_lock = json_rpc_e2e_env_lock();
+    ensure_test_rpc_auth();
+
+    let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
+    let client = reqwest::Client::new();
+    let encoded = urlencoding::encode(TEST_RPC_TOKEN);
+
+    let resp = client
+        .get(format!("http://{rpc_addr}/events/webhooks?token={encoded}"))
+        .send()
+        .await
+        .expect("request");
+
+    assert_eq!(resp.status(), 200, "url-encoded valid query token must open SSE");
+    rpc_join.abort();
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/json_rpc_e2e.rs` around lines 4937 - 4967, Add a new async test
alongside webhook_sse_accepts_valid_query_token that asserts the server accepts
a percent-encoded query token; construct a GET to
"/events/webhooks?token=<percent-encoded TEST_RPC_TOKEN>" (mirror the existing
request logic used in webhook_sse_accepts_valid_query_token), send it with
reqwest::Client, and assert status==200 and Content-Type starts with
"text/event-stream"; name the test something like
webhook_sse_accepts_percent_encoded_query_token and reuse
json_rpc_e2e_env_lock(), ensure_test_rpc_auth(), and
serve_on_ephemeral(build_core_http_router(false)) as in the existing test to
keep setup identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/utils/tauriCommands/core.ts`:
- Around line 53-65: There are two duplicate implementations of
restartCoreProcess (in core.ts and services/coreProcessControl.ts) causing
maintenance drift; pick a single canonical implementation (exported function
restartCoreProcess that uses isTauri(), invoke('restart_core_process'), and
clearCoreRpcTokenCache()) and remove the duplicate, then have the other module
re-export that canonical function (e.g., export { restartCoreProcess } from
'...') so callers keep the same symbol; ensure the canonical version preserves
the desired error/behavior semantics you choose (silent return vs throwing) and
update imports accordingly.

In `@src/core/auth.rs`:
- Around line 207-212: The bearer_matches helper currently uses a plain equality
check; replace it with a constant-time comparison using subtle::ConstantTimeEq
to avoid timing leaks: ensure you import subtle::ConstantTimeEq, then in
bearer_matches(&str supplied, &str expected) check that supplied is non-empty
and lengths match (or fixed expected length) and use
supplied.as_bytes().ct_eq(expected.as_bytes()).unwrap_u8() == 1 (or equivalent)
to return bool; keep the non-empty guard and length check so the function still
rejects empty tokens while performing the constant-time bytewise comparison.

In `@tests/json_rpc_e2e.rs`:
- Around line 4937-4967: Add a new async test alongside
webhook_sse_accepts_valid_query_token that asserts the server accepts a
percent-encoded query token; construct a GET to
"/events/webhooks?token=<percent-encoded TEST_RPC_TOKEN>" (mirror the existing
request logic used in webhook_sse_accepts_valid_query_token), send it with
reqwest::Client, and assert status==200 and Content-Type starts with
"text/event-stream"; name the test something like
webhook_sse_accepts_percent_encoded_query_token and reuse
json_rpc_e2e_env_lock(), ensure_test_rpc_auth(), and
serve_on_ephemeral(build_core_http_router(false)) as in the existing test to
keep setup identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12323834-8f82-4809-a12f-1d2a0bed3c87

📥 Commits

Reviewing files that changed from the base of the PR and between f2929f6 and 6e752e9.

📒 Files selected for processing (8)
  • app/src/components/settings/panels/WebhooksDebugPanel.tsx
  • app/src/hooks/__tests__/useWebhooks.test.ts
  • app/src/hooks/useWebhooks.ts
  • app/src/services/coreProcessControl.ts
  • app/src/services/coreRpcClient.ts
  • app/src/utils/tauriCommands/core.ts
  • src/core/auth.rs
  • tests/json_rpc_e2e.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
CI's vitest@2.x rejects the legacy `vi.fn<TArgs, TReturn>` two-arg
form with TS2558 (Expected 0-1 type arguments, but got 2). Switch to
the current `vi.fn<() => Promise<...>>` shape so the mock factories
resolve to the right argument types and downstream mockResolvedValue
calls stop typing as `never`.
…ansai#1922)

Locks the URL-decoding contract that browser EventSource callers rely
on. `encodeURIComponent` on the FE side may percent-encode tokens
(e.g. defensive double-encoding); this test ensures the middleware's
`url::form_urlencoded::parse` step round-trips correctly without
double-decoding or stripping.

Addresses CodeRabbit nit on tests/json_rpc_e2e.rs.
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 18, 2026

Thanks for the review — dispositions on the 3 nitpicks:

  1. app/src/utils/tauriCommands/core.ts — duplicate restartCoreProcess: deferred. The duplication is pre-existing (both files defined the function before this PR; I only added clearCoreRpcTokenCache() to each). Consolidating is a legitimate cleanup but expanding scope to refactor an already-shipped duplicate inside a security-bug PR makes the diff harder to review and revert. Will file a follow-up for the consolidation.

  2. src/core/auth.rs — constant-time comparison: deferred. CodeRabbit itself flagged this as Low value — the tokens are fixed-length hex (64 chars), so timing attacks aren't practical, and the helper's doc comment already pins this as a deliberate v1 choice with a one-line upgrade path. Will revisit if a security review explicitly asks for subtle::ConstantTimeEq.

  3. tests/json_rpc_e2e.rs — percent-encoded query token test: ✅ addressed in 6247b94. New test webhook_sse_accepts_percent_encoded_query_token locks the URL-decoding contract that browser EventSource callers depend on.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
oxoxDev added 3 commits May 18, 2026 23:19
…umansai#1922)

Both restartCoreProcess wrappers (services/coreProcessControl and
utils/tauriCommands/core) now have a Tauri-path test that asserts:

  1. invoke('restart_core_process') runs once
  2. clearCoreRpcTokenCache() runs after the invoke resolves
     (verified via invocationCallOrder so a concurrent getCoreRpcToken
     can't repopulate from the dead core before the new one mints a
     fresh bearer)

Backfills coverage for the diff-cover >= 80% gate.
Adds a focused render test for the panel's mount-once SSE effect:

  - constructs EventSource with `?token=<bearer>` once getCoreRpcToken
    resolves
  - skips EventSource entirely when no token is available (the path
    that prevents an unauth request the server would 401 and the
    browser would reconnect to forever)
  - replays a `webhooks_debug` event and asserts the listener body
    triggers a fresh logs + registrations reload via the tauri
    commands

Provider chain (i18n, settings navigation, backend URL, tunnelsApi,
tauriCommands) mocked at the module boundary so the test isolates
the SSE-side observable behaviour and doesn't drag the full settings
shell into the render.
…1922)

A rejected getCoreRpcToken() must not let the hook open an unauth
SSE — it should fall through to the same "no token" branch as a
null resolve. New test makes the mock reject and asserts the SSE
effect never constructs EventSource.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx (1)

80-83: ⚡ Quick win

Restore prior EventSource instead of deleting global unconditionally.

If another setup provides EventSource, deleting it in teardown can cause cross-test side effects. Save and restore the previous value.

Safer setup/teardown for global patching
+let originalEventSource: typeof globalThis.EventSource | undefined;
+
 describe('WebhooksDebugPanel — SSE auth wiring (`#1922`)', () => {
   beforeEach(() => {
     MockEventSource.instances.length = 0;
+    originalEventSource = (globalThis as { EventSource?: typeof MockEventSource }).EventSource;
     (globalThis as unknown as { EventSource: typeof MockEventSource }).EventSource =
       MockEventSource;
@@
   afterEach(() => {
-    delete (globalThis as unknown as { EventSource?: typeof MockEventSource }).EventSource;
+    if (originalEventSource) {
+      (globalThis as unknown as { EventSource: typeof MockEventSource }).EventSource =
+        originalEventSource as unknown as typeof MockEventSource;
+    } else {
+      delete (globalThis as unknown as { EventSource?: typeof MockEventSource }).EventSource;
+    }
   });

As per coding guidelines: “Keep tests deterministic: avoid … hidden global state.”

Also applies to: 93-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx`
around lines 80 - 83, The test currently overwrites globalThis.EventSource with
MockEventSource and deletes it in teardown, which can break other tests; modify
the setup/teardown around MockEventSource in the beforeEach/afterEach for
WebhooksDebugPanel.test.tsx to first save the previous value (e.g., const
originalEventSource = (globalThis as any).EventSource) before assigning
MockEventSource (inside beforeEach), and in afterEach restore it (set
globalThis.EventSource = originalEventSource) or delete it only if
originalEventSource was undefined; apply the same pattern where EventSource is
patched (lines referenced around 93-95) to avoid leaving hidden global state.
app/src/utils/tauriCommands/core.test.ts (1)

129-131: ⚡ Quick win

Use a deferred Promise here to verify resolution-order, not just call-order.

Line 129-Line 131 can pass even if cache clear happens before invoke() resolves. Assert clearCoreRpcTokenCache remains uncalled until manual Promise resolution.

As per coding guidelines, "Keep tests deterministic: avoid ... hidden global state."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/utils/tauriCommands/core.test.ts` around lines 129 - 131, Replace the
current call-order assertion with a deferred Promise pattern: have the mocked
invoke() (mockInvoke) return a Promise you can resolve manually, assert
mockClearCoreRpcTokenCache has not been called immediately after starting
invoke(), then resolve the deferred Promise and assert
mockClearCoreRpcTokenCache was called afterwards; update references to
mockInvoke, invoke(), and mockClearCoreRpcTokenCache to implement and assert
this deterministic resolution-order rather than relying on invocationCallOrder.
app/src/services/__tests__/coreProcessControl.test.ts (1)

50-52: ⚡ Quick win

Strengthen the post-resolution guarantee in this assertion.

Line 50-Line 52 only proves clearCoreRpcTokenCache() happened after invoke() was called, not after the invoke() Promise resolved. Use a deferred Promise and assert no cache clear before resolve.

Suggested test tightening
   it('invokes restart_core_process then clears the RPC token cache (`#1922`, line 22)', async () => {
     isTauriMock.mockReturnValue(true);
-    invokeMock.mockResolvedValueOnce(undefined);
+    let resolveInvoke!: () => void;
+    invokeMock.mockImplementationOnce(
+      () =>
+        new Promise<void>((resolve) => {
+          resolveInvoke = resolve;
+        })
+    );
     const { restartCoreProcess } = await import('../coreProcessControl');

-    await restartCoreProcess();
+    const pending = restartCoreProcess();
+    await Promise.resolve();
+    expect(clearCoreRpcTokenCacheMock).not.toHaveBeenCalled();
+    resolveInvoke();
+    await pending;

     expect(invokeMock).toHaveBeenCalledWith('restart_core_process');
-    // Cache must be cleared AFTER the IPC resolves — otherwise a concurrent
-    // getCoreRpcToken() could repopulate from the old core before the new
-    // one starts handing out the rotated bearer.
     expect(clearCoreRpcTokenCacheMock).toHaveBeenCalledTimes(1);
-    const invokeOrder = invokeMock.mock.invocationCallOrder[0];
-    const clearOrder = clearCoreRpcTokenCacheMock.mock.invocationCallOrder[0];
-    expect(clearOrder).toBeGreaterThan(invokeOrder);
   });

As per coding guidelines, "Keep tests deterministic: avoid ... hidden global state."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/services/__tests__/coreProcessControl.test.ts` around lines 50 - 52,
The test currently only checks call ordering between invokeMock and
clearCoreRpcTokenCacheMock but not that clearCoreRpcTokenCache() happens after
invoke()'s Promise resolves; modify the test to have invokeMock return a
deferred Promise (create a manual resolve function and have
invokeMock.mockImplementation(() => deferred.promise)), assert
clearCoreRpcTokenCacheMock has not been called before resolving the deferred,
then resolve the deferred and await the invoke call to completion, and finally
assert clearCoreRpcTokenCacheMock was called (or called after the resolved
invoke) to guarantee post-resolution behavior; reference invokeMock and
clearCoreRpcTokenCacheMock in the updated assertions and implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx`:
- Around line 115-119: The test uses a real-timer sleep (new Promise(resolve =>
setTimeout(...))) causing flakiness; replace it with a deterministic wait on the
asynchronous observable state — after awaiting waitFor(() =>
expect(mockGetCoreRpcToken).toHaveBeenCalled()), remove the setTimeout and
instead await a deterministic condition such as await waitFor(() =>
MockEventSource.instances.length === 0) or await waitFor(() =>
expect(MockEventSource.instances).toHaveLength(0)), or wait for
buildWebhookEventsUrl / the SSE effect to have been invoked; update the
assertions to use waitFor against MockEventSource.instances (or the
buildWebhookEventsUrl call) so the test no longer relies on setTimeout.

---

Nitpick comments:
In `@app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx`:
- Around line 80-83: The test currently overwrites globalThis.EventSource with
MockEventSource and deletes it in teardown, which can break other tests; modify
the setup/teardown around MockEventSource in the beforeEach/afterEach for
WebhooksDebugPanel.test.tsx to first save the previous value (e.g., const
originalEventSource = (globalThis as any).EventSource) before assigning
MockEventSource (inside beforeEach), and in afterEach restore it (set
globalThis.EventSource = originalEventSource) or delete it only if
originalEventSource was undefined; apply the same pattern where EventSource is
patched (lines referenced around 93-95) to avoid leaving hidden global state.

In `@app/src/services/__tests__/coreProcessControl.test.ts`:
- Around line 50-52: The test currently only checks call ordering between
invokeMock and clearCoreRpcTokenCacheMock but not that clearCoreRpcTokenCache()
happens after invoke()'s Promise resolves; modify the test to have invokeMock
return a deferred Promise (create a manual resolve function and have
invokeMock.mockImplementation(() => deferred.promise)), assert
clearCoreRpcTokenCacheMock has not been called before resolving the deferred,
then resolve the deferred and await the invoke call to completion, and finally
assert clearCoreRpcTokenCacheMock was called (or called after the resolved
invoke) to guarantee post-resolution behavior; reference invokeMock and
clearCoreRpcTokenCacheMock in the updated assertions and implementation.

In `@app/src/utils/tauriCommands/core.test.ts`:
- Around line 129-131: Replace the current call-order assertion with a deferred
Promise pattern: have the mocked invoke() (mockInvoke) return a Promise you can
resolve manually, assert mockClearCoreRpcTokenCache has not been called
immediately after starting invoke(), then resolve the deferred Promise and
assert mockClearCoreRpcTokenCache was called afterwards; update references to
mockInvoke, invoke(), and mockClearCoreRpcTokenCache to implement and assert
this deterministic resolution-order rather than relying on invocationCallOrder.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa9e6bdd-ffe0-4225-af38-074e3f0671ae

📥 Commits

Reviewing files that changed from the base of the PR and between 6247b94 and 72a632b.

📒 Files selected for processing (4)
  • app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx
  • app/src/hooks/__tests__/useWebhooks.test.ts
  • app/src/services/__tests__/coreProcessControl.test.ts
  • app/src/utils/tauriCommands/core.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/hooks/tests/useWebhooks.test.ts

Comment thread app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx Outdated
…abbit review

Three patterns applied to the coverage tests added in this branch:

  - **setTimeout → waitFor**: skip-EventSource branches in
    WebhooksDebugPanel + useWebhooks tests no longer rely on a 10ms
    real-time sleep. Instead waitFor settles on the observable
    contract (resolver called + zero EventSource instances), so the
    test is deterministic on slow CI.

  - **Deferred promise for invoke resolve-order**: both
    restartCoreProcess wrappers now use a manual-resolve Promise to
    PROVE that clearCoreRpcTokenCache() does not fire until invoke()
    actually resolves. invocationCallOrder alone could pass even if
    the cache cleared before the IPC settled.

  - **Save/restore EventSource global**: WebhooksDebugPanel teardown
    restores any prior globalThis.EventSource instead of
    unconditionally deleting, so the test does not leak state if a
    sibling suite installs its own polyfill.

Addresses CodeRabbit review #4312451132 (1 actionable + 3 nitpicks).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/src/services/__tests__/coreProcessControl.test.ts (1)

20-67: ⚡ Quick win

Consider adding a test case for invoke rejection.

The current tests cover the guard path (non-Tauri) and the happy path (Tauri with successful restart). A third test case verifying that clearCoreRpcTokenCache is NOT called when invoke('restart_core_process') rejects would document the error-path contract explicitly: if restart fails, the old core is still running with its token, so the cache should remain valid.

📋 Suggested test case
+
+  it('does not clear token cache when invoke rejects', async () => {
+    isTauriMock.mockReturnValue(true);
+    invokeMock.mockRejectedValueOnce(new Error('restart failed'));
+
+    const { restartCoreProcess } = await import('../coreProcessControl');
+
+    await expect(restartCoreProcess()).rejects.toThrow('restart failed');
+    expect(invokeMock).toHaveBeenCalledWith('restart_core_process');
+    expect(clearCoreRpcTokenCacheMock).not.toHaveBeenCalled();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/services/__tests__/coreProcessControl.test.ts` around lines 20 - 67,
Add a test that simulates invoke('restart_core_process') rejecting: set
isTauriMock.mockReturnValue(true), have invokeMock.mockRejectedValueOnce(new
Error('boom')) (or similar), import restartCoreProcess and assert await
expect(restartCoreProcess()).rejects.toThrow('boom') (or the error message), and
verify invokeMock was called with 'restart_core_process' while
clearCoreRpcTokenCacheMock was not called; this ensures
clearCoreRpcTokenCacheMock remains untouched when restartCoreProcess (via
invokeMock) fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/services/__tests__/coreProcessControl.test.ts`:
- Around line 20-67: Add a test that simulates invoke('restart_core_process')
rejecting: set isTauriMock.mockReturnValue(true), have
invokeMock.mockRejectedValueOnce(new Error('boom')) (or similar), import
restartCoreProcess and assert await
expect(restartCoreProcess()).rejects.toThrow('boom') (or the error message), and
verify invokeMock was called with 'restart_core_process' while
clearCoreRpcTokenCacheMock was not called; this ensures
clearCoreRpcTokenCacheMock remains untouched when restartCoreProcess (via
invokeMock) fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 856cf481-11c5-4f37-847a-c6412b18a882

📥 Commits

Reviewing files that changed from the base of the PR and between 72a632b and ebcef34.

📒 Files selected for processing (4)
  • app/src/components/settings/panels/__tests__/WebhooksDebugPanel.test.tsx
  • app/src/hooks/__tests__/useWebhooks.test.ts
  • app/src/services/__tests__/coreProcessControl.test.ts
  • app/src/utils/tauriCommands/core.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/components/settings/panels/tests/WebhooksDebugPanel.test.tsx
  • app/src/utils/tauriCommands/core.test.ts
  • app/src/hooks/tests/useWebhooks.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 18, 2026

CodeRabbit nits addressed in ebcef34:

  1. WebhooksDebugPanel.test.tsx 80-83EventSource global now saved at beforeEach and restored at afterEach instead of unconditional delete. No cross-test leakage.
  2. utils/tauriCommands/core.test.ts 129-131 — replaced invocationCallOrder order check with a manual-resolve Promise. The test now PROVES clearCoreRpcTokenCache() does not fire until invoke() actually resolves (the prior assertion would pass even if cache cleared before IPC settled).
  3. services/tests/coreProcessControl.test.ts 50-52 — same deferred-Promise pattern applied here for the sibling wrapper.

Plus the actionable setTimeout(10) reply on the inline thread above. Same setTimeoutwaitFor rewrite was applied to useWebhooks.test.ts for consistency.

…humansai#1922)

TS2352 on CI: the prior cast (`globalThis as { EventSource?: ... }`)
doesn't have enough type overlap because `typeof globalThis` and the
shorthand interface don't intersect cleanly. Route through `unknown`
to take the cast that TypeScript actually allows. Behaviour
unchanged.
@coderabbitai coderabbitai Bot added the rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. label May 18, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
`openhuman::composio::action_tool::tests::factory_routes_through_direct_when_mode_is_direct`
flaked on the prior CI run (file untouched by this PR; main passed
the same test at 14:28Z 2026-05-18). Empty commit to re-run.
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walkthrough

Solid security fix that closes the unauthenticated SSE subscription hole on /events/webhooks (#1922). The implementation is clean: server-side removes the route from PUBLIC_PATHS, adds a tightly-scoped QUERY_TOKEN_PATHS allowlist with ?token=… fallback for browser EventSource (which can't set headers), and the FE wires the core RPC bearer through a shared buildWebhookEventsUrl helper with proper token-rotation plumbing. Test coverage is thorough — 7 unit tests for the Rust helpers, 5 E2E tests for the endpoint, and 8+ FE tests covering URL building, skip-on-null, reconnection on token rotation, and invalidation-bus fire.

No critical or major issues found. The design keeps a single source of truth for the bearer (no second credential), the QUERY_TOKEN_PATHS allowlist is intentionally narrower than PUBLIC_PATHS, and known limitations (token-in-URL for v1, /events still public, no per-restart rotation yet) are explicitly documented and scoped out.

Change Summary

File Change Description
src/core/auth.rs Security Remove /events/webhooks from PUBLIC_PATHS, add QUERY_TOKEN_PATHS + ?token=… fallback, extract bearer_matches and extract_query_token helpers
app/src/services/coreRpcClient.ts Feature Export getCoreRpcToken, add buildWebhookEventsUrl, add EventTarget-based token invalidation bus
app/src/hooks/useWebhooks.ts Feature Resolve + track core RPC bearer, pass to buildWebhookEventsUrl, reconnect on token rotation/invalidation
app/src/components/.../WebhooksDebugPanel.tsx Feature Wire SSE through buildWebhookEventsUrl, skip when no token
app/src/services/coreProcessControl.ts Fix Call clearCoreRpcTokenCache() after restart_core_process
app/src/utils/tauriCommands/core.ts Fix Same cache-clear after the tauriCommands wrapper
tests/json_rpc_e2e.rs Test 5 new E2E tests (unauth, empty, wrong, valid query, valid header); remove /events/webhooks from public-paths loop
src/core/auth.rs (unit tests) Test 7 new unit tests for bearer_matches + extract_query_token
app/src/hooks/__tests__/useWebhooks.test.ts Test 8 new tests for URL building + SSE auth behavior
app/src/components/.../__tests__/WebhooksDebugPanel.test.tsx Test 3 new tests for debug panel SSE wiring
app/src/services/__tests__/coreProcessControl.test.ts Test Expanded to cover cache-clear ordering after restart
app/src/utils/tauriCommands/core.test.ts Test Expanded to cover restart + cache-clear in tauriCommands wrapper

Clean PR — no critical or major issues. Moving to manual approval queue.

@senamakel senamakel merged commit 272a6b0 into tinyhumansai:main May 19, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: SSE EventSource in useWebhooks.ts has no authentication

3 participants