Skip to content

refactor(auth): make step-up-without-Agent-binding unrepresentable (HT-75) - #97

Merged
zaridan merged 1 commit into
mainfrom
fix/ht-75-ceremony-params-type
Jul 20, 2026
Merged

refactor(auth): make step-up-without-Agent-binding unrepresentable (HT-75)#97
zaridan merged 1 commit into
mainfrom
fix/ht-75-ceremony-params-type

Conversation

@zaridan

@zaridan zaridan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes the one item #96 deliberately left open — Codex review finding 4.

The footgun

VerifyAuthenticationCeremonyParams was one shape with an optional requireAgentId, so this type-checked:

{ ceremony: 'step-up', responseJson, challengeToken }   // no requireAgentId

That combination silently disables both halves of the Agent binding added in #96 — the pre-consume check and consumeChallenge's AND agent_id = $3 — reopening the challenge-burn DoS. No caller did it. Nothing but this type stopped one from starting.

The change

A discriminated union: step-up requires requireAgentId; authentication forbids it (requireAgentId?: never), since login challenges are minted with agent_id IS NULL and binding one would match no row, breaking every login.

No caller changes were needed. Both existing call sites already passed the correct shape — that's the evidence this tightens the contract without narrowing real behavior.

Verification

Negative type test — both forbidden combinations are now compile errors:

neg-check.ts(3,7):  error TS2322: '{ ceremony: "step-up"; ... }' is not assignable
neg-check.ts(9,7):  error TS2322: '{ ceremony: "authentication"; ... requireAgentId }' is not assignable

Gates: typecheck, lint, test:coverage all exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation for authentication ceremony configurations.
    • Step-up authentication now requires an agent identifier.
    • Standard authentication explicitly rejects agent identifiers, preventing invalid configurations earlier.

…T-75)

Codex review of PR #96, finding 4 — the one item that PR deliberately left
open.

VerifyAuthenticationCeremonyParams was a single shape with an optional
requireAgentId, so `{ ceremony: 'step-up' }` with no Agent type-checked
fine. That combination silently disables BOTH halves of the binding added
in #96 — the pre-consume check and consumeChallenge's AND agent_id = $3 —
reopening the challenge-burn DoS. No caller does it; nothing but this type
stopped one from starting.

Now a discriminated union: 'step-up' requires requireAgentId, and
'authentication' forbids it via `requireAgentId?: never` (its challenges
are minted with agent_id IS NULL, so binding one would match no row and
break every login).

No caller changes were needed — both existing call sites already passed the
correct shape, which is the evidence that this tightens the contract without
narrowing real behavior.

Verified with a negative type test: both `step-up` without requireAgentId
and `authentication` with it are now compile errors (TS2322). Gates:
typecheck, lint, test:coverage all exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

VerifyAuthenticationCeremonyParams is now a discriminated union that requires requireAgentId for step-up ceremonies and forbids it for authentication ceremonies.

Changes

Authentication ceremony parameter typing

Layer / File(s) Summary
Ceremony parameter contract
src/auth/webauthn-ceremony.ts
VerifyAuthenticationCeremonyParams now ties requireAgentId to the ceremony discriminator, preventing invalid parameter combinations at compile time.

Estimated code review effort: 2 (Simple) | ~5 minutes

Possibly related PRs

🚥 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 accurately summarizes the core change: making invalid step-up authentication parameters unrepresentable at the type level.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ht-75-ceremony-params-type

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: 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 `@src/auth/webauthn-ceremony.ts`:
- Around line 98-104: Enable exactOptionalPropertyTypes in both checked-in
TypeScript configurations, then add a negative type test covering `{ ceremony:
'authentication', requireAgentId: undefined }` against
VerifyAuthenticationCeremonyParams. Keep the authentication branch’s
requireAgentId?: never restriction unchanged and ensure the test verifies
explicit undefined is rejected.
🪄 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 Plus

Run ID: 193f1ae2-5f70-4c86-9ad3-50ecebf75e67

📥 Commits

Reviewing files that changed from the base of the PR and between 46f036b and 75342d1.

📒 Files selected for processing (1)
  • src/auth/webauthn-ceremony.ts

