Skip to content

fix(deploy): stop failing deploys on a probe an API key cannot pass - #3527

Merged
kwakayama merged 4 commits into
mainfrom
fix/deploy-protected-env-api-key
Aug 10, 2026
Merged

fix(deploy): stop failing deploys on a probe an API key cannot pass#3527
kwakayama merged 4 commits into
mainfrom
fix/deploy-protected-env-api-key

Conversation

@kwakayama

@kwakayama kwakayama commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Deploying to a protected environment from an API-key-authenticated CLI always failed on the last step — reported as unknown-error, after the deployment had already been committed and verified.

$ npx veryfront@latest whoami
  ✓ Authenticated with an API key

$ npx veryfront@latest deploy
✗ [unknown-error] Unknown/unclassified error
  Detail: Could not authenticate the protected environment URL https://…production.veryfront.com/. Run veryfront login and deploy again.
  Suggestion: Check logs for more details

The suggested remedy is a dead end: veryfront login is what produced the API key.

Root cause

The readiness probe sent the stored credential as an authToken cookie (deploy-project.ts:908). The gate reading that cookie resolves it by decoding a JWT and reading userId from the verified payload (src/proxy/proxy-access-control.ts:104-166) — RS256 or HS256 only, no API-key branch. An opaque vf_… key was rejected exactly like no credential at all.

Verified against production — all three return the same sign-in redirect:

302  # no auth
302  # Cookie: authToken=<api key>   ← what the CLI sent
302  # Authorization: Bearer <api key>

So the probe could never succeed, and sending the key leaked it for no benefit.

The probe is also the last step. create-deployment (line 1193) and verify-deployment (line 1203) both complete before it runs — the deploy had already landed when the CLI reported failure.

Separately, every throw in the readiness loop was a bare Error, so the boundary wrapped these fully-diagnosed conditions in the catch-all unknown-error slug with "Check logs for more details".

Fix

1. Don't present a credential the gate cannot accept. isSessionCredential decodes the header and payload as base64url JSON and requires a string userId in the payload — the same value the gate reads before testing project membership. Anything else probes unauthenticated.

The check is deliberately fail-closed: a credential the CLI cannot positively recognise is withheld, never sent. Counting dot-separated segments would not have been enough, because an opaque a.b.c credential would still have been presented — and presenting it is the leak this check exists to prevent.

2. Accept the gate's challenge as ready when there is no session. A challenge — sign-in redirect, 401, or 403 — still proves routing resolves and the proxy is serving the environment, which is all this step can establish without a session. This reuses the existing acceptAuthenticationChallenge path already used for protected custom domains; no new semantics.

Accepting the whole challenge rather than only the redirect is the same allowance that path already makes. Narrowing it would buy nothing here either: the gate answers before the app does, so for a protected environment with no session this probe is blind to application health whichever challenge it accepts.

3. Classify the errors. The throws in the readiness loop now carry DEPLOYMENT_ERROR instead of a bare Error, so the slug, suggestion, and docs URL match the actual condition.

4. Name the challenge that actually arrived. Every challenge was reported as "redirected to sign-in", including a 401 or 403. A public environment answering 403 sent the operator looking for a redirect that never happened. The message now names the status when there was no redirect, and the error context carries it.

A JWT session that is genuinely rejected still fails the deploy, with the same message — that path is unchanged, and there "Run veryfront login" is correct advice.

Verification

End-to-end, against the reported failure — same URL, same API key, calling the real waitForEnvironmentReady:

before:  RESULT: probe failed — Could not authenticate the protected environment URL …
after:   RESULT: probe passed — deploy would complete

Checked against real credentials: the session JWT is still sent, the API key is still withheld.

Tests — 6 added. Each was confirmed to fail on the unfixed code and pass with the fix:

  • does not send an API key to the protected environment gate
  • does not send an opaque credential that merely contains dots
  • does not send a JWT-shaped credential whose payload carries no userId
  • treats a sign-in redirect as ready when the credential is an API key
  • classifies a rejected session credential as a deployment error
  • names the status when a challenge was not a sign-in redirect

