Skip to content

fix(jwt): harden provider JWT caching and refresh#3228

Merged
baktun14 merged 19 commits into
mainfrom
fix/jwt-harden-provider-caching
Jun 2, 2026
Merged

fix(jwt): harden provider JWT caching and refresh#3228
baktun14 merged 19 commits into
mainfrom
fix/jwt-harden-provider-caching

Conversation

@baktun14
Copy link
Copy Markdown
Contributor

@baktun14 baktun14 commented May 27, 2026

Why

Fixes CON-371. PR #3217 (CON-268) switched managed deploy-web to a JWT-only provider auth flow with a cache-first ensureToken() (jotai atom + localStorage, 30 min TTL). The cache works on the happy path but several edge cases reduced its value or stranded users:

  • Mount-time race regenerated a perfectly good stored token on every refresh.
  • WS streams (shell / logs / events) snapshot the token once at handshake, so mid-session expiry left a dead connection with no recovery.
  • No proactive refresh or clock-skew tolerance — a stream opened seconds before exp died almost immediately.
  • Auto-gen failures were swallowed silently.
  • Multiple credential-using hook instances raced on mount, each firing its own POST /v1/create-jwt-token.
  • Provider JWT scope was missing send-manifest / get-manifest, so PUT /manifest returned 401 in production paths after CON-268 — caught during smoke and bundled in.
  • sendManifest's internal retry loop snapshotted the token at call time; long flows (lease-not-found backoff) could outlive the cached token.

What

Client (apps/deploy-web):

  • useProviderJwt: hydrated flag + skew-aware isTokenExpired (REFRESH_SKEW_SECONDS = 60), JWT scope extended to include send-manifest / get-manifest.
  • useProviderCredentials: cockatiel retry inside ensureToken, toast on exhaustion via useNotificator, details.error exposed, in-flight tracker promoted to module scope so all hook instances dedup against the same POST. Cleared on address change.
  • provider-proxy.service.ts: WS rotation loop in getLogsStream and a new RotatingShellSession that swap the underlying WebsocketSession on tokenExpired, capped at 3 rotations within 60s. Per-session inner AbortController so cockatiel doesn't retry on intentional rotation disconnects. Emits a synthetic closed: true when the cap is exceeded or auth-gen fails, so the existing "unavailable" panel surfaces. sendManifest calls ensureToken per attempt inside its internal retry loop.
  • DeploymentLeaseShell / DeploymentLogs: sticky hasShellAccess / hasLogsAccess state so the panes survive transient usable flips during refresh; skeleton-loading placeholder while credentials load; warning alert when auth-gen ultimately fails.
  • useLeaseStatus, ManifestUpdate, LeaseRow, CreateLease, useProviderApiActions: switched from snapshot providerCredentials.details to ensureToken flow where the request is auth-bearing.

Server (apps/provider-proxy):

  • WebsocketServer: when the schema-validation failure is specifically "Token has expired", emit { type: "websocket", error: "tokenExpired" } (stable contract) instead of the generic Invalid message format shape. Other schema failures unchanged.

Specs:

  • useProviderJwt.spec.tsx, useProviderCredentials.spec.ts, provider-proxy.service.spec.ts, useLeaseQuery.spec.tsx, ManifestUpdate.spec.tsx updated for the new contract and new behaviors. Added functional case in apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts asserting the structured tokenExpired payload.

Verification

  • npm run lint -- --quiet clean in apps/deploy-web and apps/provider-proxy.
  • npx tsc --noEmit delta is 0 (17 pre-existing ManifestUpdate.spec.tsx errors unrelated to this PR).
  • npm run test:unit green in both apps.
  • Manual smoke run under a shortened 90s TTL (then reverted):
    • AC1: no POST /v1/create-jwt-token on hard refresh while the cached token is in-window.
    • AC2: opening logs/shell while the token is in the 60s skew window triggers exactly one refresh POST.
    • AC3: token expires mid-shell → DevTools shows the structured { error: "tokenExpired" } frame, a new WS handshake fires with a fresh Authorization: Bearer …, the XTerm stays mounted (no remount on refresh).
    • AC3b: /v1/create-jwt-token blocked → after the auth-gen retries exhaust, the synthetic close yields the "Shell access unavailable" alert.
    • AC4: wallet reconnect with /v1/create-jwt-token blocked → 4 attempts (1 + 3 retries, ~3.5s), error toast appears, no infinite retry; skeleton shows during the retry window so the pane isn't blank.