Comment on lines +98 to +104
export type VerifyAuthenticationCeremonyParams = VerifyAuthenticationCeremonyParamsBase &
(
| {
/** Checked against the token's OWN `ceremony` field before anything else runs (spec §7's application-level discriminator check). */
ceremony: 'authentication'
requireAgentId?: never
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'exactOptionalPropertyTypes' --glob 'tsconfig*.json' --glob '*.json' .
rg -n 'requireAgentId' --glob '*test*.ts' --glob '*test*.tsx' .

Repository: Helpthread/helpthread

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tsconfig files ==\n'
git ls-files 'tsconfig*.json' '**/tsconfig*.json'

printf '\n== exactOptionalPropertyTypes occurrences ==\n'
rg -n 'exactOptionalPropertyTypes' --glob 'tsconfig*.json' --glob '**/tsconfig*.json' --glob '*.json' .

printf '\n== webauthn ceremony file map ==\n'
ast-grep outline src/auth/webauthn-ceremony.ts --view expanded || true

printf '\n== requireAgentId test/type-test occurrences ==\n'
rg -n 'requireAgentId' --glob '*test*.ts' --glob '*test*.tsx' --glob '*.ts' --glob '*.tsx' src test tests .

printf '\n== nearby source context ==\n'
sed -n '1,220p' src/auth/webauthn-ceremony.ts

Repository: Helpthread/helpthread

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tsconfig.json ==\n'
cat -n tsconfig.json

printf '\n== web/tsconfig.json ==\n'
cat -n web/tsconfig.json

printf '\n== src/auth/webauthn-ceremony.ts (1-180) ==\n'
sed -n '1,180p' src/auth/webauthn-ceremony.ts

printf '\n== requireAgentId mentions in repo ==\n'
rg -n 'requireAgentId' src web . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**'

Repository: Helpthread/helpthread

Length of output: 14190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== type-test / expect-error occurrences ==\n'
rg -n '`@ts-expect-error`|tsd|expectType|satisfies VerifyAuthenticationCeremonyParams|VerifyAuthenticationCeremonyParams' src tests .

printf '\n== src/auth/webauthn-ceremony.test.ts (160-260) ==\n'
sed -n '160,260p' src/auth/webauthn-ceremony.test.ts

printf '\n== package.json ==\n'
cat -n package.json

Repository: Helpthread/helpthread

Length of output: 6840


Tighten the authentication branch’s requireAgentId restriction at src/auth/webauthn-ceremony.ts:98-104
requireAgentId?: never still accepts { ceremony: 'authentication', requireAgentId: undefined } under the current TypeScript config, since neither tsconfig.json nor web/tsconfig.json enables exactOptionalPropertyTypes. Turn that on for the checked-in configs and add a negative type test for the explicit undefined case.

🤖 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/auth/webauthn-ceremony.ts` around lines 98 - 104, Enable
exactOptionalPropertyTypes in both checked-in TypeScript configurations, then
add a negative type test covering `{ ceremony: 'authentication', requireAgentId:
undefined }` against VerifyAuthenticationCeremonyParams. Keep the authentication
branch’s requireAgentId?: never restriction unchanged and ensure the test
verifies explicit undefined is rejected.

@zaridan

zaridan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Adjudicated: valid observation, skipping the fix.

Verified the property that actually matters — { ceremony: 'step-up', requireAgentId: undefined } is rejected:

error TS2322: Type '{ ceremony: "step-up"; ...; requireAgentId: undefined; }'
  is not assignable to type 'VerifyAuthenticationCeremonyParams'.

undefined isn't assignable to string, so the binding-disabling case this PR exists to prevent is closed regardless of exactOptionalPropertyTypes.

The residual case CodeRabbit flags — { ceremony: 'authentication', requireAgentId: undefined } — is accepted, but it is behaviorally identical to omitting the property: the runtime guard is params.requireAgentId !== undefined, so an explicit undefined takes exactly the same branch as absence. No reachable defect.

Not enabling exactOptionalPropertyTypes: it's a repo-wide compiler-strictness change that would cascade across every optional property in the codebase, well outside this PR's scope, to close a hole with no behavioral consequence. Worth considering on its own merits as a separate change.

@zaridan
zaridan merged commit 2933259 into main Jul 20, 2026
5 checks passed
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