Skip to content

fix: capture CAP LS values so WebAuthn 2FA button enables on supported servers#204

Merged
ValwareIRC merged 1 commit into
mainfrom
fix/cap-values-webauthn
May 12, 2026
Merged

fix: capture CAP LS values so WebAuthn 2FA button enables on supported servers#204
ValwareIRC merged 1 commit into
mainfrom
fix/cap-values-webauthn

Conversation

@ValwareIRC
Copy link
Copy Markdown
Contributor

@ValwareIRC ValwareIRC commented May 12, 2026

Summary

The "Add biometric / security key" button in TwoFactorSettingsModal was always grayed out, even on obbyircd which advertises `draft/account-2fa=totp,webauthn,oauth`. Tooltip read "Server does not advertise WebAuthn support".

Root cause: the button gated on grepping `"webauthn"` inside the `server.capabilities[]` entries, but that array is populated from CAP ACK (store/handlers/auth.ts:557) — which only echoes cap names. The `=value` suffix lives only in CAP LS (`:server CAP * LS :sasl draft/account-2fa=totp,webauthn,oauth ...`), and the LS handler ignored every `=value` except `unrealircd.org/link-security`.

Fix

  • Add `Server.capabilityValues: Record<string, string>` to carry the CAP LS values.
  • Parse every `name=value` pair out of CAP LS in the existing handler and merge it into `capabilityValues`.
  • Modal now reads `server.capabilityValues["draft/account-2fa"]`, splits on commas, and checks for `"webauthn"`. If the cap is acked but no value was advertised (some servers omit the factor list), assume the standard set rather than silently disabling.

Test plan

  • On a server advertising `draft/account-2fa=totp,webauthn,oauth`, the "Add biometric / security key" button is enabled.
  • On a server advertising `draft/account-2fa` with no factor list, the button stays enabled (assumed standard set).
  • On a server that does NOT advertise `draft/account-2fa` at all, the button is disabled with tooltip "Server does not advertise WebAuthn support".
  • On a browser without `navigator.credentials` (insecure-context or non-WebAuthn build), the button is disabled with the browser tooltip.

Summary by CodeRabbit

  • Refactor
    • Improved server capability detection and storage for more accurate feature availability assessment.

Review Change Stack

…d servers

The "Add biometric / security key" button was always grayed out, even
on obbyircd which advertises draft/account-2fa=totp,webauthn,oauth.
Root cause: the button gated on grepping "webauthn" inside the
server.capabilities[] entries, but that array is populated from
CAP ACK -- which only echoes cap NAMES. The =value suffix lives in
CAP LS, which the handler ignored for everything except
unrealircd.org/link-security.

- Add Server.capabilityValues: Record<string, string> to carry the
  CAP LS values.
- Parse every name=value pair out of CAP LS in the existing handler
  and merge it into capabilityValues.
- Modal now reads server.capabilityValues["draft/account-2fa"],
  splits on commas, and checks for "webauthn". If the cap is acked
  but no value was advertised (some servers omit the factor list),
  assume the standard set rather than silently disabling.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

This PR adds infrastructure for parsing and consuming server capability sub-features. A new capabilityValues field stores per-capability metadata from CAP LS messages. The CAP handler now extracts name=value pairs and populates this map. The WebAuthn modal updates its detection logic to read the parsed draft/account-2fa values instead of searching raw capability strings.

Changes

Capability Values for Feature Detection

Layer / File(s) Summary
Capability values schema
src/types/index.ts
Server interface extends with optional capabilityValues?: Record<string, string> to store per-capability value strings from CAP LS/ACK for UI feature detection.
CAP LS parsing and storage
src/store/handlers/auth.ts
CAP LS handler iterates over capability tokens, extracts name=value pairs (excluding unrealircd.org/link-security), and merges them into the server's capabilityValues map.
WebAuthn detection via capability values
src/components/ui/TwoFactorSettingsModal.tsx
supportsWebAuthn logic refactored to check the draft/account-2fa capability's parsed values, with fallback to true when the capability is advertised but lacks explicit values.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A curious rabbit hops through the IRC streams,
Parsing values from capabilities and dreams,
WebAuthn now reads the server's song,
Sub-features detected all day long! 🔐✨

🚥 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 reflects the main change: capturing CAP LS values to enable WebAuthn 2FA button on supported servers, which directly corresponds to the modifications across all three files.
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.

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

✨ 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/cap-values-webauthn

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown

Pages Preview
Preview URL: https://fix-cap-values-webauthn.obsidianirc.pages.dev

Automated deployment preview for the PR in the Cloudflare Pages.

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 `@src/store/handlers/auth.ts`:
- Around line 499-522: The loop that parses cliCaps currently ignores tokens
without "=", so previously cached capability values are never cleared; update
the parsing in auth.ts (the for loop over cliCaps and the advertised map) to
treat tokens like "draft/account-2fa" as an explicit removal by adding an entry
for that name (e.g., advertised[name] = null) when eq <= 0, and keep the
subsequent store.setState merge logic so the null value overwrites the old value
in server.capabilityValues (ensuring stale values are cleared); reference tokens
parsing, advertised, and the store.setState/server.capabilityValues merge to
locate where to apply this change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ae751b1-c4c9-4e36-8c3a-9695d71e6412

