Skip to content

feat(example-apps): harden WIF login against ambiguous identity resolution#109

Merged
thephez merged 6 commits into
mainfrom
fix/login-via-wif
Jul 21, 2026
Merged

feat(example-apps): harden WIF login against ambiguous identity resolution#109
thephez merged 6 commits into
mainfrom
fix/login-via-wif

Conversation

@thephez

@thephez thephez commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Treats a pasted WIF as a credential, not an identity identifier. When a private key maps to more than one identity, the login flow no longer guesses among them or exposes the candidate list — it requires the user to supply the exact identity ID, fetches it directly, and verifies the key against it.

This hardening lands in both Dashnote and TokenOps, and the shared auth code is now byte-identical across the two apps, enforced by CI.

What changed

Resolver (loginWithPrivateKey.ts)

  • When an explicit identity ID is supplied, resolve via identities.fetch(id) and verify the key against it — never select by result order.
  • Fall back to paginated byNonUniquePublicKeyHash when the unique byPublicKeyHash lookup misses; throw AmbiguousIdentityError as soon as a second match is found. Pagination guards against a non-advancing cursor.
  • Match ECDSA keys by full public key (type 0) or ECDSA HASH160 (type 2), in hex or base64; reject other 20-byte key types (BIP13 script hashes, EDDSA HASH160).

UI

  • Dashnote: eager useWifPreview surfaces ambiguous / identity-mismatch states before submit; prompts for an identity ID when needed.
  • TokenOps: reveals the identity-ID field after an ambiguous submit fails.
  • Both persist the identity ID / DPNS label only when "remember" is enabled (Dashnote).

Shared-code parity

  • loginWithPrivateKey.ts, detectSecretShape.ts, keyManagerFromKey.ts, and the resolver test suite are byte-identical across Dashnote and TokenOps.
  • scripts/check-shared-auth-parity.sh enforces this and is wired into both CI workflows, with cross-app path triggers.

Housekeeping

  • Pin prettier to exact 3.8.4 in both apps for deterministic formatting.
  • Deep-link each app's GitHub footer to its own example-apps/ subdirectory; add the missing token-ops entry to the example-apps README.
  • Add an Example Apps section to the root README.

Testing

  • Dashnote: npm test — 346 passing.
  • TokenOps: npm test — 215 passing.
  • Both apps type-check clean (tsc -b).
  • scripts/check-shared-auth-parity.sh passes.

Notes

  • looksLikeWif in TokenOps' detectSecretShape.ts is unused there — it exists only to keep the file byte-identical with Dashnote for the parity check.
  • The prettier pin exposes pre-existing formatting drift in a few untouched TokenOps files (dash/contract.ts, dash/governance.ts, and untouched regions of dash/types.ts / session/SessionContext.tsx). TokenOps CI does not gate on format:check, so this is non-blocking; a follow-up npm run format would reset the baseline.

Summary by CodeRabbit

  • New Features

    • Improved WIF sign-in to handle keys associated with multiple identities by requesting an Identity ID.
    • Added stricter validation for supported authentication keys and identity matching.
    • Added a new Token Ops example app focused on group and multi-signer governance.
  • Documentation

    • Expanded example-app documentation and clarified WIF login behavior.
    • Updated “View on GitHub” links to point to each specific example app.
  • Bug Fixes

    • Improved handling of ambiguous, invalid, disabled, or mismatched WIF credentials.
  • Tests

    • Added coverage for ambiguous identity selection and authentication parity across example apps.

thephez added 5 commits July 20, 2026 15:35
…solution

Treat a WIF as a credential rather than an identity identifier. When a key resolves to multiple identities, refuse to list or guess among them: fall back to paginated `byNonUniquePublicKeyHash`, and require the user to supply the exact intended identity ID, which is fetched directly and verified.

Match ECDSA keys by full public key or ECDSA HASH160 (hex or base64) while rejecting other HASH160 key types. Persist the identity ID and DPNS label only when the remember checkbox is enabled.
Bring TokenOps to parity with Dashnote's WIF sign-in resolver: paginate `byNonUniquePublicKeyHash`, match only full-ECDSA and ECDSA HASH160 key types, and require an explicit identity ID when a key maps to multiple identities — fetching it directly and verifying rather than selecting by result order.