The first of these also holds the invariant against isApiKeyToken in cli/auth/login.ts: it goes red the moment an API key becomes presentable to the gate again.

The existing fixture token test-token was not JWT-shaped, so it would have silently taken the new unauthenticated path and stopped covering the authenticated one. It is now a JWT-shaped sessionToken, keeping those assertions meaningful.

cli/shared/deployment/ — 12 files, 87 steps, all passing. deno check, deno lint, deno fmt --check clean.

Scope

This does not make API keys work against protected environments — whether an API key should grant browser access to a protected environment is a product decision with real security implications, and belongs in the proxy, not the CLI. This PR stops a probe from failing a deploy that succeeded.

It also does not change the broader shape of the last step: a readiness failure still fails a deploy that has already been committed and verified. That is fixed here only for the case that could never have succeeded. Making readiness a warning rather than a failure affects every credential type and every failure mode, so it belongs in its own change.

Closes veryfront/veryfront-issue-inbox#444

Summary by CodeRabbit

  • Bug Fixes
    • Improved deployment environment readiness checks for session-based and API-key authentication.
    • Prevented API keys and invalid session credentials from being sent to protected environment gates.
    • Accepted sign-in redirects and authentication challenges as valid readiness responses for API-key probes.
    • Added clearer deployment errors for rejected credentials, HTTP status failures, unavailable environments, and timeouts.
    • Improved session credential validation so only authenticated sessions are used during deployment checks.

Deploying to a protected environment from an API-key-authenticated CLI
always failed on the last step, reported as `unknown-error`, after the
deployment had already been committed and verified.

The readiness probe sent the stored credential as an `authToken` cookie.
The gate reading that cookie resolves it by decoding a JWT and reading
`userId` from the verified payload (src/proxy/proxy-access-control.ts);
it has no API-key branch. An opaque `vf_` key was therefore rejected
exactly like no credential at all, so the probe could never succeed and
sending the key leaked it for no benefit.

The probe now checks whether the credential is JWT-shaped. When it is
not, it probes unauthenticated and accepts the sign-in redirect as ready.
That redirect still proves routing resolves and the proxy is serving the
environment, which is all this step can establish without a session --
and `create-deployment` and `verify-deployment` have both already
completed by the time it runs.

The throws in the readiness loop were bare `Error`s, so the boundary
wrapped every diagnosed condition in the catch-all `unknown-error` slug
with "Check logs for more details". They now carry DEPLOYMENT_ERROR.

Verified against the reported failure: the same URL and API key that
produced `unknown-error` now pass the probe, and fail identically on the
unfixed code.

Closes veryfront/veryfront-issue-inbox#444
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a16c73e-8a19-4277-a8b6-37020550dc89

📥 Commits

Reviewing files that changed from the base of the PR and between cafd912 and 0f41533.

📒 Files selected for processing (2)
  • cli/shared/deployment/deploy-project.test.ts
  • cli/shared/deployment/deploy-project.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • cli/shared/deployment/deploy-project.ts
  • cli/shared/deployment/deploy-project.test.ts

📝 Walkthrough

Walkthrough

Environment readiness probes now distinguish JWT-shaped session credentials from API keys. API keys are withheld from protected gates, authentication challenges can indicate readiness without a session, and failures use structured DEPLOYMENT_ERROR details. Tests cover these paths and updated cookies.

Changes

Deployment readiness probing

