Skip to content

Conversation

@rvlb
Copy link
Contributor

@rvlb rvlb commented Dec 11, 2025

Summary by Sourcery

Integrate optional VCI directory snapshot support into SHCReader and improve shared test fixtures and documentation.

New Features:

  • Allow SHCReader to automatically fetch issuer metadata from the VCI directory snapshot via a new useVciDirectory configuration option.

Enhancements:

  • Prevent invalid SHCReader configurations by throwing SHCReaderConfigError when both issuerDirectory and useVciDirectory are provided.
  • Refactor sample directory JSON into a shared test helper for reuse across tests.

Documentation:

  • Document how to use SHCReader with the VCI directory snapshot and with a custom Directory object, including example code.

Tests:

  • Add tests covering VCI directory integration with SHCReader and configuration error handling when conflicting directory options are set.

Closes #4

@rvlb rvlb requested a review from fjsj December 11, 2025 18:53
@rvlb rvlb self-assigned this Dec 11, 2025
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 11, 2025

Reviewer's Guide

Adds optional integration of the VCI directory snapshot into SHCReader, wires it through config and runtime verification flow, documents usage in README, introduces a dedicated config error, and centralizes shared test fixtures.

Sequence diagram for SHCReader.fromJWS with optional VCI directory snapshot

sequenceDiagram
  actor Developer
  participant SHCReader
  participant FHIRBundleProcessor
  participant VCProcessor
  participant Directory as IssuerDirectory
  participant VciDirectory as VciDirectorySnapshot
  participant SHC

  Developer->>SHCReader: new SHCReader(config)
  alt both issuerDirectory and useVciDirectory set
    SHCReader-->>Developer: throw SHCReaderConfigError
  else valid configuration
    SHCReader-->>Developer: SHCReader instance
  end

  Developer->>SHCReader: fromJWS(jws)
  activate SHCReader
  SHCReader->>FHIRBundleProcessor: validate(originalBundle)
  FHIRBundleProcessor-->>SHCReader: ok
  SHCReader->>VCProcessor: validate(vc)
  VCProcessor-->>SHCReader: ok

  alt issuerDirectory provided
    SHCReader->>Directory: getIssuerInfo()
    Directory-->>SHCReader: issuerInfo
  else useVciDirectory is true
    SHCReader->>VciDirectory: fromVCI()
    VciDirectory-->>SHCReader: vciDirectory
    SHCReader->>VciDirectory: getIssuerInfo()
    VciDirectory-->>SHCReader: issuerInfo
  else no directory configured
    SHCReader-->>SHCReader: issuerInfo = []
  end

  SHCReader->>SHC: new SHC(jws, originalBundle, issuerInfo)
  SHC-->>SHCReader: shc
  SHCReader-->>Developer: shc
  deactivate SHCReader
Loading

Class diagram for updated SHCReader configuration and VCI integration

classDiagram
  class SHCReader {
    - SHCReaderConfigParams config
    - FHIRBundleProcessor bundleProcessor
    - VCProcessor vcProcessor
    + SHCReader(config SHCReaderConfigParams)
    + fromJWS(jws string) Promise~SHC~
  }

  class SHCReaderConfigParams {
    + string issuer
    + string publicKey
    + boolean enableQROptimization
    + boolean strictReferences
    + boolean verifyExpiration
    + Directory issuerDirectory
    + boolean useVciDirectory
  }

  class Directory {
    + getIssuerInfo() Issuer[]
    + fromVCI() Directory
  }

  class SHC {
    + SHC(jws string, bundle FHIRBundle, issuerInfo Issuer[])
    + asBundle() FHIRBundle
    + getIssuerInfo() Issuer[]
  }

  class SHCError {
    + string code
    + string message
  }

  class SHCReaderConfigError {
    + SHCReaderConfigError(message string)
  }

  class Issuer {
  }

  class FHIRBundle {
  }

  class FHIRBundleProcessor {
    + validate(bundle FHIRBundle) void
  }

  class VCProcessor {
    + validate(vc VerifiableCredential) void
  }

  class VerifiableCredential {
  }

  SHCReader --> SHCReaderConfigParams : uses
  SHCReader --> FHIRBundleProcessor : composes
  SHCReader --> VCProcessor : composes
  SHCReader --> Directory : optional issuerDirectory
  SHCReader --> SHC : returns
  SHCReaderConfigParams --> Directory : issuerDirectory
  SHCReaderConfigError --|> SHCError
  Directory --> Issuer : returns
  SHC --> Issuer : contains
  FHIRBundleProcessor --> FHIRBundle : validates
  VCProcessor --> VerifiableCredential : validates
Loading

File-Level Changes

Change Details Files
Introduce optional use of the VCI directory snapshot in SHCReader and its verification flow.
  • Extend SHCReaderConfigParams with a boolean useVciDirectory flag defaulting to false.
  • Update SHCReader constructor to normalize useVciDirectory, and guard against simultaneous issuerDirectory and useVciDirectory via SHCReaderConfigError.
  • Update fromJWS to load issuer metadata from either a provided issuerDirectory or, when enabled, from Directory.fromVCI(), and pass issuerInfo into the SHC instance.
src/shc/types.ts
src/shc/reader.ts
Add a specific configuration error type for SHCReader misuse.
  • Create SHCReaderConfigError extending SHCError with INVALID_CONFIGURATION code and proper metadata.
  • Export and use SHCReaderConfigError in tests to assert invalid combinations of configuration options.
src/shc/errors.ts
test/shc/shc.test.ts
src/index.ts
Document and test VCI directory snapshot usage and refactor shared test data.
  • Document SHCReader usage with VCI directory snapshot and with a generic Directory in README, including a full TypeScript usage example.
  • Add tests that mock fetch to exercise Directory.fromVCI integration in SHCReader and ensure issuerInfo from VCI is bound into SHC instances.
  • Add a test asserting SHCReaderConfigError is thrown when issuerDirectory and useVciDirectory are both set.
  • Move SAMPLE_DIRECTORY_JSON into a reusable test helper module and update existing tests to import it.
README.md
test/shc/shc.test.ts
test/shc/directory.test.ts
test/helpers.ts

Possibly linked issues

  • #SHC VCI Directory support: The PR implements SHCReader support for the VCI directory snapshot, wiring Directory.fromVCI and documenting useVciDirectory.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link

github-actions bot commented Dec 11, 2025

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 95.93% 1959 / 2042
🔵 Statements 95.93% 1959 / 2042
🔵 Functions 98.51% 133 / 135
🔵 Branches 90.37% 620 / 686
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/shc/errors.ts 100% 100% 100% 100%
src/shc/reader.ts 94.32% 77.35% 100% 94.32% 200-201, 203-204, 218-219, 224-225
src/shc/types.ts 0% 0% 0% 0%
Generated in workflow #110 for commit 3cbcd89 by the Vitest Coverage Report Action

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In the new VCI directory test, globalThis.fetch is reassigned without a try/finally, so if an assertion or await fails the global fetch may remain mocked; consider using try/finally or vi.stubGlobal to ensure it’s always restored.
  • SHCReader.fromJWS now calls Directory.fromVCI() on each invocation when useVciDirectory is true, which will repeatedly hit the network; consider adding an internal cache or allowing the directory snapshot to be injected so it can be reused.
  • SAMPLE_DIRECTORY_JSON in test/helpers.ts could be typed as DirectoryJSON (or the appropriate type) to ensure the test fixture stays aligned with the expected directory schema.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the new VCI directory test, `globalThis.fetch` is reassigned without a `try/finally`, so if an assertion or await fails the global fetch may remain mocked; consider using `try/finally` or `vi.stubGlobal` to ensure it’s always restored.
- `SHCReader.fromJWS` now calls `Directory.fromVCI()` on each invocation when `useVciDirectory` is true, which will repeatedly hit the network; consider adding an internal cache or allowing the directory snapshot to be injected so it can be reused.
- `SAMPLE_DIRECTORY_JSON` in `test/helpers.ts` could be typed as `DirectoryJSON` (or the appropriate type) to ensure the test fixture stays aligned with the expected directory schema.

## Individual Comments

### Comment 1
<location> `src/shc/reader.ts:174-175` </location>
<code_context>
       let issuerInfo: Issuer[] = []
       if (this.config.issuerDirectory) {
         issuerInfo = this.config.issuerDirectory.getIssuerInfo()
+      } else if (this.config.useVciDirectory) {
+        const vciDirectory = await Directory.fromVCI()
+        issuerInfo = vciDirectory.getIssuerInfo()
       }
</code_context>

<issue_to_address>
**suggestion (performance):** Repeatedly constructing the VCI directory on every call may be unnecessarily expensive; consider caching or sharing the instance.

