Skip to content

Add endpoint to list (about-to-)expired wallet credentials#4224

Draft
reinkrul wants to merge 3 commits into
masterfrom
4217-expiring-credentials-endpoint
Draft

Add endpoint to list (about-to-)expired wallet credentials#4224
reinkrul wants to merge 3 commits into
masterfrom
4217-expiring-credentials-endpoint

Conversation

@reinkrul
Copy link
Copy Markdown
Member

Closes #4217.

Summary

  • Adds GET /internal/vcr/v2/holder/expiring?within=<duration> — aggregates across all wallets on the node, returns a JSON object keyed by subject ID with the list of expired or about-to-expire credentials per subject.
  • Default within is 720h (30 days); 0s returns only already-expired credentials. Negative or unparseable values give 400.
  • Response uses a focused monitoring DTO (id, holder, issuer, type, expirationDate) rather than the raw VC, so the shape stays uniform regardless of whether the underlying credential is JSON-LD or JWT-encoded. Operators needing the full VC can fetch it by id via existing wallet endpoints.
{
  "90BC1AE9-752B-432F-ADC3-DD9F9C61843C": [
    {
      "id":             "did:web:issuer.example.com#abc",
      "holder":         "did:web:example.com:iam:123",
      "issuer":         "did:web:issuer.example.com",
      "type":           ["NutsOrganizationCredential"],
      "expirationDate": "2026-05-15T12:00:00Z"
    }
  ]
}

Subjects with no expiring credentials are omitted from the response.

Test plan

  • Unit tests covering: groups across subjects, custom within, within=0 (only already-expired), no subjects (empty map), invalid within, negative within, subject manager error
  • go build ./...
  • go test ./vcr/api/vcr/v2/...

Assisted by AI

Adds GET /internal/vcr/v2/holder/expiring which aggregates credentials
across all wallets on the node and returns a JSON object grouping
expiring credentials by subject ID. Operators can poll a single URL to
monitor and refresh credentials before they expire (closes #4217).

The response is a focused monitoring DTO (id, holder, issuer, type,
expirationDate) rather than the raw VC, so the shape stays uniform
regardless of whether the underlying credential is JSON-LD or JWT-encoded.

Assisted by AI
@qltysh
Copy link
Copy Markdown
Contributor

qltysh Bot commented Apr 30, 2026

1 new issue

Tool Category Rule Count
qlty Structure Function with many returns (count = 8): NewGetExpiringCredentialsInWalletRequest 1

@reinkrul
Copy link
Copy Markdown
Member Author

reinkrul commented Apr 30, 2026

TODO / open question — filtering by credential type:

Some credential types are expected to expire and shouldn't trigger refresh alerts, but operators still want to keep them in the wallet for audit / paper-trail purposes (e.g. NutsAuthorizationCredential). For those, deleting is not an option.

We may want to add type-based filtering to this endpoint, e.g. ?excludeType=NutsAuthorizationCredential (and/or ?includeType=...), so monitoring tools can suppress credentials that are expected to expire.

Assisted by AI

@qltysh
Copy link
Copy Markdown
Contributor

qltysh Bot commented Apr 30, 2026

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on master by 0.03%.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
vcr/api/vcr/v2/api.go97.0%533-534
Total97.0%
🤖 Increase coverage with AI coding...
In the `4217-expiring-credentials-endpoint` branch, add test coverage for this new code:

- `vcr/api/vcr/v2/api.go` -- Line 533-534

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

Copy link
Copy Markdown
Member

@stevenvegt stevenvegt left a comment

Choose a reason for hiding this comment

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

Didn't look into to the implementation yet, first lets discuss the API and how people might want to use this feature.

in: query
description: |
Time window relative to now in which a credential's `expirationDate` falls for it to be considered
expiring. Accepts a Go duration string (e.g. `24h`, `720h`, `30m`). Must be non-negative.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Most devs probably are not familiar with go duration strings, so better to take the relevant parts from the spec:
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".

summary: List credentials across all wallets on this node that are expired or about to expire.
description: |
Returns all credentials held by any subject on this node whose `expirationDate` is at or before
`now + within`. This includes credentials that are already expired. Credentials without an
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since eOverdracht (and others) uses NutsAuthVCs, this list will quickly become longer and longer. I think you might want to add some additional filtering here to include or exclude certain types. Also a param to ignore already expired VCs might be a good idea since you probably want to signal for upcoming expiration?

Copy link
Copy Markdown
Member Author

@reinkrul reinkrul May 18, 2026

Choose a reason for hiding this comment

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

Since eOverdracht (and others) uses NutsAuthVCs, this list will quickly become longer and longer. I think you might want to add some additional filtering here to include or exclude certain types.

That's right, we need something like that. I'm leaning towards an exclude model, because an include model quickly becomes out of date if a new credential is introduced. So you'd have something like:

/expiring?within=30d&exclude=NutsAuthorizationCredential

The downside is that you have to explicitly (in many cases, always) exclude certain types, but at least it'll be visible for operators if the configuration is off.

Also a param to ignore already expired VCs might be a good idea since you probably want to signal for upcoming expiration?

Maybe... You don't want to keep being bothered if you don't clean up expired VCs, on the other hand you could've missed/ignored (and forgot) about renewing it. We could make it more flexible (at the cost of a more complex API), by adding a parameter which specifies for how long we'll keep returning it, after it expired. E.g., return VCs that expired less than a week ago.

Now I think of it, you also don't want to send e-mails (if your monitoring system does that) every day for the same VC, 30 days straight (if you're checking for VCs that expire within 30 days, every day). Not sure if we should solve that here, but it complicates things.

