Commit Graph

52 Commits

Author SHA1 Message Date
Cursor Agent
88eb340aba Use VCR cassette for PostHog driver test
Replace the PostHog driver unit test's local HTTP server with the same\nrecorder-backed test style used by the other access-review drivers.\n\nAdd a committed PostHog cassette under testdata so replay mode works\nwithout network access while keeping fixture coverage for role, MFA,\nand timestamp mapping expectations.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 18:37:18 -07:00
Cursor Agent
4d1417b512 Add PostHog access review connector support
Introduce a PostHog access-review driver that lists organization\nmembers and maps role, MFA, and timestamp fields into account\nrecords.\n\nRegister PostHog as a builtin API-key connector provider and expose\nit through the connector provider enum so access-review source\ncreation can discover it.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 18:37:18 -07:00
Cursor Agent
b493534545 Use k7 Metabase cassette fixture
Align the Metabase driver VCR test with the existing driver fixture
pattern by using a k7 Metabase host in the cassette and default test
instance URL.

This keeps cassette replay deterministic and consistent with the
expected environment naming used by other driver tests.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 18:36:36 -07:00
Cursor Agent
0920785bdf Add Metabase access review source
Implement Metabase as a first-class access review connector backed by
GET /api/user, including account mapping and error handling in the
driver. Register the provider with API-key auth metadata and required
instance URL settings so connectors can be created and resolved
consistently.

Expose Metabase through the console GraphQL and UI flows by adding the
provider enum value, API-key extra setting field wiring, and source
label mapping. Add migration support for the connector_provider enum and
cover driver/provider behavior with focused tests.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-28 18:36:36 -07:00
Cursor Agent
5ca1e369c0 Adopt VCR cassette style for Grafana tests
Refactor the Grafana driver test to use the shared recorder and VCR
helpers used by other access-review drivers.

This aligns the test with the existing cassette workflow and adds a
committed cassette fixture for deterministic replay.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 18:35:02 -07:00
Cursor Agent
f5a632ffac Add Grafana access review connector support
Add Grafana as an access review connector-backed source.

This introduces a Grafana access-review driver, provider registration,
and connector settings for the Grafana base URL. It also wires the
new provider through GraphQL and access-review UI input mapping so
API-key connectors can be created from the product.

A connector_provider enum migration is included so Grafana can be
persisted in existing databases.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 18:35:01 -07:00
Aurélien Sibiril
427af88f3a Add Cursor access-review driver
Fetch team members from the Cursor Admin API (GET /teams/members)
and map them to access records. The endpoint is not paginated, so
a single request returns the whole team; removed members are
returned as inactive rather than dropped, and team owners are
flagged as admins.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-29 00:05:48 +02:00
Bryan Frimin
122bc02a23 Fix too much account on MS365 access review
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 14:56:54 -07:00
Aurélien Sibiril
50093c9d19 Add Anthropic access-review driver and resolver
The driver lists organization members through the Anthropic Admin API
(GET /v1/organizations/users) with cursor pagination, mapping the role
and the RFC 3339 added_at timestamp. The name resolver reads the
organization name from /v1/organizations/me; a non-2xx response (for
example a revoked key) yields no name rather than making the
source-name worker retry forever. Both send the required
anthropic-version header.

Add a VCR test helper that injects the key via x-api-key so the
cassette stays recordable, and strip x-api-key on save. The cassette
holds synthetic members covering the user, developer, and admin roles.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 22:33:29 +02:00
Aurélien Sibiril
e0c53e7a51 Name Heroku personal account without an API call
GET /teams/@personal 404s, which would loop the source-name worker the
same way a stale Sentry slug did. Short-circuit the personal-account
slug to a static name before any HTTP call.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 22:22:46 +02:00
Aurélien Sibiril
3b54a58d14 Offer Personal account in Heroku org picker
Heroku Teams are opt-in, so a solo account surfaces no options and the
picker used to dead-end at a free-text slug the user cannot fill. Always
append a synthetic Personal account entry so the picker offers personal
mode instead.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 22:22:46 +02:00
Aurélien Sibiril
05fc9508ec Review solo Heroku accounts via app collaborators
A personal Heroku account has no Team, so GET /teams returns an empty
list and the team-members path has nothing to fetch. Solo accounts
grant access per app, so enumerate the user's personal apps and collect
each app's owner plus its collaborators, deduped by Heroku user ID.

An empty teamID (or the reserved personal-account slug) now selects this
personal mode. Factor the Range/Next-Range pagination shared by the
members, apps, and collaborators endpoints into herokuListAll.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 22:22:46 +02:00
Bryan Frimin
bf4b3214f6 Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 12:36:45 -07:00
Bryan Frimin
0161b21120 Best effort connector delete
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 12:30:36 -07:00
Bryan Frimin
d579879707 Add tailscale driver
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 12:30:34 -07:00
Aurélien Sibiril
93d8d20dbf Surface stale Sentry slug in ListAccounts error
ListAccounts on a connector whose stored slug is no longer accessible
to its OAuth token currently returns "cannot fetch sentry members:
unexpected status 404" -- opaque, and indistinguishable from a real
Sentry outage. The campaign source-fetch worker records that string
verbatim as the customer-visible LastError, with no hint that the
connector itself needs reconnection.

queryMembers now returns a sentinel errSentryOrgNotAccessible on 404,
and ListAccounts wraps it with the slug and a directive to reconnect.
errors.Is preserves the chain so future callers can branch on the
permanent-config-failure case without string matching.

No auto-recovery: the only safe slug is one the customer explicitly
chose. Picking a different visible org would silently rebind the
source to the wrong tenant.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 19:22:45 +02:00
Aurélien Sibiril
1844797b39 Stop source-name worker from looping on stale Sentry slug
The source-name worker re-claims any AccessSource whose name resolver
returns an error. kit/worker drains tasks in a tight inner loop per
tick, so a permanently-failing resolver hammers Sentry as fast as the
HTTP RTT allows -- in prod, ~5 errors/s for 12h+ on one stale slug.

A 404 from /api/0/organizations/{slug} means the stored slug is no
longer visible to the OAuth token (org renamed/deleted, membership
changed). Retrying cannot recover the name, so return ("", nil) like
the openai and intercom resolvers already do: the worker marks the
row synced, the flood stops, and the source keeps its generic name.

Other non-2xx (401/403/5xx) stay retryable so OAuth refresh and
transient outages still get another chance.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 19:22:45 +02:00
Aurélien Sibiril
036cd3306e Reuse ListSentryOrganizations in Sentry driver
SentryDriver.resolveOrgSlug duplicated the same /organizations/?member=true
call already implemented in ListSentryOrganizations, which is consumed by
the OAuth org picker. Delegating to the shared helper prevents the two
call sites from drifting (response shape, header set, pagination) and
keeps the driver focused on member listing.

Pure refactor: no behavior change. Add an httptest-backed smoke test
covering the empty-stored-slug path end-to-end through ListAccounts so
the auto-discovery flow stays exercised after the refactor.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 19:22:45 +02:00
Cursor Agent
a7639be86b Handle archived HubSpot users
Parse HubSpot archived and deactivated status signals when building
access-review account records. This preserves inactive users as
explicit records instead of dropping them when status fields differ
from the current payload assumptions.

Also accept roleIds payloads and keep records that only expose an
external ID, which is common for archived users with empty email
fields. Add a focused unit test to lock in archived-user handling.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-27 19:10:37 +00:00
Aurélien Sibiril
e18ecdda8b Consolidate connector provider dispatch behind a typed *Registry
The console previously dispatched per-provider logic through a fan of
init()-side-effect maps (driver names, OAuth2 metadata, probe URLs,
display names, settings switches), spread across pkg/connector,
pkg/accessreview/drivers and the console v1 resolvers. Adding a new
provider required edits in every one of those places and a corresponding
switch arm in CreateConnectorRequest. The same per-provider knowledge
also leaked into Helm templates as hand-rolled environment-variable
blocks per connector.

This commit collapses the dispatch surface into a single typed
*provider.Registry. The registry is constructed once by
NewBuiltinRegistry at probod startup and threaded as an explicit
dependency into every consumer (accessreview service, console v1
resolver, OAuth2 wiring). There is no package-level state. Each
provider lives in one file under pkg/connector/provider/ that exposes
a private xxxRegistration() *Registration constructor; NewBuiltinRegistry
enumerates them.

CreateConnectorRequest loses its per-provider settings fields and
takes a single RawSettings json.RawMessage produced by the
per-provider MarshalSettings closure. The 1Password SCIM bridge URL
is validated at create time (http(s) scheme + non-empty host) so a
malformed value fails fast at the resolver boundary. The Helm chart
gains probo.connectorEnv and probo.connectorSecretEntries templates
so adding a connector requires zero Helm changes. Access-review name
resolution moves into the same Registration value to keep one
authoritative dispatch table.

Tests cover every Registration (DisplayName, NewDriver wired),
Register error paths (nil, empty Provider, empty DisplayName,
duplicate), All / ProviderDisplayName / ProviderOAuth2Scopes /
ProbeURL hit and miss paths, the ApplyOAuth2Defaults templating and
PKCE branches, and ConnectorSettings[T] round-trip plus malformed-JSON
error path. The pre-refactor ApplyProviderDefaults test in
pkg/connector is replaced by the equivalent in
pkg/connector/provider.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-27 00:34:39 +02:00
Émile Ré
7ac77b85e9 Fix wsl_v5 lint errors and exclude node_modules from linting 2026-05-20 15:05:08 +04:00
Émile Ré
8ec07aa697 Add url.PathEscape to remaining url.JoinPath call sites
Escape all dynamic path segments that were previously unescaped: GitHub
org and login, Sentry orgSlug, Cloudflare accountID, DocuSign accountID,
Microsoft 365 roleID, and Tally/Sentry/GitHub name resolvers.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 13:06:30 +04:00
Émile Ré
f0fe70fe1c Restore url.PathEscape on user-supplied path segments in url.JoinPath calls
url.JoinPath does not percent-encode slashes or reserved characters in
its arguments, so user-supplied values (group IDs, slugs, team IDs) must
be wrapped with url.PathEscape to prevent path traversal. Update cursor
rule and contrib guide to codify this as a mandatory practice.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 12:57:26 +04:00
Émile Ré
74ce2bc5de Fix VCR cassette URLs after url.JoinPath refactor
The Sentry and Asana cassettes still had URLs from the old
fmt.Sprintf construction. Update them to match the output of
url.JoinPath / url.Values (no trailing slash, alphabetical
query params, percent-encoded comma).

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 12:02:04 +04:00
Émile Ré
f5703d390b Enforce Go style rules across codebase
Apply five style rules: convert iota string enums to typed
string constants, replace errors.As with errors.AsType,
merge three-group imports into two groups, fix multiline
parameter/argument formatting, and replace fmt.Sprintf URL
construction with net/url.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 11:46:39 +04:00
Émile Ré
9156d6a16a Add wsl linter and fix
Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 09:27:28 +04:00
Aurélien Sibiril
7d0dec82f6 Guard Helm connector env vars on clientId / clientSecret → Address review feedback on access-review drivers
- Guard Helm connector env vars on clientId / clientSecret
- Decode Vercel pagination cursor as *int64
- Drop Monday probe URL — no valid GET endpoint
- Address review feedback on access-review drivers

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:51 +02:00
Aurélien Sibiril
995769c4b6 Drop dead OAuth2 TokenExtraParams plumbing → Reject non-numeric ClickUp timestamps via strconv
- Drop dead OAuth2 TokenExtraParams plumbing
- Drop Deel-only x-client-id header from basic-form
- Follow Bitbucket workspace pagination cursor
- Reject non-numeric ClickUp timestamps via strconv

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:50 +02:00
Aurélien Sibiril
c2db47e698 Require CLIENT_SECRET for new access-review connectors → Drop Snyk, Ramp, Lever, Deel access-review providers
- Require CLIENT_SECRET for new access-review connectors
- Use Heroku account UUID as ExternalID
- Bump GitHub orgs picker to per_page=100
- Drop Snyk, Ramp, Lever, Deel access-review providers

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:50 +02:00
Aurélien Sibiril
ceacaea34e Inline Vercel driver URL with url.URL → Drop allowedHosts:true from vite dev server
- Inline Vercel driver URL with url.URL
- Escape URL path segments in connector name resolvers
- Dedup Vercel /v2/user fallback in name resolver
- Drop allowedHosts:true from vite dev server

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:50 +02:00
Aurélien Sibiril
7b814576df Refine Asana OAuth2 scopes → Build driver query URLs with url.URL not concatenation
- Refine Asana OAuth2 scopes
- Re-record access-review cassettes from live tokens
- Escape URL path segments in access-review drivers
- Build driver query URLs with url.URL not concatenation

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:50 +02:00
Aurélien Sibiril
f3a745c245 Add List*Organizations helpers in pkg/accessreview/drivers → Ignore .oauth-credentials.txt
- Add List*Organizations helpers in pkg/accessreview/drivers
- Replace AccessSource picker switches with map dispatch
- Drop dead per-provider connector settings wrappers
- Ignore .oauth-credentials.txt

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:49 +02:00
Aurélien Sibiril
8917b46541 Move Vercel /v2/user fetch to pkg/connector/vercel.go → Migrate connector settings callers to generic
- Move Vercel /v2/user fetch to pkg/connector/vercel.go
- Authorize NeedsConfiguration and SelectedOrganization
- Add coredata.ConnectorSettings[T] generic helper
- Migrate connector settings callers to generic

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:49 +02:00
Aurélien Sibiril
842f9cfbc5 Stop leaking customer email domain in cassette guard → Add PKCE coverage tests for entropy and replay
- Stop leaking customer email domain in cassette guard
- Deep-copy ExtraAuthParams in ApplyProviderDefaults
- Drop raw monday graphql error from returned errors
- Add PKCE coverage tests for entropy and replay

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:49 +02:00
Aurélien Sibiril
7504099507 Strip email local-part from cassette guard failure → Wire 13 access-review connectors in Helm chart
- Strip email local-part from cassette guard failure
- Drop dead src/components/connectors vite include
- Cover all 13 access-review providers in builder test
- Wire 13 access-review connectors in Helm chart

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:48 +02:00
Aurélien Sibiril
1532e94eb1 Add 13 access-review driver implementations → Dispatch new providers in review engine and name worker
- Add 13 access-review driver implementations
- Add VCR-driven driver tests with synthetic cassettes
- Guard cassettes against non-synthetic email leaks
- Dispatch new providers in review engine and name worker

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:48 +02:00
Aurélien Sibiril
0147acd9f0 Add PKCE, token-body extras, AuthURL templating to OAuth2 → Add settings structs for Pattern-2 connector providers
- Add PKCE, token-body extras, AuthURL templating to OAuth2
- Add 13 connector provider enum values
- Add scopes, display names, name resolvers for 13 providers
- Add settings structs for Pattern-2 connector providers

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-17 17:22:48 +02:00
Bryan Frimin
4336b8eb48 Remove MS365 external user from access review
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-07 17:18:17 +02:00
Bryan Frimin
6708cc901c Drop FAILED status from access review campaigns
Source fetch failures stay surfaced on the source fetch (status and
last error) instead of failing the entire campaign, so reviewers can
proceed on the sources that succeeded.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-07 12:59:55 +02:00
Bryan Frimin
5e55c888c4 Add Microsoft 365 SCIM bridge and access review driver
Microsoft 365's native SCIM endpoint is unreliable, so mirror the
Google Workspace bridge over Microsoft Graph: a new MICROSOFT_365
OAuth2 connector, a SCIM bridge provider listing /v1.0/users with
$select pagination, and an access review driver that derives admin
status from /directoryRoles members. Refactor the bridge runner to
share OAuth2 plumbing across providers and surface the new bridge
type, scopes, UI card, and bootstrap env wiring.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-06 15:41:30 +02:00
Sacha Al Himdani
8ff0a24f7a Ignore versioned client headers in VCR matcher
The Google Workspace driver test broke after the dependabot
google.golang.org/api bump (v0.269.0 → v0.277.0): the recorded cassette
embeds X-Goog-Api-Client: gdcl/0.269.0, which the upgraded SDK no
longer sends, so the default matcher rejects every interaction.

Configure the matcher to ignore User-Agent and X-Goog-Api-Client so
cassettes survive client-library version bumps.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-05 14:58:42 +02:00
Aurélien Sibiril
c43445d487 Add Notion workspace name resolver
Notion was the only wired access-review connector without a name
resolver, so the source kept the generic "Notion" placeholder. Fetch
the workspace name from /v1/users/me (bot.workspace_name) following
the same pattern as the other resolvers, and refresh the stale scope
comment now that Notion participates in name resolution.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-27 10:49:22 +02:00
Aurélien Sibiril
7934c54f42 Request Google customer directory scope for name sync
The access-review source-name worker calls Customers.Get("my_customer")
on the Google Admin SDK to resolve the Google Workspace primary domain.
That endpoint requires admin.directory.customer.readonly; without it
the request returns 403 and the source keeps the generic placeholder
name. The scope is already requested by the SCIM bridge -- align the
access-review driver with it.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-27 10:49:22 +02:00
Aurélien Sibiril
da80ce3eb1 Include all accounts in access-review fetch
Drivers whose source API surfaces an account-status signal return every
account (including inactive / suspended / deleted) and populate Active
accordingly; drivers without such a signal no longer fabricate it.

The fetch pipeline records every account with decision PENDING and no
flags. Producing flags or a non-PENDING decision is reserved to human
reviewers or a purpose-built agent run against the campaign -- the engine
must not form an implicit verdict.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-22 11:15:48 +02:00
Aurélien Sibiril
91eb41bd15 Add Slack access review scopes
Slack has two OAuth2 use cases in the app: the compliance page
integration (chat:write, channels:join, incoming-webhook) and the
access review driver that lists workspace members via users.list
(users:read, users:read.email). The per-caller scope refactor
already handled the compliance page but missed the access review
path, leaving it silently broken (zero scopes → missing_scope from
Slack API).

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-13 19:24:46 +02:00
Bryan Frimin
43336078f4 Migrate workers to kit/worker
Replace hand-rolled polling loops, semaphores, and WaitGroups
in all 7 background workers with go.gearno.de/kit/worker. Each
worker now implements Handler[T] (Claim/Process) and optionally
StaleRecoverer, gaining automatic Prometheus metrics and
OpenTelemetry tracing. Bumps kit from v0.3.0 to v0.5.0.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-13 12:25:42 +02:00
Aurélien Sibiril
56c042b7ed Add OAuth2 scope sources for access review, slack, googleworkspace
Each module that initiates an OAuth2 flow now declares its scopes
in its own package instead of duplicating them in the frontend or
in shared connector config:

- pkg/accessreview/drivers: per-provider scopes for the access
  review drivers
- pkg/slack: scopes for the compliance page integration
- pkg/iam/scim/bridge/provider/googleworkspace: scopes for the
  SCIM provisioning bridge

These constants are surfaced to the frontend via GraphQL fields
so the frontend never hardcodes scope strings.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-08 11:05:12 +02:00
Sacha Al Himdani
96cfbe19d8 Bump Go from 1.26.1 to 1.26.2
Address security vulnerabilities fixed in Go 1.26.2.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-08 09:43:13 +02:00
Bryan Frimin
f17fb7bf49 Upgrade to kit v0.3.0
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-03 11:56:06 +02:00
Aurélien Sibiril
2320e1e0be Add access source drivers
Add Driver interface and implementations for Google
Workspace, Linear, Slack, 1Password, HubSpot, DocuSign,
Notion, Brex, Tally, Cloudflare, CSV, Probo memberships,
Sentry, OpenAI, Supabase, GitHub, Intercom, and Resend.
Include name resolvers, VCR test infrastructure with
cassettes, and RFC 5988 link header parser.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-02 14:37:29 +02:00