With `useVciDirectory` enabled, `Directory.fromVCI()` runs on every read, which may involve unnecessary network or initialization work. If the directory data is stable for the lifetime of `SHCReader`, consider caching the `Directory` instance (e.g., on the reader instance or via a shared cache) to avoid repeated calls.

Suggested implementation:

```typescript
      // Step 4: Get the issuer info from a provided directory
      // or from the VCI snapshot
      let issuerInfo: Issuer[] = []
      if (this.config.issuerDirectory) {
        issuerInfo = this.config.issuerDirectory.getIssuerInfo()
      } else if (this.config.useVciDirectory) {
        if (!this.vciDirectory) {
          this.vciDirectory = await Directory.fromVCI()
        }
        issuerInfo = this.vciDirectory.getIssuerInfo()
      }

```

To fully implement the caching suggestion, also:

1. Add a cached directory field to the `SHCReader` class (or whichever class this method belongs to), for example:
   `private vciDirectory: Directory | null = null;`

2. Ensure `Directory` is already imported in this file (it likely is, since `Directory.fromVCI()` is used). If not, add the appropriate import.

These changes will keep a single `Directory` instance per `SHCReader` (or class instance), avoiding repeated network/initialization work when `useVciDirectory` is enabled.
</issue_to_address>

### Comment 2
<location> `test/shc/shc.test.ts:128-137` </location>
<code_context>
+    it('should fetch the VCI directory and bundle issuerInfo into SHC', async () => {
</code_context>

<issue_to_address>
**suggestion (testing):** Restore `globalThis.fetch` in a `finally` block to avoid cross-test leakage if the test throws before the manual restore.

Because the restore happens only after the async calls, any thrown error will leave `globalThis.fetch` mocked for later tests. Wrapping the override in `try/finally` avoids this:

```ts
const originalFetch = globalThis.fetch
const fetchMock = vi.fn().mockImplementation(/* ... */)
;(globalThis as any).fetch = fetchMock

try {
  const vciDirectory = await Directory.fromVCI()
  const verifiedHealthCard = await readerWithDirectory.fromJWS(jws)
  expect(verifiedHealthCard.getIssuerInfo()).toEqual(
    vciDirectory.getIssuerInfo(),
  )
} finally {
  ;(globalThis as any).fetch = originalFetch
}
```

Suggested implementation:

```typescript
    it('should fetch the VCI directory and bundle issuerInfo into SHC', async () => {
      const { importPKCS8, importSPKI } = await import('jose')

      const privateKeyCrypto = await importPKCS8(testPrivateKeyPKCS8, 'ES256')
      const publicKeyCrypto = await importSPKI(testPublicKeySPKI, 'ES256')

      const configWithCryptoKeys: SHCConfig = {
        issuer: 'https://example.com/issuer',
        privateKey: privateKeyCrypto,
        publicKey: publicKeyCrypto,
        expirationTime: null,
      }

      const originalFetch = globalThis.fetch
      const fetchMock = vi.fn().mockResolvedValue({
        ok: true,
        json: async () => SAMPLE_DIRECTORY_JSON,
      })
      ;(globalThis as any).fetch = fetchMock

      try {
        // existing test logic that:
        //  - creates a Directory via Directory.fromVCI()
        //  - creates a readerWithDirectory
        //  - calls readerWithDirectory.fromJWS(jws)
        //  - asserts on getIssuerInfo()
      } finally {
        ;(globalThis as any).fetch = originalFetch
      }

```

You will need to:

1. Move the existing body of this test (everything after `const configWithCryptoKeys` that currently calls `Directory.fromVCI`, `readerWithDirectory.fromJWS`, and the `expect(...)` assertion) into the `try { ... }` block, replacing the placeholder comment.
2. Remove any existing manual restore of `globalThis.fetch` at the end of the test body (e.g. `;(globalThis as any).fetch = originalFetch`) so that restoration only happens in the `finally` block.
3. If the test currently defines `fetchMock` or `originalFetch` elsewhere in the body, deduplicate so that there is a single `const originalFetch = globalThis.fetch` defined before the override, and a single `const fetchMock = ...` used for the override.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@vercel
Copy link

vercel bot commented Dec 11, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
medplum-shl Ready Ready Preview Comment Dec 11, 2025 6:55pm

@rvlb
Copy link
Contributor Author

rvlb commented Dec 13, 2025

Closing this in favor of #9

@rvlb rvlb closed this Dec 13, 2025
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.

SHC VCI Directory support

2 participants