Proposal: keep this feature simple at first;

  • Add excludeTypes parameter
  • Let the monitoring system deal with not sending too many notifications for the same VC every hour/day (we're not building a monitoring system here, just feeding it with data)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thinking about it, lets include expired credentials, since those should be visible so vendors can clean them up.

@reinkrul
Copy link
Copy Markdown
Member Author

We also need to add this to the official monitoring documentation.

@reinkrul
Copy link
Copy Markdown
Member Author

We'll be adding exclude_types as parameter

reinkrul added 2 commits May 22, 2026 12:18
Allows operators to suppress credentials that are expected to expire
and are kept for audit purposes (e.g. NutsAuthorizationCredential).
A credential is excluded if any of its types matches any supplied value.

Assisted by AI
@reinkrul
Copy link
Copy Markdown
Member Author

reinkrul commented May 22, 2026

TODO: push filtering down to the SQL store

Neither within nor excludeTypes is currently pushed to the SQL layer. GetExpiringCredentialsInWallet calls Wallet().SearchCredential(ctx, holderDID), which runs SELECT ... WHERE holder_did = ? and parses every raw VC blob for that holder; within and excludeTypes are then applied in the Go loop (api.go).

For larger wallets this means a full load + JSON parse of every credential of every subject on the node on every call. Both filters should be propagated all the way down to the SQL store before this merges.

What that needs:

  • excludeTypes: straightforward — credential.type is a queryable column. It stores only the first non-VerifiableCredential type, but since the node doesn't support credentials with multiple types, that column fully represents the credential's type. A WHERE type NOT IN (...) works directly.
  • within: no queryable column exists; expirationDate is a top-level VC property and isn't indexed (credential_prop only covers credentialSubject.* paths). Needs a new expiration_date column + migration.
  • A new walletStore query method taking the filter args, instead of the current list(holderDID).

Assisted by AI

@stevenvegt stevenvegt marked this pull request as draft May 22, 2026 14:43
@stevenvegt
Copy link
Copy Markdown
Member

Given the previous comment, I think this PR needs some more work? I've converted it to state "draft".

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.

Add endpoint for detecting (about to) expired credentials

2 participants