Layer / File(s) Summary
Credential-aware readiness probes
cli/shared/deployment/deploy-project.ts, cli/shared/deployment/deploy-project.test.ts
Probes authenticate only with JWT-shaped session credentials. API keys, opaque dotted credentials, and JWTs without userId are withheld from protected gates. Sign-in redirects and authentication challenges count as ready when no usable session credential is available. Tests update credential and cookie expectations.
Structured readiness failures
cli/shared/deployment/deploy-project.ts, cli/shared/deployment/deploy-project.test.ts
Authentication, HTTP-status, and timeout failures now use DEPLOYMENT_ERROR instances with URL, status, and timeout context. Tests cover rejected session credentials and non-redirect HTTP 403 responses.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: kojiwakayama

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: preventing deployments from failing when readiness probes cannot authenticate with an API key.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deploy-protected-env-api-key

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@cli/shared/deployment/deploy-project.ts`:
- Around line 962-985: Remove probe.url from all user-facing
DEPLOYMENT_ERROR.detail messages in the environment readiness and
response-status branches, replacing it with generic error text. Preserve only
approved redacted diagnostics in the non-user-facing context path, and add
coverage verifying a custom hostname never appears in detail.
- Around line 842-845: Update isSessionCredential to decode the first two
base64url segments, parse the payload as JSON, and return true only when it
contains a userId; otherwise reject the credential, including opaque dotted
values. Add or update a focused test verifying authenticate withholds a dotted
opaque credential from the authToken cookie.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f59d86ec-20ef-4eab-8bcb-eb87bf14340b

📥 Commits

Reviewing files that changed from the base of the PR and between a63ffc0 and 2b64437.

📒 Files selected for processing (2)
  • cli/shared/deployment/deploy-project.test.ts
  • cli/shared/deployment/deploy-project.ts

Comment thread cli/shared/deployment/deploy-project.ts
Comment thread cli/shared/deployment/deploy-project.ts
…ntial

Counting three non-empty segments let an opaque credential containing dots
through, so `opaque.segment.value` would still be sent in the authToken
cookie.

The check now decodes the header and payload as base64url JSON and requires
a string `userId` in the payload -- which is exactly what the gate reads
before testing project membership. Anything that cannot resolve to a member
is withheld rather than presented and rejected.

Verified against real credentials: the Google session JWT is still sent, and
the API key is still withheld.
Every authentication challenge was reported as "redirected to sign-in", but
a challenge is also a 401 or a 403. A public environment answering 403 sent
the operator looking for a redirect that never happened.

The message now names the status when there was no redirect, and the error
context carries it. Also records why the probe accepts the whole challenge
rather than only the redirect: it is the allowance the protected custom-domain
probe already makes, and narrowing it would buy nothing, because the gate
answers before the app does.
@kwakayama

Copy link
Copy Markdown
Contributor Author

Two follow-up commits since the review, both from issues raised on this PR.

321f27b — tightens the credential check. Counting three non-empty segments let opaque.segment.value through and it would still have been sent in the authToken cookie. The check now decodes the header and payload and requires a string userId, which is what the gate reads. Two tests, both failing on the previous check.

cafd912 — the error message named the wrong thing. Every authentication challenge was reported as "redirected to sign-in", but a challenge is also a 401 or a 403, and a public environment answering 403 sent the operator looking for a redirect that never happened. The message now names the status when there was no redirect, and the context carries it. One test.

That commit also records why the probe accepts the whole challenge and not only the redirect, which the description had understated. It is the allowance the protected custom-domain probe already makes, and narrowing it would buy nothing: the gate answers before the app does, so with no session this probe is blind to application health whichever challenge it accepts.

Left alone on purpose: a readiness failure still fails a deploy that has already been committed and verified. Fixed here only for the case that could never have succeeded. Turning readiness into a warning touches every credential type and every failure mode, so it belongs in its own change.

Description updated to match. cli/shared/deployment/ — 12 files, 87 steps, green.

@kwakayama
kwakayama enabled auto-merge August 10, 2026 11:39
@kwakayama
kwakayama added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit d68fe07 Aug 10, 2026
31 checks passed
@kwakayama
kwakayama deleted the fix/deploy-protected-env-api-key branch August 10, 2026 11:56
@kwakayama kwakayama mentioned this pull request Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant