Skip to content

[auth-unkey] Expand fail-closed resolver coverage#41

Open
devcool20 wants to merge 5 commits into
inthhq:mainfrom
devcool20:fix/17-auth-unkey-test-coverage
Open

[auth-unkey] Expand fail-closed resolver coverage#41
devcool20 wants to merge 5 commits into
inthhq:mainfrom
devcool20:fix/17-auth-unkey-test-coverage

Conversation

@devcool20

@devcool20 devcool20 commented May 17, 2026

Copy link
Copy Markdown

Linked Issue

Closes #17

Description

This expands @dsar/auth-unkey coverage around invalid and failing Unkey verification paths.

Before this change, invalid Unkey verification results returned undefined, but a thrown Unkey verification error escaped from makeUnkeyBearerResolver(). That meant an unreachable or failing Unkey client could bubble a provider error instead of failing closed as unauthenticated.

This change wraps the Unkey verifyKey call and returns undefined when verification throws, keeping thrown provider failures consistent with invalid/rejected key results.

The tests cover:

  • expired-style invalid results
  • revoked-style invalid results
  • malformed-style invalid results
  • rate-limited invalid results
  • malformed-looking token inputs
  • thrown Unkey verification failures
  • tenant metadata preservation for tenant-scoped authorization
  • tenant mismatch rejection through host identity mapping

Type of Change

  • Bug fix
  • Test coverage
  • New feature
  • Breaking change
  • Documentation update

Test Coverage

Targeted tests passed:

bun run --filter=@dsar/auth-unkey test
bun run --filter=@dsar/auth-unkey typecheck
bun run check-types --filter=@dsar/auth-unkey
bun run --filter=@dsar/auth-unkey lint
bun x ultracite check packages/auth-unkey/src/index.ts packages/auth-unkey/test/index.test.ts
bun run check:tsdoc
git diff --check

Latest targeted local verification:

bun run --filter=@dsar/auth-unkey test
bun run --filter=@dsar/auth-unkey typecheck
bun x ultracite check packages/auth-unkey/src/index.ts packages/auth-unkey/test/index.test.ts
git diff --check

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved Unkey bearer token verification to fail closed: verification errors or unreachable provider responses now result in an unauthenticated request instead of an exception.
    • Invalid or malformed tokens now consistently resolve as no identity.
    • Enforced tenant-scoped behavior so mismatched tenant requests are rejected.
  • New Features
    • Added an optional onVerifyError hook to allow hosts to log/emit metrics when verification throws.
  • Tests
    • Expanded fail-closed test coverage, including malformed token handling and tenant fallback scenarios.

Walkthrough

The Unkey bearer resolver now treats verification exceptions as unauthenticated and can report them through an optional callback. Tests expand coverage for invalid codes, malformed tokens, tenant scoping, and metadata preservation.

Changes

Fail-closed verification error handling

Layer / File(s) Summary
Config and error hook
packages/auth-unkey/src/types.ts
UnkeyBearerResolverConfig adds an optional onVerifyError callback with documentation that verification exceptions still resolve as unauthenticated.
Fail-closed error handling in verification
packages/auth-unkey/src/index.ts
isValidResult becomes a type guard for optional verification results. verifyUnkeyToken wraps client.keys.verifyKey(...) in try/catch, forwards thrown errors to the optional callback, and returns undefined on failure. The resolver now uses the helper.
Fail-closed test coverage
packages/auth-unkey/test/index.test.ts, .changeset/auth-unkey-fail-closed.md
Test fixtures cover invalid verification codes, malformed tokens, invalid-result mocks, tenant-two identity data, and tenant-header mismatch handling. The fail-closed suite asserts undefined for invalid, rejected, or malformed verification paths, reports thrown errors through onVerifyError, and preserves metadata when verification succeeds. The changeset records the behavior.

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

Poem

🐰 The token hops, then meets a gentle wall,
If Unkey stumbles, we do not let it fall.
We catch the blur, return a quiet “none,”
And tenant gates stay steady in the sun.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The new optional onVerifyError hook and config surface go beyond issue #17's test-coverage scope. Split the diagnostics hook into a separate PR or remove it so this change stays focused on the linked coverage and fail-closed behavior.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main auth-unkey coverage change.
Description check ✅ Passed The description is on-topic and matches the fail-closed Unkey verification and expanded test coverage changes.
Linked Issues check ✅ Passed The changes cover the linked auth-unkey cases: invalid results, malformed tokens, tenant scoping, rate limits, and fail-closed Unkey failures.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@pullfrog pullfrog 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.

No new issues found.

TL;DR — Wraps client.keys.verifyKey() in try/catch inside @dsar/auth-unkey so a thrown Unkey verification error fails closed (resolver returns undefined) instead of bubbling out, and backfills test coverage for invalid-result codes, provider throws, malformed-token pass-through, and tenant metadata preservation.

Key changes

  • Fail closed on verifyKey throw — extract a verifyUnkeyToken() helper that returns undefined when verifyKey rejects, and widen isValidResult to accept undefined.
  • Table-driven invalid-result coverageINVALID_VERIFY_RESULTS exercises EXPIRED, REVOKED, MALFORMED, and RATE_LIMITED valid: false shapes.
  • Malformed-token pass-through assertion — documents that the resolver delegates malformed-looking tokens to Unkey rather than short-circuiting locally.
  • Tenant metadata preservation test — sanity check that tenant-scoped metadata flows into the resolved identity.

Summary | 2 files | 1 commit | base: mainfix/17-auth-unkey-test-coverage


Fail-closed on provider throw

Before: A thrown error from client.keys.verifyKey() escaped makeUnkeyBearerResolver() and surfaced as a provider error to callers.
After: Thrown verification errors are caught in verifyUnkeyToken() and converted to undefined, matching the behavior for valid: false results.

The contract narrows in the safe direction: callers no longer have to distinguish "Unkey threw" from "key rejected" — both surface as unauthenticated. The trade is that infra-level Unkey outages become observationally indistinguishable from real auth rejections at this layer; if observability hooks land later, this catch block is the place to emit them.

packages/auth-unkey/src/index.ts


Expanded fail-closed test coverage

Before: Only one invalid-result shape and no provider-throw case were exercised.
After: it.each tables cover four invalid-result codes plus a dedicated rejection test, and malformed-token inputs are asserted to round-trip to Unkey unchanged.

The new createInvalidVerifyKey / createRecordingInvalidVerifyKey helpers keep the table-driven tests compact, and the tenant-metadata test pins down the happy-path mapping for tenant-scoped identities.

packages/auth-unkey/test/index.test.ts

Pullfrog  | View workflow run | Using Claude Opus𝕏

@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 `@packages/auth-unkey/src/index.ts`:
- Around line 194-197: The catch block that swallows provider failures for
client.keys.verifyKey(verifyInput) should capture the thrown error (use catch
(err)), emit an observability signal (e.g., increment a metric like
"unkey.verify.failure" and/or call a structured logger such as
processLogger.warn or metricsClient.increment) including context (verifyInput
identifier or relevant metadata) and the error details, and then continue to
return undefined to preserve the fail-closed behavior; update the catch in the
function that returns UnkeyVerifyResultShape to perform these actions before
returning undefined.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 59a6c455-64ec-4d16-ba13-e4c06ca71c1f

📥 Commits

Reviewing files that changed from the base of the PR and between e0c300b and e42ab1b.

📒 Files selected for processing (2)
  • packages/auth-unkey/src/index.ts
  • packages/auth-unkey/test/index.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

Files:

  • packages/auth-unkey/src/index.ts
  • packages/auth-unkey/test/index.test.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Don't use eval() or assign directly to document.cookie
Avoid spread syntax in accumulators within loops
Use top-level regex literals instead of creating them in loops
Prefer specific imports over namespace imports
Use descriptive names for functions, variables, and types instead of relying on Oxlint + Oxfmt to catch naming issues
Add comments for complex logic, but prefer self-documenting code

Files:

  • packages/auth-unkey/src/index.ts
  • packages/auth-unkey/test/index.test.ts
**/index.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid barrel files (index files that re-export everything)

Files:

  • packages/auth-unkey/src/index.ts
**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{js,jsx,ts,tsx}: Write assertions inside it() or test() blocks
Avoid done callbacks in async tests - use async/await instead
Don't use .only or .skip in committed code
Keep test suites reasonably flat - avoid excessive describe nesting

Files:

  • packages/auth-unkey/test/index.test.ts
🔇 Additional comments (1)
packages/auth-unkey/test/index.test.ts (1)

8-35: LGTM!

Also applies to: 78-92, 134-147, 337-421

Comment thread packages/auth-unkey/src/index.ts
@devcool20

Copy link
Copy Markdown
Author

I kept this scoped to #17’s fail-closed requirement. @dsar/auth-unkey currently does not expose a logger/metrics hook or have an established package-level diagnostics pattern, so adding one here would introduce a new API/design surface beyond the issue scope.

The resolver now treats thrown Unkey verification errors the same as invalid verification results by returning undefined, preserving the backend auth contract. I’d be happy to add structured diagnostics in a follow-up if maintainers prefer a logger/metrics option for this package.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — one new test in packages/auth-unkey/test/index.test.ts exercising tenant-mismatch rejection via a custom mapIdentity. No source changes since the prior review.

  • Added tenant-mismatch rejection test — uses verifyAdminKey (resolves tenantId: "tenant-1") with a mapRequestedTenantIdentity mapper that returns null when the x-requested-tenant-id header disagrees, asserting the resolver short-circuits to undefined. Closes a gap around custom mapIdentity returning null.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@KayleeWilliams

Copy link
Copy Markdown
Collaborator

Maintainer review: reviewed the diff for supply-chain safety (no dependency, lockfile, or workflow changes; no exec/network beyond the resolver under test) and behavior. The fail-closed wrapping of verifyKey is the right call for issue #17's unreachable-Unkey scenario, and the new coverage matches the issue scope (expired/revoked/malformed/rate-limited results, malformed tokens, thrown provider failures, tenant metadata + mismatch). I pushed a merge of latest main plus a dsar patch changeset for the behavior change. Verified locally: 20/20 tests pass, ultracite and tsdoc checks clean. This fully closes #17.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the new commit adds an optional onVerifyError observability hook to the fail-closed Unkey resolver, directly resolving the "errors swallowed silently" tradeoff flagged in prior review context.

  • Added onVerifyError config hookUnkeyBearerResolverConfig gains an optional onVerifyError?: (error: unknown) => void, invoked inside verifyUnkeyToken's catch block before returning undefined. The resolver still fails closed; the hook only lets hosts log or emit metrics for thrown verification failures.
  • Fires on throw only — the hook is intentionally not called for valid: false results, keeping normal auth rejections distinct from provider errors. The doc comment on the type reflects this.
  • Added coverage — a test asserts the hook receives the thrown error while the resolver still resolves to undefined.
  • Bumped changeset patchminor — correct semver for the new public API surface.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@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.

Caution

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

⚠️ Outside diff range comments (1)
packages/auth-unkey/src/index.ts (1)

188-201: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Protect fail-closed behavior when onVerifyError throws.

If the onVerifyError callback throws synchronously (e.g., a metrics SDK is also down), the exception propagates out of the catch block before return undefined is reached, breaking the fail-closed guarantee this PR establishes. The resolver would throw instead of returning undefined, potentially surfacing a 500 instead of an unauthenticated response.

🔒️ Proposed fix
  } catch (error) {
-   onVerifyError?.(error);
+   try {
+     onVerifyError?.(error);
+   } catch {
+     // Swallow callback errors to preserve fail-closed behavior
+   }
    return undefined;
  }
🤖 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 `@packages/auth-unkey/src/index.ts` around lines 188 - 201, The fail-closed
path in verifyUnkeyToken is broken if onVerifyError throws inside the catch
block, because the exception escapes before undefined is returned. Update
verifyUnkeyToken in index.ts so the onVerifyError callback is invoked safely
inside its own protected handling, preserving the current behavior of returning
undefined whenever client.keys.verifyKey fails. Keep the fix localized to
verifyUnkeyToken and ensure any callback failure cannot turn the resolver into a
throw.
🤖 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.

Outside diff comments:
In `@packages/auth-unkey/src/index.ts`:
- Around line 188-201: The fail-closed path in verifyUnkeyToken is broken if
onVerifyError throws inside the catch block, because the exception escapes
before undefined is returned. Update verifyUnkeyToken in index.ts so the
onVerifyError callback is invoked safely inside its own protected handling,
preserving the current behavior of returning undefined whenever
client.keys.verifyKey fails. Keep the fix localized to verifyUnkeyToken and
ensure any callback failure cannot turn the resolver into a throw.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9a38d310-3590-4287-9324-c9629a4cf220

📥 Commits

Reviewing files that changed from the base of the PR and between 7368f0d and 23d1d61.

📒 Files selected for processing (4)
  • .changeset/auth-unkey-fail-closed.md
  • packages/auth-unkey/src/index.ts
  • packages/auth-unkey/src/types.ts
  • packages/auth-unkey/test/index.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

Files:

  • packages/auth-unkey/src/types.ts
  • packages/auth-unkey/src/index.ts
  • packages/auth-unkey/test/index.test.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Don't use eval() or assign directly to document.cookie
Avoid spread syntax in accumulators within loops
Use top-level regex literals instead of creating them in loops
Prefer specific imports over namespace imports
Use descriptive names for functions, variables, and types instead of relying on Oxlint + Oxfmt to catch naming issues
Add comments for complex logic, but prefer self-documenting code

Files:

  • packages/auth-unkey/src/types.ts
  • packages/auth-unkey/src/index.ts
  • packages/auth-unkey/test/index.test.ts
**/index.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid barrel files (index files that re-export everything)

Files:

  • packages/auth-unkey/src/index.ts
**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{js,jsx,ts,tsx}: Write assertions inside it() or test() blocks
Avoid done callbacks in async tests - use async/await instead
Don't use .only or .skip in committed code
Keep test suites reasonably flat - avoid excessive describe nesting

Files:

  • packages/auth-unkey/test/index.test.ts
🪛 markdownlint-cli2 (0.22.1)
.changeset/auth-unkey-fail-closed.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🔇 Additional comments (5)
packages/auth-unkey/src/types.ts (1)

54-61: LGTM!

packages/auth-unkey/src/index.ts (1)

219-223: LGTM!

packages/auth-unkey/test/index.test.ts (2)

390-412: LGTM!

Also applies to: 414-435, 437-458, 460-480


352-369: 📐 Maintainability & Code Quality

Test arrays cover the required cases.

.changeset/auth-unkey-fail-closed.md (1)

1-5: LGTM!

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.

[auth-unkey] Expand test coverage

2 participants