The resolver, secret-shape detector, key-manager adapter, and their core test suite are now byte-identical across both apps, enforced by scripts/check-shared-auth-parity.sh and wired into both CI workflows.
Pin prettier to an exact 3.8.4 in dashnote and token-ops so formatting is deterministic across contributors and CI.
…-ops

Deep-link each app's GitHub footer to its own example-apps subdirectory instead of the repo root, and add the missing token-ops entry to the example-apps README (with section emoji).
Link the root README to the example-apps directory so the standalone React app projects are discoverable from the top level.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashnote and TokenOps now support explicit identity selection for ambiguous WIF credentials, stricter key matching, shared authentication parity checks, updated tests, CI workflows, and documentation.

Changes

WIF authentication and shared parity

Layer / File(s) Summary
Identity resolution and SDK contracts
example-apps/{dashnote,token-ops}/src/dash/*
WIF resolution supports expected identity IDs, typed key matching, paginated ambiguity detection, and authentication-key validation.
Login flow and identity prompt
example-apps/{dashnote,token-ops}/src/components/*, src/session/*, src/hooks/*, src/lib/*
Login options and preview state carry expected identity IDs, while ambiguous WIFs render an identity prompt and block incomplete submissions.
Authentication validation coverage
example-apps/{dashnote,token-ops}/test/*
Tests cover ambiguity handling, identity forwarding, pagination, encoding restrictions, disabled keys, purpose validation, and signer behavior.
Parity enforcement and documentation
.github/workflows/*, scripts/*, README.md, example-apps/*/README.md, example-apps/*/CLAUDE.md, example-apps/*/package.json, example-apps/*/src/components/AppShell.tsx
Shared authentication parity checks, workflow triggers, app documentation, formatter versions, and example-app footer links were updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.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 accurately summarizes the main change: hardening WIF login against ambiguous identity resolution in the example apps.
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/login-via-wif

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.

example-apps/dashmint-lab/src/components/AppShell.tsx

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

example-apps/dashnote/package.json

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

example-apps/dashnote/src/components/AppShell.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 21 others

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.

@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

Caution

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

⚠️ Outside diff range comments (1)
example-apps/dashnote/src/hooks/useWifPreview.ts (1)

161-163: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include expectedIdentityId in the cache key to prevent stale preview states.

The resolved cache is currently keyed only by wif. When the user types an expectedIdentityId, the hook incorrectly returns the stale resolved.state (e.g., "ambiguous") during the 400ms debounce instead of yielding "checking". This hides the "Checking…" UI indicator and causes visual confusion.

Include expectedIdentityId in the cache state and compare it before returning the cached result.

🐛 Proposed fix

Update the useState definition (around line 75):

   const [resolved, setResolved] = useState<{
     wif: string;
+    expectedIdentityId: string | undefined;
     state: WifPreviewState;
   } | null>(null);

Update the setResolved call (around line 152):

       if (cancelled) return;
-      setResolved({ wif: trimmed, state: next });
+      setResolved({ wif: trimmed, expectedIdentityId, state: next });
     }, DEBOUNCE_MS);

Update the cache validation check:

   if (!gateOk) return IDLE;
-  if (resolved && resolved.wif === trimmed) return resolved.state;
+  if (resolved && resolved.wif === trimmed && resolved.expectedIdentityId === expectedIdentityId) return resolved.state;
   return CHECKING;
🤖 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 `@example-apps/dashnote/src/hooks/useWifPreview.ts` around lines 161 - 163,
Update the resolved cache state in useWifPreview to store expectedIdentityId
alongside wif and state, populate it in the setResolved call, and require both
the trimmed WIF and expectedIdentityId to match before returning resolved.state;
otherwise return CHECKING during debounce.
🧹 Nitpick comments (3)
example-apps/dashnote/src/dash/loginWithPrivateKey.ts (2)

391-399: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New tryDecodeHex duplicates the inline hex-decode loop in tryDecodeKeyData (lines 105-115).

Both implement the identical fixed-width hex-to-bytes loop. Since extractPubKeyBytes was already updated (line 430) to call the new shared tryDecodeHex, consider also refactoring tryDecodeKeyData's hex branch to call it, eliminating the last copy of this logic.

♻️ Proposed fix
 function tryDecodeKeyData(data: string): Uint8Array | null {
   if (typeof data !== "string" || data.length === 0) return null;

   if (/^[0-9a-fA-F]+$/.test(data) && data.length % 2 === 0) {
-    try {
-      const bytes = new Uint8Array(data.length / 2);
-      for (let i = 0; i < bytes.length; i += 1) {
-        bytes[i] = parseInt(data.slice(i * 2, i * 2 + 2), 16);
-      }
-      return bytes;
-    } catch {
-      // fall through to base64
-    }
+    const decoded = tryDecodeHex(data);
+    if (decoded) return decoded;
   }
🤖 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 `@example-apps/dashnote/src/dash/loginWithPrivateKey.ts` around lines 391 -
399, Update the hex branch of tryDecodeKeyData to reuse the shared tryDecodeHex
helper instead of maintaining its inline hex-to-bytes loop. Preserve the
existing invalid-input and decoded-byte behavior while removing the duplicated
parsing logic.

333-340: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

matchIdentityKey duplicates the identityIdOf helper.

Lines 338-339 re-implement the same typeof identity.id === "string" ? identity.id : identity.id.toString() logic that identityIdOf (defined later in this file) already encapsulates. Call identityIdOf(identity) here instead to avoid two independent implementations of identity-ID stringification drifting apart.

♻️ Proposed fix
 function matchIdentityKey(
   identity: IdentityLike,
   ourPubKeyBytes: Uint8Array | null,
   ourPubKeyHashBytes: Uint8Array | null,
 ): MatchedIdentity | null {
-  const identityId =
-    typeof identity.id === "string" ? identity.id : identity.id.toString();
+  const identityId = identityIdOf(identity);
   const publicKeys = identity.toJSON?.().publicKeys ?? [];
🤖 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 `@example-apps/dashnote/src/dash/loginWithPrivateKey.ts` around lines 333 -
340, Update matchIdentityKey to obtain identityId by calling the existing
identityIdOf(identity) helper instead of duplicating the identity.id
stringification logic; leave the surrounding public-key matching behavior
unchanged.
example-apps/dashnote/src/components/LoginModal.tsx (1)

202-213: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Tailwind v4 CSS variable syntax.

With Tailwind CSS v4, you can reference CSS variables directly using parentheses instead of the verbose arbitrary value syntax. For example, text-[color:var(--color-danger)] can be simplified to text-(--color-danger).

♻️ Proposed fix
               {wifPreview.status === "ambiguous" && (
-                <span className="text-[color:var(--color-danger)]">
+                <span className="text-(--color-danger)">
                   This key is associated with multiple identities. Enter the
                   full identity ID you intend to use.
                 </span>
               )}
               {wifPreview.status === "identity-mismatch" && (
-                <span className="text-[color:var(--color-danger)]">
+                <span className="text-(--color-danger)">
                   This key could not be verified as an eligible authentication
                   key for that identity ID.
                 </span>
               )}
🤖 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 `@example-apps/dashnote/src/components/LoginModal.tsx` around lines 202 - 213,
Update the danger text classes on the ambiguous and identity-mismatch status
messages in LoginModal to use Tailwind v4’s parenthesized CSS variable syntax,
replacing the verbose arbitrary color syntax while preserving the existing
styling.
🤖 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 `@example-apps/dashnote/src/components/LoginModal.tsx`:
- Around line 51-58: Update the needsIdentityId logic in LoginModal to latch the
preview’s ambiguousWif state, preserving the Identity ID input while the value
is cleared and the preview transitions through stale or checking states. Use the
latched ambiguousWif state alongside hasIdentityPrompt when determining whether
the input remains mounted.

In `@example-apps/token-ops/src/dash/loginWithPrivateKey.ts`:
- Around line 94-101: Correct the decode documentation above the public-key
data-field helper to state that hex is attempted first and base64 is used as the
fallback, matching the existing branch order. Apply the identical comment-only
change in the corresponding Dashnote shared-auth implementation to preserve
byte-identical parity.

---

Outside diff comments:
In `@example-apps/dashnote/src/hooks/useWifPreview.ts`:
- Around line 161-163: Update the resolved cache state in useWifPreview to store
expectedIdentityId alongside wif and state, populate it in the setResolved call,
and require both the trimmed WIF and expectedIdentityId to match before
returning resolved.state; otherwise return CHECKING during debounce.

---

Nitpick comments:
In `@example-apps/dashnote/src/components/LoginModal.tsx`:
- Around line 202-213: Update the danger text classes on the ambiguous and
identity-mismatch status messages in LoginModal to use Tailwind v4’s
parenthesized CSS variable syntax, replacing the verbose arbitrary color syntax
while preserving the existing styling.

In `@example-apps/dashnote/src/dash/loginWithPrivateKey.ts`:
- Around line 391-399: Update the hex branch of tryDecodeKeyData to reuse the
shared tryDecodeHex helper instead of maintaining its inline hex-to-bytes loop.
Preserve the existing invalid-input and decoded-byte behavior while removing the
duplicated parsing logic.
- Around line 333-340: Update matchIdentityKey to obtain identityId by calling
the existing identityIdOf(identity) helper instead of duplicating the
identity.id stringification logic; leave the surrounding public-key matching
behavior unchanged.
🪄 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: 451f5c08-d3c1-42bf-9a35-b3fa88566563

📥 Commits

Reviewing files that changed from the base of the PR and between 466814f and 9751378.

⛔ Files ignored due to path filters (2)
  • example-apps/dashnote/package-lock.json is excluded by !**/package-lock.json
  • example-apps/token-ops/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • .github/workflows/dashnote-e2e.yml
  • .github/workflows/token-ops-ci.yml
  • README.md
  • example-apps/README.md
  • example-apps/dashmint-lab/src/components/AppShell.tsx
  • example-apps/dashnote/CLAUDE.md
  • example-apps/dashnote/README.md
  • example-apps/dashnote/package.json
  • example-apps/dashnote/src/components/AppShell.tsx
  • example-apps/dashnote/src/components/LoginModal.tsx
  • example-apps/dashnote/src/dash/loginWithPrivateKey.ts
  • example-apps/dashnote/src/dash/types.ts
  • example-apps/dashnote/src/hooks/useWifPreview.ts
  • example-apps/dashnote/src/session/SessionContext.tsx
  • example-apps/dashnote/test/LoginModal.test.tsx
  • example-apps/dashnote/test/SessionContext.test.tsx
  • example-apps/dashnote/test/loginWithPrivateKey.test.ts
  • example-apps/dashnote/test/useWifPreview.test.tsx
  • example-apps/dashproof-lab/src/components/AppShell.tsx
  • example-apps/dashrate/src/App.tsx
  • example-apps/token-ops/CLAUDE.md
  • example-apps/token-ops/README.md
  • example-apps/token-ops/package.json
  • example-apps/token-ops/src/components/LoginModal.tsx
  • example-apps/token-ops/src/dash/loginWithPrivateKey.ts
  • example-apps/token-ops/src/dash/types.ts
  • example-apps/token-ops/src/lib/detectSecretShape.ts
  • example-apps/token-ops/src/session/SessionContext.tsx
  • example-apps/token-ops/src/session/keyManagerFromKey.ts
  • example-apps/token-ops/test/LoginModal.test.tsx
  • example-apps/token-ops/test/SessionContext.test.tsx
  • example-apps/token-ops/test/loginWithPrivateKey.test.ts
  • scripts/check-shared-auth-parity.sh

Comment thread example-apps/dashnote/src/components/LoginModal.tsx
Comment thread example-apps/token-ops/src/dash/loginWithPrivateKey.ts
…tions

The identity-ID prompt was tied to the live preview status, so it flickered away when a follow-up preview cycled through checking/idle. Latch the ambiguous result in an effect so the prompt persists once a key is known to map to multiple identities.

Also correct the tryDecodeKeyData comment in the shared resolver to describe the actual hex-first, base64-fallback order (kept byte-identical across both apps).
@thephez
thephez merged commit 06d3c31 into main Jul 21, 2026
7 checks passed
@thephez
thephez deleted the fix/login-via-wif branch July 21, 2026 13:29
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