Skip to content

fix(vc): validate decoded credential shape in parseJwtCredential#115

Merged
venables merged 3 commits into
mainfrom
fix/parse-jwt-credential-shape-guard
Jun 20, 2026
Merged

fix(vc): validate decoded credential shape in parseJwtCredential#115
venables merged 3 commits into
mainfrom
fix/parse-jwt-credential-shape-guard

Conversation

@venables

@venables venables commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #113. Addresses the unchecked-cast review comment left on #110: parseJwtCredential cast verifyCredential's result to Verifiable<W3CCredential> without validating it.

verifyCredential (did-jwt-vc) returns its own credential shape. parseJwtCredential now validates that shape conforms to our W3CCredential (via the existing isCredential / credentialSchema) before returning it, throwing InvalidCredentialError on a divergent shape rather than silently casting. Since parseJwtCredential is the single decode point used by verifyProof / verifyParsedCredential / verifyPaymentReceipt, this hardens the whole verification chain.

Changes

  • packages/vc/src/verification/parse-jwt-credential.ts — validate result.verifiableCredential with isCredential before returning.
  • packages/vc/src/verification/parse-jwt-credential.test.ts — regression test for a non-conforming decoder result (delegating did-jwt-vc mock so the existing real-signing test still exercises the real implementation).

Verification

  • Legitimate did-jwt-vc output is unaffected (the real-signing parse test still passes).
  • @agentcommercekit/vc 49/49 and @agentcommercekit/ack-pay 34/34 pass; full pnpm run check is green.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved validation of JWT credentials by verifying the decoded credential’s structure before processing.
    • Updated proof verification to preserve InvalidCredentialError when credential shape validation fails, and treat other failures as InvalidProofError.
  • Tests
    • Expanded parseJwtCredential and verifyProof test coverage for edge cases and additional credential payload fields.
  • Documentation
    • Added a changeset noting that parseJwtCredential now validates credential shape instead of relying on unchecked casting.

did-jwt-vc's `verifyCredential` returns its own credential shape, which
`parseJwtCredential` cast to `Verifiable<W3CCredential>` unchecked. If that
shape ever diverged, the cast would silently mask the mismatch.

Validate the decoded credential against `isCredential` (our `credentialSchema`)
before returning, throwing `InvalidCredentialError` on a non-conforming shape.
Adds a regression test (delegating did-jwt-vc mock so the real-signing tests
still run); legitimate did-jwt-vc output is unaffected.

Follow-up to #113 (addresses the unchecked-cast review comment from #110).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@venables, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 15 minutes and 18 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43608b53-25d6-4854-ad68-d3f2cb5d3a22

📥 Commits

Reviewing files that changed from the base of the PR and between 463daac and eaf5ccf.

📒 Files selected for processing (1)
  • packages/vc/src/verification/parse-jwt-credential.test.ts

Walkthrough

parseJwtCredential now validates decoded credentials against the W3CCredential shape via a local isDecodedCredential guard, throwing InvalidCredentialError when shape requirements like issuer.id, credentialSubject, type array, and proof presence are not met. The error handling in verify-proof now preserves InvalidCredentialError instead of converting it to InvalidProofError. Tests mock did-jwt-vc and validate both the new rejection path for mismatched/malformed credentials and acceptance of valid ones with JSON-LD entries; the verify-proof test fixture is expanded with required credential fields. A changeset entry documents the patch.

Changes

parseJwtCredential shape guard

Layer / File(s) Summary
Shape guard implementation in parseJwtCredential
packages/vc/src/verification/parse-jwt-credential.ts
Adds isDecodedCredential type guard with documented validation rules for issuer.id, credentialSubject, type, and proof; parseJwtCredential uses this guard to throw InvalidCredentialError on mismatch instead of performing an unchecked cast.
Error propagation in verify-proof
packages/vc/src/verification/verify-proof.ts
Expands the ./errors import to include InvalidCredentialError; verifyJwtProof catch block now preserves InvalidCredentialError by rethrowing it, instead of converting all errors to InvalidProofError.
Test coverage for shape validation and fixture expansion
packages/vc/src/verification/parse-jwt-credential.test.ts, packages/vc/src/verification/verify-proof.test.ts
Introduces module-level vi.mock("did-jwt-vc") with an overridable verifyCredential; adds tests asserting InvalidCredentialError is thrown for non-credentials and malformed issuer shapes, and that valid credentials with JSON-LD entries are accepted; expands verify-proof fixture with @context, type, issuanceDate, and proof.
Changeset documentation
.changeset/parse-jwt-credential-shape-guard.md
Documents the patch: parseJwtCredential now validates decoded credential shape and throws InvalidCredentialError on mismatch instead of relying on an unchecked cast.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • agentcommercekit/ack#113: Both PRs address the proof→credential decoding path: this PR hardens parseJwtCredential with runtime W3C shape validation and ensures verifyProof rethrows InvalidCredentialError, while the related PR refactors verifyProof/verifyParsedCredential to rely on the credential decoded from the verified JWT, making invalid-decoded shapes fail consistently.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 and concisely summarizes the main change: validating decoded credential shape in parseJwtCredential, which is the core purpose of this PR.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/parse-jwt-credential-shape-guard

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.

@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/vc/src/verification/parse-jwt-credential.test.ts`:
- Around line 72-83: Replace the `test` function call with `it` at the test case
"throws when the verified JWT does not decode to a valid credential" to follow
the project's test naming convention. The test should use the `it()` pattern for
assertive test names that clearly describe the expected behavior.
🪄 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: CHILL

Plan: Pro

Run ID: b7a70d4c-de9b-4e81-9a50-1e678f3b22bb

📥 Commits

Reviewing files that changed from the base of the PR and between 81c68bf and 28234c0.

📒 Files selected for processing (4)
  • .changeset/parse-jwt-credential-shape-guard.md
  • packages/vc/src/verification/parse-jwt-credential.test.ts
  • packages/vc/src/verification/parse-jwt-credential.ts
  • packages/vc/src/verification/verify-proof.test.ts

Comment thread packages/vc/src/verification/parse-jwt-credential.test.ts Outdated
Panel review of #115 found the `isCredential` guard was the wrong validator:
- it validates ACK's authoring schema's pre-transform shape, so it would PASS a
  top-level string `issuer` that downstream then reads as `issuer.id ===
  undefined` (codex);
- and it is simultaneously too strict — rejecting valid W3C VCs with an object
  `@context` entry or a VC 2.0 shape that the previous cast accepted (claude).

Replace it with a focused `isDecodedCredential` guard that validates exactly
what the verification chain consumes: `credentialSubject` (object), `issuer.id`
(string), `type` (array), and `proof` — and nothing else, so valid third-party
VCs are not false-rejected. Add tests for the string-issuer rejection and the
object-`@context` acceptance.

Also preserve `InvalidCredentialError` through `verifyJwtProof` instead of
re-wrapping it as `InvalidProofError`, so a malformed decoded credential is
distinguishable from a bad signature (opencode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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/vc/src/verification/parse-jwt-credential.test.ts (1)

16-21: ⚠️ Potential issue | 🟡 Minor

Use vi.importActual() instead of the importOriginal parameter for this partial mock.

The codebase standardizes on vi.importActual() to preserve real implementations in partial mocks. Replace the importOriginal parameter with a direct call to vi.importActual("did-jwt-vc") to match the pattern used elsewhere in the verification module (e.g., verify-proof.test.ts).

🤖 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/vc/src/verification/parse-jwt-credential.test.ts` around lines 16 -
21, Replace the `importOriginal` parameter usage in the vi.mock call with a
direct call to vi.importActual("did-jwt-vc") to match the codebase's
standardization pattern for partial mocks. Instead of using the importOriginal
callback parameter, call vi.importActual("did-jwt-vc") to obtain the actual
implementation, then spread it in the return object and override only the
verifyCredential function with vi.fn(actual.verifyCredential) to maintain the
same behavior.

Source: Coding guidelines

♻️ Duplicate comments (1)
packages/vc/src/verification/parse-jwt-credential.test.ts (1)

72-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename new test cases to assertive it("throws...") / it("returns...") forms.

The added regression tests still use test(...); this violates the required naming convention for *.test.ts files.

As per coding guidelines, "Use assertive test names with patterns: it(\"creates...\") , it(\"throws...\") , it(\"requires...\") , it(\"returns...\")."

Also applies to: 85-103, 105-127

🤖 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/vc/src/verification/parse-jwt-credential.test.ts` around lines 72 -
83, The test cases in this file use the `test(...)` function instead of
`it(...)`, which violates the required assertive naming convention. Replace all
instances of `test(` with `it(` in the new test cases, including the one
starting with "throws when the verified JWT does not decode to a valid
credential" and the other test cases referenced in the comment. The test names
themselves are already properly assertive with patterns like "throws..." and
"returns...", so only the function call needs to change from `test` to `it`.

Source: Coding guidelines

🤖 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/vc/src/verification/parse-jwt-credential.test.ts`:
- Around line 16-21: Replace the `importOriginal` parameter usage in the vi.mock
call with a direct call to vi.importActual("did-jwt-vc") to match the codebase's
standardization pattern for partial mocks. Instead of using the importOriginal
callback parameter, call vi.importActual("did-jwt-vc") to obtain the actual
implementation, then spread it in the return object and override only the
verifyCredential function with vi.fn(actual.verifyCredential) to maintain the
same behavior.

---

Duplicate comments:
In `@packages/vc/src/verification/parse-jwt-credential.test.ts`:
- Around line 72-83: The test cases in this file use the `test(...)` function
instead of `it(...)`, which violates the required assertive naming convention.
Replace all instances of `test(` with `it(` in the new test cases, including the
one starting with "throws when the verified JWT does not decode to a valid
credential" and the other test cases referenced in the comment. The test names
themselves are already properly assertive with patterns like "throws..." and
"returns...", so only the function call needs to change from `test` to `it`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 108b0c0c-ce58-4176-aa1a-b30e4458009d

📥 Commits

Reviewing files that changed from the base of the PR and between 28234c0 and 463daac.

📒 Files selected for processing (3)
  • packages/vc/src/verification/parse-jwt-credential.test.ts
  • packages/vc/src/verification/parse-jwt-credential.ts
  • packages/vc/src/verification/verify-proof.ts

Resolves the CodeRabbit comment on #115: the file used test() while the rest
of the suite (and AGENTS.md) uses assertive it("...") names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@venables venables merged commit 101a823 into main Jun 20, 2026
3 checks passed
@venables venables deleted the fix/parse-jwt-credential-shape-guard branch June 20, 2026 17:33
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