Commit Graph

2139 Commits

Author SHA1 Message Date
Émile Ré
daabee85b4 Fix lint
Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 13:42:16 +02:00
Émile Ré
12f9dfa352 Expose HTTP cookie source through the console API
The coredata CookieSource enum and the ingestion path both support an
HTTP source, but the GraphQL CookieSource enum never declared it. The
generated marshaler is a plain map lookup with no fallback, so an HTTP
value missed the map and serialized to an empty string. The console UI
treats that empty string as falsy and rendered no source badge at all,
making HTTP-sourced trackers look sourceless.

Add the HTTP member to the GraphQL enum so the value round-trips, and
fold the duplicated tracker-type and tracker-source badge helpers from
three components into a shared @probo/helpers module, adding an explicit
HTTP label while consolidating.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 12:56:36 +02:00
Émile Ré
24bece6f86 Add tracker description enrichment worker
Tracker descriptions were only filled on the agent-identification path,
so patterns resolved by domain, sibling, or fallback stayed without one,
and empty mapping upserts could clobber a researched description on the
shared catalog row.

Move description ownership to a dedicated, global common-pattern
enrichment worker. New catalog rows are queued on insert; the worker
researches a compliance-grade description with web search, records it on
the common pattern, and fans it out to every linked tracker pattern. The
mapping worker no longer generates descriptions and only propagates an
already-enriched one at link time.

Rename TrackerMappingConfig to TrackerAgentsConfig since the mapping and
enrichment agents now share it.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 11:48:10 +02:00
Émile Ré
29791ae775 Scope mapping writes and stabilize sibling lookup
The tracker-mapping worker loaded a pattern in its claim transaction and
committed the resolution in a separate, later transaction. A full-row
Update would write back stale values and clobber any user edit made in
between. Add UpdateMapping, which writes only the worker-resolved
columns (common_tracker_pattern_id, third_party_id, and a description
filled only when still empty), leaving user-editable fields untouched.

Also add ORDER BY tracker_pattern_id to the sibling pattern lookup: the
query used LIMIT without an ORDER BY, so an over-limit match set
returned an arbitrary subset and could resolve the third party
differently across runs.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:33 +02:00
Émile Ré
62aa4a2dc4 Re-trigger mapping when a tracker source is promoted
A tracker pattern's source ratchets PRE_EXISTING -> EXTENSION -> SCRIPT
as stronger detections arrive, but that promotion was never reflected
back to the mapping pipeline. The detection that promotes the source
also brings a fresh initiator domain that matchByDomain and
matchBySiblingOrigin can use, and an EXTENSION -> SCRIPT promotion lifts
the creationAllowed gate that blocks org third-party creation. Yet the
pattern's mapping_requested_at was already cleared after its first pass,
so the worker never revisited it.

Re-arm mapping_requested_at via SetMappingRequested at each
source-promotion site (reportDetectedTracker plus the glob-merge and
adoption paths in the pattern-analysis worker). Update's SET clause does
not cover mapping_requested_at, so assigning the field before Update
would be a silent no-op; SetMappingRequested only writes when the column
is NULL, keeping already-queued patterns from being double-enqueued.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:33 +02:00
Émile Ré
8c10997681 Re-enqueue unmapped siblings after mapping
The tracker-mapping worker processes one pattern at a time and
matchBySiblingOrigin only reads already-resolved siblings, so vendor
propagation across a banner was forward-only. A sibling processed
before its peer resolved a vendor (for example, one that failed the
agent and fell back to an unmatched catalog row) was never revisited,
even once a later sibling clearly identified the same third party.

When a Process run newly establishes a common third party, re-arm
mapping_requested_at on same-banner siblings that share an initiator
domain and are still unpromoted and non-extension-sourced. The worker
re-claims them and matchBySiblingOrigin now finds the freshly mapped
pattern. Guarding on third_party_id IS NULL, mapping_requested_at IS
NULL, and a not-pre-existing common third party keeps cascades finite.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:32 +02:00
Émile Ré
c11bc57c36 Match sibling trackers on first-party origin
Sibling matching is an org-local co-occurrence signal: two patterns
served from the same origin on one banner are likely the same vendor,
even when that origin is the site's own (first-party) host. First-party
filtering only protects the global catalog (domain) match, where a
proxied tracker would otherwise hit the site owner's own entry. It now
runs solely before matchByDomain, so matchBySiblingOrigin sees the
unfiltered domains and promotion happens for patterns detected on the
banner's own origin.

Resolve the sibling's direct org third party and its catalog third party
as independent signals, so a single shared org third party no longer
short-circuits the common-pattern backfill.

Make the shared test fixtures unique per tenant: common_third_parties
and common_tracker_patterns are global with unique indexes, so parallel
tests previously collided on name, slug, and pattern. Also align the
sibling tests' stored initiator domains with production, which records
the eTLD+1.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:32 +02:00
Émile Ré
2a0523c5f2 Map trackers by sibling patterns sharing an origin
Tracker patterns detected on the same banner that share initiator
domains are a strong indicator of the same third party. Previously the
mapping worker only checked the global third-party domain catalog, so a
tracker whose domain was not registered there fell through to the
expensive LLM identification step even when a co-located pattern was
already mapped.

Add a matchBySiblingOrigin step that finds other patterns on the same
banner sharing the same initiator domains and reuses their resolved
common third party. It prefers siblings already promoted to an org
third party (the strongest signal) and falls back to siblings carrying
only a catalog link, skipping when the siblings disagree. The step runs
before the catalog domain lookup since an already-qualified sibling is
at least as reliable as a raw domain match.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:32 +02:00
Émile Ré
9dfd04b449 Filter first-party domains from tracker mapping
Tracker scripts loaded through a first-party reverse proxy (e.g.
t.probo.com proxying PostHog) share the scanned site's eTLD+1 and
were incorrectly matched against the site owner's own
CommonThirdParty entry in matchByDomain. This caused trackers like
ph_phc_* to be attributed to the site owner instead of PostHog.

Load the CookieBanner origin in Process and pass it to both
matchByDomain and identifyWithAgent. Both now filter out initiator
domains whose eTLD+1 matches the site before querying the catalog
or feeding domains to the LLM agent. The prompt is also updated to
warn about proxy domains.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:32 +02:00
Émile Ré
979486020e Backfill tracker description from common catalog
When the mapping worker resolves a CommonTrackerPattern, propagate
its description back to the org TrackerPattern if the latter is
still empty. This ensures agent-produced descriptions reach the
user-facing tracker instead of staying only in the catalog.

The Update method now covers all mutable TrackerPattern columns
including common_tracker_pattern_id and third_party_id, replacing
the removed UpdateMapping method.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:31 +02:00
Émile Ré
ed93301a1f Use subquery for common third party filter
Replace the two-step ID-materializing pattern (fetch IDs in Go, pass
as ANY(@ids)) with an IN-subquery that keeps the filtering entirely
in the database and eliminates an extra round trip. Remove the now
unused LoadIDsByCommonThirdPartyID and its service wrapper. Update
the coredata rule to clarify that subqueries for filtering are OK.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:31 +02:00
Émile Ré
44aca07de3 Guard LinkToCommon against overwriting existing catalog link
When an org third party already has a common_third_party_id set,
LinkToCommon now skips the write instead of overwriting it with a
different catalog ID. This prevents heuristic or agent false
positives from corrupting a previous, more accurate association.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:31 +02:00
Émile Ré
243c400115 Set FirstLevel true for auto-created third parties
Third parties created by the tracker mapping worker are confirmed
active on the organization's cookie banner, making them first-level
by definition. Also remove unused ptr test helpers.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:30 +02:00
Émile Ré
7f4a66b310 Skip third-party promotion for uncategorised trackers
Catalog resolution (common_tracker_pattern_id) still runs for every
pattern, but promoteThirdParty is now gated on the tracker's cookie
category: patterns still sitting in the uncategorised bucket are not
promoted to an org ThirdParty until the user moves them to a real
category, which re-triggers the worker via SetMappingRequested.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:30 +02:00
Émile Ré
a99a4dde14 Promote tracker patterns to org third parties via worker
Manual moves of a non-extension TrackerPattern lacking a ThirdPartyID
now request mapping, which the tracker-mapping worker resolves with a
four-stage pipeline: exact common_third_party_id link, heuristic
ranking, agent disambiguation, and finally CreateFromCommon. Existing
fuzzy-matched org rows are tagged with common_third_party_id so the
next promotion takes the O(1) exact-link path.

The matching primitives live in pkg/thirdparty (RankCandidates,
LinkToCommon, CreateFromCommon, ScoredCandidate, threshold constants)
so the disambiguation agent and the heuristic share one candidate
type. Cookiebanner orchestrates them; cookie-banner-specific concerns
(pattern -> common-pattern -> common-party navigation, the EXTENSION
gate, and structured logs) stay in the worker.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:30 +02:00
Émile Ré
4520b10187 Fix lint issues
Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:30 +02:00
Émile Ré
88d7961ac6 Take resolver scope from authorize, not the GID
The authorize/Authorize helpers (GraphQL and MCP) already return the
*coredata.Scope resolved from the resource's organization_id attribute,
but several resolvers discarded it and rebuilt the scope with
coredata.NewScopeFromObjectID(...) right after. NewScopeFromObjectID
only reads the tenant encoded in the GID, while the authorizer derives
the scope from loaded resource attributes, so the two silently drift if
the resource lookup ever changes.

Capture scope from authorize and feed it straight to the service/coredata
layer. For the LinkX/UnlinkX MCP tools, move the per-case Authorize
inside the switch and drop the shared scope so each case owns its own
authorization result. Document the rule in contrib/claude/authorization.md
and add a matching .cursor/rules/go-authorize-scope.mdc, including the
narrow exception for global-catalog authorize calls (e.g. identity-scoped
ActionCommonThirdPartyList) where downstream services take no scope.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:29 +02:00
Émile Ré
66ffcf3411 Filter banner trackers by linked third party
The trackers list page on the cookie-banner configuration screen needs
to filter rows by the third party that the pattern resolves to. The
tricky part is that "third party" comes from two unrelated tables:
ThirdParty (org-scoped, linked through tracker_patterns.third_party_id)
and CommonThirdParty (global catalog, reached indirectly through
common_tracker_patterns.common_third_party_id). The filter must accept
either flavour of GID and resolve transparently.

Add a single thirdPartyId field to TrackerPatternFilter and dispatch
on the GID's entity-type prefix at the resolver:

  ThirdPartyEntityType       -> WithThirdPartyID
  CommonThirdPartyEntityType -> resolve common_tracker_pattern_id list
                                via the cookiebanner service, then
                                WithCommonTrackerPatternIDs

Any other entity type returns an Invalid error rather than silently
matching everything; an unknown caller-supplied GID is a contract bug.
The empty-but-non-nil ID slice produced when a CommonThirdParty has no
patterns yet correctly yields zero rows because the SQL fragment uses
ANY(...).

To populate the filter combobox, add a new CookieBanner.linkedThirdParties
field returning [TrackerPatternThirdPartyLink!]!, a union of ThirdParty
and CommonThirdParty. The resolver collects DISTINCT third_party_id and
common_tracker_pattern_id from the banner's tracker patterns (no joins,
per coredata convention), then chains the catalog lookup through
CommonTrackerPatterns -> CommonThirdParties. Authorization scopes
follow the existing trackerPattern resolvers: ActionTrackerPatternList
gates the aggregation, ActionThirdPartyGet and ActionCommonThirdPartyGet
each gate their respective fan-out only when that branch has work.

The org-scoped fan-out uses dataloadgen.LoadAll so it batches in a
single round-trip; per-key NotFound errors are dropped (a deleted
third party doesn't fail the whole list), other errors bubble up.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:29 +02:00
Émile Ré
d93ff7ba25 Surface third party links on TrackerPattern in GraphQL
Each tracker pattern carries either a direct org-scoped third_party_id
or an indirect link via common_tracker_pattern_id, but the console API
never surfaced either. Expose two optional resolver-driven fields on
the GraphQL TrackerPattern node:

  thirdParty: ThirdParty
  commonThirdParty: CommonThirdParty

The org-scoped ThirdParty takes priority. When ThirdPartyID is set the
commonThirdParty resolver short-circuits to nil, so the chained
common_tracker_pattern -> common_third_party lookup is only paid for
when a pattern has not been promoted to a tenant-managed third party.

To make the resolver pattern viable across paginated banner trackers
listings, the model now uses @goModel and a custom struct that carries
the foreign-key handles (ThirdPartyID, CommonTrackerPatternID) without
exposing them in the schema. NewTrackerPatternNode populates them from
coredata.

Two new request-scoped dataloaders (CommonTrackerPattern,
CommonThirdParty) batch the chained lookup, mirroring the existing
ThirdParty / CookieCategory loaders. The console mux now wires the
third-party service through dataloader.NewMiddleware so the second
loader has its backing service.

Authorization follows existing precedent: ActionThirdPartyGet for the
org-scoped lookup, ActionCommonThirdPartyGet (granted by the
identity-scoped CommonThirdPartyCatalogPolicy) for the catalog lookup.
ErrResourceNotFound and dataloadgen.ErrNotFound are mapped to a null
field rather than an error.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:29 +02:00
Émile Ré
cdd7eb1171 Add coredata batch loaders for tracker third parties
Surface the third-party links carried by tracker patterns (org-scoped
ThirdParty via third_party_id, or global CommonThirdParty via
common_tracker_pattern_id) requires three new batch loaders and two
filter dimensions, all kept inside their owning entity tables to honour
the no-cross-entity-JOIN rule.

  * CommonTrackerPatterns gains LoadByIDs and the ID-only
    LoadIDsByCommonThirdPartyID helper, which lets callers translate a
    common third party into a set of common_tracker_pattern_id values
    without ever JOINing against tracker_patterns.

  * CommonThirdParties gains LoadByIDs.

  * TrackerPatterns gains LoadDistinctThirdPartyIDsByCookieBannerID and
    LoadDistinctCommonTrackerPatternIDsByCookieBannerID, used by the
    upcoming CookieBanner.linkedThirdParties resolver to enumerate the
    third parties referenced in a banner.

  * TrackerPatternFilter gains thirdPartyID and commonTrackerPatternIDs
    filter dimensions; the GraphQL layer will dispatch a single
    thirdPartyId argument to the right one based on the GID entity-type
    prefix.

Service-layer wrappers (cookiebanner.GetCommonTrackerPatternsByIDs,
cookiebanner.LoadCommonTrackerPatternIDsByCommonThirdPartyID,
cookiebanner.LoadDistinctThirdPartyIDsByCookieBannerID,
cookiebanner.LoadDistinctCommonTrackerPatternIDsByCookieBannerID, and
thirdparty.GetCommonThirdPartiesByIDs) expose the new loaders to the
console resolvers and dataloaders that follow.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:28 +02:00
Bryan Frimin
36bd377d47 Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 23:42:35 -07:00
Bryan Frimin
0e40938158 Fix metabase list user
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 20:12:24 -07:00
Bryan Frimin
1853e5af39 Fix posthig resolver name
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 19:42:05 -07:00
Bryan Frimin
1f9ce02f10 Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 18:44:40 -07:00
Cursor Agent
93ec0d73da Fix last owner demotion regression
Use the Connect schema's profile connection in the e2e regression so the test can find the sole owner's membership. Adjust whitespace around the resolver error path to satisfy Go lint.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-28 18:37:44 -07:00
Cursor Agent
e9bcdc85f4 Reject last owner demotion
Return a conflict when membership role updates would demote the final active owner in an organization.

Add an end-to-end regression that verifies the mutation fails and leaves the owner role intact.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-28 18:37:44 -07:00
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
Bryan Frimin
fcd7d68778 Fix wsl_v5 lint: add blank line between if blocks in createSignatureRequestInTx
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 18:19:37 -07:00
Bryan Frimin
65bfaa9f51 Cancel signature requests when a contract ends
When UpdateUser sets a contract end date that is already in the past,
the user can no longer fulfill outstanding signature requests. Delete
their still-pending requests as part of the same update so they stop
appearing as awaiting signatures.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 18:12:40 -07:00
Bryan Frimin
1a8ca29264 Scope document signatures to the major version
A signature applies to a whole major: minor publishes keep it and the
export unions signatures across every minor of the major. The request
guard was scoped to a single minor, so re-requesting on a newer minor
(or twice on the same version) inserted duplicate rows and a signatory
appeared several times on the exported signature page.

Deduplicate by loading any existing signature across the major before
inserting, cancel still-pending requests from prior majors when a new
major is published, and restrict the export to active signatories
(comparing contract end dates against the current date). A migration
collapses the duplicate rows already in the table, preferring a signed
row over a pending one and then the most recent.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-28 18:12:32 -07:00
Aurélien Sibiril
bd83799fdd Register Cursor access-review connector
Wire Cursor into the connector-provider registry as an API-key,
single-tenant connector using HTTP Basic auth, with no picker,
settings, or name resolver. Add the CURSOR enum value, its
migration, and the GraphQL enum binding so the provider is
accepted across the API surface.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-29 00:05:48 +02: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
Aurélien Sibiril
0149ca4f55 Support HTTP Basic auth in API-key connections
Cursor's Admin API authenticates with the admin key as the HTTP
Basic auth username (empty password) and rejects Bearer tokens.
The API-key connection previously supported only Bearer and a
custom header (Anthropic's x-api-key); add a Basic-auth mode
selected by Registration.APIKeyBasicAuth, and reject providers
that set both it and APIKeyHeader.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-29 00:05:48 +02:00
Cursor Agent
72c35a9967 Inline signature delete error check
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 14:58:01 -07:00
Cursor Agent
f7309c642a Cancel signature requests on deactivation
Disabling a profile now removes pending document signature requests tied to that signatory. This keeps inactive accounts from retaining signature work across manual, archived, and SCIM deactivation flows.

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

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 14:58:01 -07: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
83b48333d8 Register Anthropic access-review connector
Add the ANTHROPIC connector_provider enum value, its migration, and the
GraphQL enum binding, then register the provider as an API-key connector
that authenticates via x-api-key.

The probe URL is left empty because the shared connection probe cannot
send the required anthropic-version header and would misreport a valid
key; a dead key surfaces on the first member fetch instead.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-05-28 22:33:29 +02: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
f6510e25a1 Support a custom API-key auth header in connections
The API-key connection always presented the key as Authorization:
Bearer. Some providers, notably Anthropic, require the key in a custom
header such as x-api-key and reject Bearer auth, returning 400 when
both headers are present.

Add an optional Header field on APIKeyConnection (empty preserves the
Bearer default for every existing provider) served by a small transport
that omits Authorization, plus a registry-declared APIKeyHeader so the
create-connector resolver wires the right scheme per provider.

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
e467060056 Allow empty team for the Heroku connector driver
The driver now runs in personal mode when no team is configured, so the
team_id-required guard would block solo accounts from ever building a
driver. Drop it and let an empty or personal-account slug flow through.

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