📥 Commits

Reviewing files that changed from the base of the PR and between b5796c4 and 22efbb3.

📒 Files selected for processing (3)
  • src/components/ui/TwoFactorSettingsModal.tsx
  • src/store/handlers/auth.ts
  • src/types/index.ts

Comment on lines +499 to +522
for (const tok of cliCaps.split(/\s+/)) {
if (!tok) continue;
const eq = tok.indexOf("=");
if (eq <= 0) continue;
const name = tok.slice(0, eq);
const value = tok.slice(eq + 1);
if (name === "unrealircd.org/link-security") continue;
advertised[name] = value;
}
if (Object.keys(advertised).length) {
store.setState((state) => ({
servers: state.servers.map((server) =>
server.id === serverId
? {
...server,
capabilityValues: {
...(server.capabilityValues ?? {}),
...advertised,
},
}
: server,
),
}));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear previously cached cap values when the cap is now value-less.

Line 508 only updates state when name=value tokens exist. If draft/account-2fa was previously cached with a value and later arrives as just draft/account-2fa, stale data can persist and incorrectly affect WebAuthn enablement.

Suggested fix
-    const advertised: Record<string, string> = {};
+    const advertised: Record<string, string> = {};
+    const valueLessCaps = new Set<string>();
     for (const tok of cliCaps.split(/\s+/)) {
       if (!tok) continue;
       const eq = tok.indexOf("=");
-      if (eq <= 0) continue;
+      if (eq <= 0) {
+        if (tok !== "unrealircd.org/link-security") valueLessCaps.add(tok);
+        continue;
+      }
       const name = tok.slice(0, eq);
       const value = tok.slice(eq + 1);
       if (name === "unrealircd.org/link-security") continue;
       advertised[name] = value;
     }
-    if (Object.keys(advertised).length) {
+    if (Object.keys(advertised).length || valueLessCaps.size > 0) {
       store.setState((state) => ({
         servers: state.servers.map((server) =>
           server.id === serverId
             ? {
                 ...server,
-                capabilityValues: {
-                  ...(server.capabilityValues ?? {}),
-                  ...advertised,
-                },
+                capabilityValues: (() => {
+                  const next = { ...(server.capabilityValues ?? {}) };
+                  for (const cap of valueLessCaps) delete next[cap];
+                  return { ...next, ...advertised };
+                })(),
               }
             : server,
         ),
       }));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const tok of cliCaps.split(/\s+/)) {
if (!tok) continue;
const eq = tok.indexOf("=");
if (eq <= 0) continue;
const name = tok.slice(0, eq);
const value = tok.slice(eq + 1);
if (name === "unrealircd.org/link-security") continue;
advertised[name] = value;
}
if (Object.keys(advertised).length) {
store.setState((state) => ({
servers: state.servers.map((server) =>
server.id === serverId
? {
...server,
capabilityValues: {
...(server.capabilityValues ?? {}),
...advertised,
},
}
: server,
),
}));
}
const advertised: Record<string, string> = {};
const valueLessCaps = new Set<string>();
for (const tok of cliCaps.split(/\s+/)) {
if (!tok) continue;
const eq = tok.indexOf("=");
if (eq <= 0) {
if (tok !== "unrealircd.org/link-security") valueLessCaps.add(tok);
continue;
}
const name = tok.slice(0, eq);
const value = tok.slice(eq + 1);
if (name === "unrealircd.org/link-security") continue;
advertised[name] = value;
}
if (Object.keys(advertised).length || valueLessCaps.size > 0) {
store.setState((state) => ({
servers: state.servers.map((server) =>
server.id === serverId
? {
...server,
capabilityValues: (() => {
const next = { ...(server.capabilityValues ?? {}) };
for (const cap of valueLessCaps) delete next[cap];
return { ...next, ...advertised };
})(),
}
: server,
),
}));
}
🤖 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/store/handlers/auth.ts` around lines 499 - 522, The loop that parses
cliCaps currently ignores tokens without "=", so previously cached capability
values are never cleared; update the parsing in auth.ts (the for loop over
cliCaps and the advertised map) to treat tokens like "draft/account-2fa" as an
explicit removal by adding an entry for that name (e.g., advertised[name] =
null) when eq <= 0, and keep the subsequent store.setState merge logic so the
null value overwrites the old value in server.capabilityValues (ensuring stale
values are cleared); reference tokens parsing, advertised, and the
store.setState/server.capabilityValues merge to locate where to apply this
change.

@ValwareIRC ValwareIRC merged commit b5acfcd into main May 12, 2026
4 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.

2 participants