Summary by CodeRabbit

  • New Features

    • UI now shows an explicit auth fallback (error alert or loading skeleton) and gates shell/log actions until provider access is granted.
    • Long-running shell and log sessions automatically rotate credentials to stay connected; queued shell output is preserved across rotations.
  • Bug Fixes

    • More accurate token-expiry detection and clearer "token expired" websocket messaging.
    • Actions (send manifest, downloads, log streaming) obtain fresh tokens on demand with retry behavior and clearer failure notifications.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 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

Centralizes provider auth on an ensureToken callback: JWT hooks add hydration, skew and error tracking with retries; provider-proxy APIs accept ensureToken and implement per-session WebSocket rotation with a RotationTracker; server signals token expiry; UI components gate access via useProviderAccess and render ProviderAuthFallback; call sites and tests updated.

Changes

JWT Token Refresh and WebSocket Rotation

Layer / File(s) Summary
useProviderJwt: hydration, scopes, refresh skew
apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts, useProviderJwt.spec.tsx
Adds isHydrated, exports REFRESH_SKEW_SECONDS (60s), treats near-expiry tokens as expired, and expands lease leases.access scopes to include send-manifest and get-manifest.
useProviderCredentials: retries, dedupe, error state
apps/deploy-web/src/hooks/useProviderCredentials/useProviderCredentials.ts, useProviderCredentials.spec.ts
Wraps token generation in a retry policy with notificator wiring, deduplicates in-flight requests by wallet address, exposes details.error and resets it on address change, and gates auto-refresh on hydration.
Provider-proxy API contract refactor
apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts
Service methods and SendManifestToProviderOptions now require ensureToken: () => Promise<string> instead of static credentials/providerCredentials.
getLogsStream: rotation loop and RotationTracker
apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts, provider-proxy.service.spec.ts (getLogsStream tests)
Authenticates each websocket session with a fresh token from ensureToken(), detects server {type:"websocket", error:"tokenExpired"} messages, rotates sessions subject to RotationTracker limits, and merges abort signals.
connectToShell: RotatingShellSession
apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts, apps/deploy-web/src/services/provider-proxy/RotatingShellSession.ts, provider-proxy.service.spec.ts (connectToShell tests)
connectToShell returns a RotatingShellSession that encodes command args into the URL, authenticates with fresh tokens via ensureToken(), buffers outbound messages across reconnections, rotates on token-expiry, and exposes send, async receive(), and disconnect.
Server token-expiry detection
apps/provider-proxy/src/services/WebsocketServer.ts, apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts
WebsocketServer inspects Zod validation errors and returns { type: "websocket", error: "tokenExpired" } when auth.token reports "Token has expired". Functional test sends an expired JWT and asserts the tokenExpired response.
Component access gating and fallbacks
apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx, DeploymentLogs.tsx, DeploymentLeaseShell.tsx
Adds useProviderAccess(providerCredentials) and ProviderAuthFallback({hasAccess,error}); DeploymentLogs and DeploymentLeaseShell render the fallback when unauthorized and only initialize streams/shell when hasAccess is true.
Call-site updates to pass ensureToken
apps/deploy-web/src/components/deployments/LeaseRow.tsx, ManifestUpdate.tsx, CreateLease/*, useProviderApiActions.tsx, useLeaseQuery.ts, tests...
All call sites now pass ensureToken: providerCredentials.ensureToken (function) instead of pre-resolved credentials. Tests and fixtures updated to include details.error where applicable; provider-proxy tests add rotation and failure scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • akash-network/console#3217: Both PRs refactor deploy-web’s provider authentication to JWT ensureToken patterns; this PR extends that work with ProviderAuthGate UI fallbacks and provider-proxy WebSocket rotation.

Suggested labels

size: XL

Suggested reviewers

  • stalniy
  • devalpatel67
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/jwt-harden-provider-caching

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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

❌ Patch coverage is 81.22271% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.72%. Comparing base (2f9ac22) to head (00af04a).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...-web/src/components/deployments/DeploymentLogs.tsx 0.00% 16 Missing and 3 partials ⚠️
...rc/services/provider-proxy/RotatingShellSession.ts 78.68% 12 Missing and 1 partial ⚠️
...rc/components/deployments/DeploymentLeaseShell.tsx 0.00% 7 Missing and 1 partial ⚠️
...deploy-web/src/components/deployments/LeaseRow.tsx 0.00% 1 Missing ⚠️
...loy-web/src/services/provider-proxy/ws-rotation.ts 95.45% 1 Missing ⚠️
...pps/provider-proxy/src/services/WebsocketServer.ts 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3228      +/-   ##
==========================================
- Coverage   66.93%   65.72%   -1.22%     
==========================================
  Files        1076      996      -80     
  Lines       26329    24403    -1926     
  Branches     6349     5964     -385     
==========================================
- Hits        17623    16038    -1585     
+ Misses       7610     7299     -311     
+ Partials     1096     1066      -30     
Flag Coverage Δ *Carryforward flag
api 84.72% <ø> (ø) Carriedforward from e46e6b2
deploy-web 51.40% <80.46%> (+0.35%) ⬆️
log-collector ?
notifications 91.06% <ø> (ø) Carriedforward from e46e6b2
provider-console 81.38% <ø> (ø) Carriedforward from e46e6b2
provider-inventory ?
provider-proxy 86.37% <92.85%> (+0.28%) ⬆️
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...ents/deployments/ManifestUpdate/ManifestUpdate.tsx 88.04% <ø> (ø)
...eb/src/components/deployments/ProviderAuthGate.tsx 100.00% <100.00%> (ø)
...ponents/new-deployment/CreateLease/CreateLease.tsx 75.00% <ø> (-0.15%) ⬇️
...b/src/hooks/useProviderAccess/useProviderAccess.ts 100.00% <100.00%> (ø)
...pps/deploy-web/src/hooks/useProviderApiActions.tsx 0.00% <ø> (ø)
...s/useProviderCredentials/useProviderCredentials.ts 100.00% <100.00%> (ø)
...loy-web/src/hooks/useProviderJwt/useProviderJwt.ts 95.00% <100.00%> (+0.40%) ⬆️
apps/deploy-web/src/queries/useLeaseQuery.ts 95.55% <100.00%> (+0.10%) ⬆️
.../services/provider-proxy/provider-proxy.service.ts 96.10% <100.00%> (+0.68%) ⬆️
...s/deploy-web/src/store/providerCredentialsStore.ts 100.00% <100.00%> (ø)
... and 6 more

... and 87 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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: 3

🤖 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 `@apps/deploy-web/src/components/deployments/DeploymentLeaseShell.tsx`:
- Around line 40-43: The current hasShellAccess state is sticky so the
auth-failure banner gated by "!hasShellAccess" never reappears once shell access
was granted; change the banner rendering logic to check
providerCredentials.details.error (e.g., render when
Boolean(providerCredentials.details.error)) so the alert shows whenever the
provider credentials report an error even if hasShellAccess is true, and update
the second occurrence (the block referenced at lines 245-253) the same way; keep
the useEffect that sets hasShellAccess from providerCredentials.details.usable
unchanged.

In `@apps/deploy-web/src/components/deployments/DeploymentLogs.tsx`:
- Around line 60-63: The component DeploymentLogs sets hasLogsAccess to true but
never resets it, so auth-failure alerts are hidden after an initial success;
update the useEffect that currently reads providerCredentials.details.usable to
setHasLogsAccess(providerCredentials.details.usable) (i.e., assign the boolean
value rather than only setting true), or remove the derived state and read
providerCredentials.details.usable directly where the alert is rendered (also
apply the same fix for the similar block around the code handling lines 241-249)
so the UI reflects current token usability changes instead of sticking true
forever.

In `@apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts`:
- Around line 240-285: The generator currently exits silently when
input.ensureToken() rejects or when rotation is disallowed
(tracker.allowRotation() is false), leaving consumers without a terminal `{
closed: true }` message; update the loop in provider-proxy.service.ts so that if
await input.ensureToken() throws you catch the error, yield a terminal `{
closed: true }` message (or an equivalent LogStreamMessage with closed: true),
then rethrow or return to stop the generator, and likewise before any early
return where `!rotate || !tracker.allowRotation()` occurs yield the `{ closed:
true }` terminal message; use the existing symbols (input.ensureToken,
rotationAbort, session.disconnect, tracker.allowRotation,
WebsocketSession.receive) to locate the places to add the catch-and-yield and
the pre-return yield so consumers always get a final closed event.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 23894ffc-98c2-41f6-bd0c-ac310c806aee

📥 Commits

Reviewing files that changed from the base of the PR and between 93f96f9 and 07264c0.

📒 Files selected for processing (17)
  • apps/deploy-web/src/components/deployments/DeploymentLeaseShell.tsx
  • apps/deploy-web/src/components/deployments/DeploymentLogs.tsx
  • apps/deploy-web/src/components/deployments/LeaseRow.tsx
  • apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.spec.tsx
  • apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.tsx
  • apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx
  • apps/deploy-web/src/hooks/useProviderApiActions.tsx
  • apps/deploy-web/src/hooks/useProviderCredentials/useProviderCredentials.spec.ts
  • apps/deploy-web/src/hooks/useProviderCredentials/useProviderCredentials.ts
  • apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.spec.tsx
  • apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts
  • apps/deploy-web/src/queries/useLeaseQuery.spec.tsx
  • apps/deploy-web/src/queries/useLeaseQuery.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.spec.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts
  • apps/provider-proxy/src/services/WebsocketServer.ts
  • apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts

Comment thread apps/deploy-web/src/components/deployments/DeploymentLeaseShell.tsx Outdated
Comment thread apps/deploy-web/src/components/deployments/DeploymentLogs.tsx Outdated
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts (1)

50-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh the JWT inside the manifest retry loop.

ensureToken() is resolved once before the loop, so attempts 2/3 reuse the same JWT after the retry backoff. If that token expires between attempts, this path keeps resending stale auth instead of actually refreshing.

Suggested fix
   const serializedManifest = manifestToSortedJSON(manifest);
-  const credentials: ProviderCredentials = { type: "jwt", value: await options.ensureToken() };
   let response: AxiosResponse | undefined;
 
   for (let i = 1; i <= 3 && !response; i++) {
     this.logger.info({ event: "ATTEMPT_SEND_MANIFEST", attempt: i, providerAddress: providerInfo.owner, dseq: options.dseq });
     try {
       if (!response) {
+        const credentials: ProviderCredentials = { type: "jwt", value: await options.ensureToken() };
         response = await this.request(`/deployment/${options.dseq}/manifest`, {
           method: "PUT",
           credentials,
           body: serializedManifest,
           timeout: 60_000,
🤖 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 `@apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts` around
lines 50 - 61, The current code captures the JWT once (via
options.ensureToken()) before the retry loop so subsequent attempts reuse a
potentially-expired token; move the await options.ensureToken() call into the
retry loop and build a fresh ProviderCredentials object (type "jwt", value:
await options.ensureToken()) immediately before calling
this.request(`/deployment/${options.dseq}/manifest`, ...) so each attempt uses a
newly-fetched token; update references around manifestToSortedJSON,
ProviderCredentials, response and the for-loop to ensure credentials is
re-created per iteration.
🤖 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 `@apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx`:
- Around line 17-21: The hook currently short-circuits to return fallback: null
whenever hasAccess is true, which hides later errors in
providerCredentials.details.error; update the ProviderAuthGate hook so it always
supplies a fallback component when providerCredentials.details.error exists
(even if hasAccess is true) — keep the hasAccess boolean for gating behavior but
set fallback to <ProviderAuthFallback error={providerCredentials.details.error}
/> when providerCredentials.details.error is truthy; locate the hasAccess return
branch in ProviderAuthGate and change the fallback logic to prefer the
error-based ProviderAuthFallback over null, ensuring ProviderAuthFallback,
hasAccess, and providerCredentials.details.error are used as the deciding
symbols.

---

Outside diff comments:
In `@apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts`:
- Around line 50-61: The current code captures the JWT once (via
options.ensureToken()) before the retry loop so subsequent attempts reuse a
potentially-expired token; move the await options.ensureToken() call into the
retry loop and build a fresh ProviderCredentials object (type "jwt", value:
await options.ensureToken()) immediately before calling
this.request(`/deployment/${options.dseq}/manifest`, ...) so each attempt uses a
newly-fetched token; update references around manifestToSortedJSON,
ProviderCredentials, response and the for-loop to ensure credentials is
re-created per iteration.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4393bd0f-9248-485c-9875-140eb8936af9

📥 Commits

Reviewing files that changed from the base of the PR and between 07264c0 and 0907033.

📒 Files selected for processing (9)
  • apps/deploy-web/src/components/deployments/DeploymentLeaseShell.tsx
  • apps/deploy-web/src/components/deployments/DeploymentLogs.tsx
  • apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx
  • apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx
  • apps/deploy-web/src/hooks/useProviderCredentials/useProviderCredentials.ts
  • apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.spec.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts
  • apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts
💤 Files with no reviewable changes (1)
  • apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts
  • apps/deploy-web/src/hooks/useProviderCredentials/useProviderCredentials.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.spec.ts

Comment thread apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx Outdated
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

🤖 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 `@apps/deploy-web/src/services/provider-proxy/RotatingShellSession.ts`:
- Around line 37-39: openNextSession currently calls this.options.ensureToken()
before checking teardown state, which can trigger token work even after
cancellation/disconnect; change the logic in openNextSession to first check
this.isDisconnected and this.options.signal?.aborted and return early if set,
then call this.options.ensureToken(), so token generation is skipped for
torn-down sessions (refer to method openNextSession and the ensureToken call and
guards isDisconnected/options.signal?.aborted).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 94ebbd20-e825-4740-830b-6094201c9fde

📥 Commits

Reviewing files that changed from the base of the PR and between 0907033 and 2ba9386.

📒 Files selected for processing (5)
  • apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx
  • apps/deploy-web/src/services/provider-proxy/RotatingShellSession.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.spec.ts
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts
  • apps/deploy-web/src/services/provider-proxy/ws-rotation.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx
  • apps/deploy-web/src/services/provider-proxy/provider-proxy.service.ts

Comment thread apps/deploy-web/src/services/provider-proxy/RotatingShellSession.ts
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: 2

🤖 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 `@apps/deploy-web/src/hooks/useProviderAccess/useProviderAccess.spec.ts`:
- Around line 40-46: The test helper buildCredentials currently creates a nested
mock by passing details: mock<UseProviderCredentialsResult["details"]>({...})
which yields proxy-shaped nested objects; instead, instantiate a root mock of
UseProviderCredentialsResult (via mock<UseProviderCredentialsResult>()) and then
assign its details property to a plain object with usable and error fields (not
a mock) so assertions inspect a normal POJO; update buildCredentials to return
that root mock with details replaced by the plain object.

In `@apps/deploy-web/src/hooks/useProviderAccess/useProviderAccess.ts`:
- Around line 6-10: The hook initializes hasAccess to false causing a wrong
first render; change the useState initialization in useProviderAccess (the
hasAccess/setHasAccess state) to seed from the current
providerCredentials.details.usable value instead of always false so initial
render reflects current credentials; keep the existing useEffect that updates
hasAccess on providerCredentials.details.usable changes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39867adb-8061-4270-9b61-f7193b0f299a

📥 Commits

Reviewing files that changed from the base of the PR and between ca1a021 and 7cf216f.

📒 Files selected for processing (6)
  • apps/deploy-web/src/components/deployments/DeploymentLeaseShell.tsx
  • apps/deploy-web/src/components/deployments/DeploymentLogs.tsx
  • apps/deploy-web/src/components/deployments/ProviderAuthGate.spec.tsx
  • apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx
  • apps/deploy-web/src/hooks/useProviderAccess/useProviderAccess.spec.ts
  • apps/deploy-web/src/hooks/useProviderAccess/useProviderAccess.ts
💤 Files with no reviewable changes (1)
  • apps/deploy-web/src/components/deployments/ProviderAuthGate.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/deploy-web/src/components/deployments/DeploymentLogs.tsx
  • apps/deploy-web/src/components/deployments/DeploymentLeaseShell.tsx

Comment thread apps/deploy-web/src/hooks/useProviderAccess/useProviderAccess.spec.ts Outdated
Comment thread apps/deploy-web/src/hooks/useProviderAccess/useProviderAccess.ts Outdated
@baktun14 baktun14 requested review from stalniy and ygrishajev May 29, 2026 16:06
baktun14 added 8 commits June 2, 2026 14:21
- Hydrate JWT atom from storage before auto-gen fires, so a hard refresh
  with a valid cached token no longer triggers a fresh POST.
- Make `isTokenExpired` skew-aware so we refresh 60s before exp.
- Wrap `ensureToken` in cockatiel retry, surface a toast via notificator
  and populate `details.error` once the retries are exhausted.
- Dedup in-flight token generation at module scope so multiple hook
  instances share one POST instead of racing on mount.
- Emit a structured `{ error: "tokenExpired" }` payload from the proxy's
  schema-validation path so the client matches a stable contract.
- Add a WS rotation loop in `provider-proxy.service` that swaps the
  underlying session on `tokenExpired`, capped at 3 rotations in 60s,
  emitting a synthetic close when the cap or auth-gen fails so the UI
  surfaces the existing "unavailable" panel.
- Render a skeleton while credentials load and a warning alert when
  auth-gen ultimately fails, so the shell/logs panes no longer stay
  blank for ~3.5s.
- Refresh the token per-attempt inside `sendManifest`'s internal retry
  loop and inside `useLeaseStatus`, since a long-running flow would
  otherwise stale-token-401 the provider.
- Extend the managed JWT scope to include `send-manifest` /
  `get-manifest` (previously missing, blocking PUT /manifest with 401).
- Extract useProviderAuthGate hook to dedupe sticky-access state and
  skeleton/Alert fallback shared by DeploymentLeaseShell and DeploymentLogs
- Drop dead credentials? field from SendManifestToProviderOptions and
  hoist ensureToken() out of the retry loop
- Cap RotatingShellSession outbound queue, attach .catch() to sessionReady
  to suppress unhandled rejections
- Always disconnect WebsocketSession in finally so consumer break-out does
  not leak the underlying socket
- Remove redundant setIsHydrated(false) toggle and stale narrating comment
- Fix CreateLease.spec.tsx assertions that were left referencing the old
  credentials shape
- Use Buffer "base64url" encoding in provider-proxy-ws test helper
Extract RotatingShellSession and its supporting ws-rotation primitives
(RotationTracker, mergeSignals, isTokenExpiredMessage) into sibling
files. provider-proxy.service.ts re-exports ReceivedShellMessage so
existing consumer import paths are unchanged.
…xhaustion

- ProviderAuthGate now renders the warning alert whenever the credentials
  error is set, even after hasAccess has become true, so token-refresh
  exhaustion mid-session is visible.
- getLogsStream yields { closed: true } when ensureToken rejects or when
  the rotation cap is hit, so consumers see a terminal state instead of
  silently stalling.
Replace useProviderAuthGate (hook returning JSX) with useProviderAccess
(sticky-access boolean) and ProviderAuthFallback (presentational
component). Cleaner separation between state and rendering.
Guard openNextSession against running token generation work after the
session was disconnected or its abort signal fired before the call.
Extracts the sticky provider-access hook out of ProviderAuthGate.tsx into
hooks/useProviderAccess and covers it with renderHook tests. Adds tests
for ProviderAuthFallback covering the no-fallback, skeleton, error and
sticky-then-error branches.
Initialize the sticky access state from providerCredentials.details.usable
so the first render reports the correct value when credentials are
already usable. Update the spec helper to build credentials.details as a
plain object instead of a nested mock proxy.
@baktun14 baktun14 force-pushed the fix/jwt-harden-provider-caching branch from dcf3dbd to 13b9c68 Compare June 2, 2026 18:21
baktun14 added 5 commits June 2, 2026 14:40
Make ensureToken a ref-backed stable callback so long-lived consumers
(RotatingShellSession, getLogsStream) don't capture stale token state and
DeploymentLeaseShell can include it in deps without the shell session
tearing down on JWT rotation.
Replaces the module-level mutable inFlightTracker with a jotai atom read
via getDefaultStore(), matching the JWT_TOKEN_ATOM pattern in
useProviderJwt. Same dedup semantics — concurrent ensureToken calls from
multiple hook instances share one in-flight generateToken promise.
Follow the src/store/*Store.ts pattern (camelCase atoms grouped in a
default-exported namespace, e.g. walletStore, sdlStore) instead of
declaring the atom inline in the hook.
…atom

Match the convention used by walletStore/sdlStore consumers.
The hook now uses useSetAtom on dedicated write atoms (claim/release/
clearForOtherAddress) that perform atomic check-and-set against the
in-flight atom via jotai's get/set callbacks — no more raw imperative
store access from inside the hook.
@github-actions github-actions Bot added size: XL and removed size: L labels Jun 2, 2026
baktun14 added 3 commits June 2, 2026 15:22
Exercise claim/release/clear-for-other-address via a fresh jotai store
per test, asserting state transitions and dedup semantics directly.
The CA-authorized sync branch in createProviderSocket emitted "verified"
inside the "upgrade" handler, while the ws was still in CONNECTING. Any
once("verified", ...) listener that called ws.send then threw
"WebSocket is not open: readyState 0 (CONNECTING)". The new
RotatingShellSession + WS-rotation flow on this branch opens provider
sockets more often and made the race observable. The async cert-
validation branch already checks readyState before emitting; the sync
branch now goes through the same guard via emitVerifiedWhenOpen.
useProviderList refetches on window focus by default, returning a new
providers array with new object identities. The shellSession useMemo
was depending on the whole providerInfo/selectedLease objects, so the
benign refetch tore down and rebuilt the shell session and re-sent the
init Uint8Array, reprinting the "/app $" prompt. Now depending on the
primitive fields actually consumed by connectToShell.
baktun14 added 3 commits June 2, 2026 17:33
DeploymentLogs had two sources of identity churn that restarted the
log stream on every tab refocus:
- selectedLease (object) in the stream useEffect deps
- selectedServices replaced with a new array on every leaseStatus
  refetch, even when the keys were identical

Switching the deps to primitive ids (dseq/gseq/oseq, providerHostUri,
providerAddress) and short-circuiting setSelectedServices when the
new key list deep-equals the previous one keeps the stream attached.
Replace useState+useEffect sticky latch in useProviderAccess with a
render-time useRef mutation, avoiding the extra render on the
unusable→usable flip.

In RotationTracker.allowRotation, check the cap before pushing so
refused rotations don't leave a stale timestamp in the window buffer.
Cover emitVerifiedWhenOpen (both OPEN and CONNECTING branches) and
isJwtExpiredError (match + non-match paths) directly, lifting codecov
patch coverage above the 80% threshold.
@baktun14 baktun14 merged commit 6fca979 into main Jun 2, 2026
57 checks passed
@baktun14 baktun14 deleted the fix/jwt-harden-provider-caching branch June 2, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant