diff --git a/packages/ui/src/Atoms/ThirdParties/Apollo.tsx b/packages/ui/src/Atoms/ThirdParties/Apollo.tsx
new file mode 100644
index 000000000..47bb447b9
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/Apollo.tsx
@@ -0,0 +1,13 @@
+import type { ComponentProps } from "react";
+
+export function Apollo(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/ClickHouse.tsx b/packages/ui/src/Atoms/ThirdParties/ClickHouse.tsx
new file mode 100644
index 000000000..c8852fd59
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/ClickHouse.tsx
@@ -0,0 +1,9 @@
+import type { ComponentProps } from "react";
+
+export function ClickHouse(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/Deepgram.tsx b/packages/ui/src/Atoms/ThirdParties/Deepgram.tsx
new file mode 100644
index 000000000..c43dc844f
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/Deepgram.tsx
@@ -0,0 +1,9 @@
+import type { ComponentProps } from "react";
+
+export function Deepgram(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/Langfuse.tsx b/packages/ui/src/Atoms/ThirdParties/Langfuse.tsx
new file mode 100644
index 000000000..8ce644370
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/Langfuse.tsx
@@ -0,0 +1,15 @@
+import type { ComponentProps } from "react";
+
+export function Langfuse(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/Mercury.tsx b/packages/ui/src/Atoms/ThirdParties/Mercury.tsx
new file mode 100644
index 000000000..ff686287f
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/Mercury.tsx
@@ -0,0 +1,9 @@
+import type { ComponentProps } from "react";
+
+export function Mercury(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
index 910148feb..a2318c2c0 100644
--- a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
+++ b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
@@ -15,15 +15,18 @@
import type { ComponentProps, FC } from "react";
import { Anthropic } from "./Anthropic";
+import { Apollo } from "./Apollo";
import { Asana } from "./Asana";
import { BetterStack } from "./BetterStack";
import { Bitbucket } from "./Bitbucket";
import { Brex } from "./Brex";
import { Clerk } from "./Clerk";
+import { ClickHouse } from "./ClickHouse";
import { ClickUp } from "./ClickUp";
import { Cloudflare } from "./Cloudflare";
import { Cursor } from "./Cursor";
import { Datadog } from "./Datadog";
+import { Deepgram } from "./Deepgram";
import { DocuSign } from "./DocuSign";
import { Figma } from "./Figma";
import { GitHub } from "./GitHub";
@@ -33,7 +36,9 @@ import { Grafana } from "./Grafana";
import { Heroku } from "./Heroku";
import { HubSpot } from "./HubSpot";
import { Intercom } from "./Intercom";
+import { Langfuse } from "./Langfuse";
import { Linear } from "./Linear";
+import { Mercury } from "./Mercury";
import { Metabase } from "./Metabase";
import { Microsoft } from "./Microsoft";
import { Monday } from "./Monday";
@@ -60,15 +65,18 @@ import { Zendesk } from "./Zendesk";
const thirdParties: Record>> = {
ANTHROPIC: Anthropic,
+ APOLLO: Apollo,
ASANA: Asana,
BETTER_STACK: BetterStack,
BITBUCKET: Bitbucket,
BREX: Brex,
CLERK: Clerk,
+ CLICKHOUSE: ClickHouse,
CLICKUP: ClickUp,
CLOUDFLARE: Cloudflare,
CURSOR: Cursor,
DATADOG: Datadog,
+ DEEPGRAM: Deepgram,
DOCUSIGN: DocuSign,
FIGMA: Figma,
GITHUB: GitHub,
@@ -79,7 +87,9 @@ const thirdParties: Record>> = {
HEROKU: Heroku,
HUBSPOT: HubSpot,
INTERCOM: Intercom,
+ LANGFUSE: Langfuse,
LINEAR: Linear,
+ MERCURY: Mercury,
METABASE: Metabase,
MICROSOFT: Microsoft,
MICROSOFT_365: Microsoft,
diff --git a/packages/ui/src/Atoms/ThirdParties/index.ts b/packages/ui/src/Atoms/ThirdParties/index.ts
index 8a1128f57..4b8c370df 100644
--- a/packages/ui/src/Atoms/ThirdParties/index.ts
+++ b/packages/ui/src/Atoms/ThirdParties/index.ts
@@ -1,13 +1,16 @@
export { Anthropic } from "./Anthropic";
+export { Apollo } from "./Apollo";
export { Asana } from "./Asana";
export { BetterStack } from "./BetterStack";
export { Bitbucket } from "./Bitbucket";
export { Brex } from "./Brex";
export { Clerk } from "./Clerk";
+export { ClickHouse } from "./ClickHouse";
export { ClickUp } from "./ClickUp";
export { Cloudflare } from "./Cloudflare";
export { Cursor } from "./Cursor";
export { Datadog } from "./Datadog";
+export { Deepgram } from "./Deepgram";
export { DocuSign } from "./DocuSign";
export { Figma } from "./Figma";
export { GitHub } from "./GitHub";
@@ -17,7 +20,9 @@ export { Grafana } from "./Grafana";
export { Heroku } from "./Heroku";
export { HubSpot } from "./HubSpot";
export { Intercom } from "./Intercom";
+export { Langfuse } from "./Langfuse";
export { Linear } from "./Linear";
+export { Mercury } from "./Mercury";
export { Metabase } from "./Metabase";
export { Microsoft } from "./Microsoft";
export { Monday } from "./Monday";
diff --git a/pkg/accessreview/drivers/apollo.go b/pkg/accessreview/drivers/apollo.go
new file mode 100644
index 000000000..c58efdc24
--- /dev/null
+++ b/pkg/accessreview/drivers/apollo.go
@@ -0,0 +1,196 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+const (
+ apolloUsersEndpoint = "https://api.apollo.io/api/v1/users/search"
+ apolloUsersPageSize = 100
+)
+
+// ApolloDriver lists the teammates (seats) of a single Apollo.io account.
+// The master API key is bound to one account, so GET /api/v1/users/search
+// returns every teammate of that account. The key is presented in the
+// x-api-key header by the connection transport, not here.
+type ApolloDriver struct {
+ httpClient *http.Client
+}
+
+var _ Driver = (*ApolloDriver)(nil)
+
+type apolloUser struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Email string `json:"email"`
+ // Role is Apollo's permission-profile name (e.g. "Admin", "Billing and
+ // Seat Manager"). It is decoded as RawMessage and read via apolloRole so
+ // a non-string shape (object/null on some plans) degrades to an empty
+ // role instead of failing the decode of the whole page.
+ Role json.RawMessage `json:"role"`
+}
+
+type apolloUsersResponse struct {
+ Users []apolloUser `json:"users"`
+ Pagination struct {
+ Page int `json:"page"`
+ PerPage int `json:"per_page"`
+ TotalPages int `json:"total_pages"`
+ TotalEntries int `json:"total_entries"`
+ } `json:"pagination"`
+}
+
+func NewApolloDriver(httpClient *http.Client) *ApolloDriver {
+ return &ApolloDriver{httpClient: httpClient}
+}
+
+func (d *ApolloDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
+ var records []AccountRecord
+
+ for page := 1; page <= maxPaginationPages; page++ {
+ resp, err := d.fetchUsersPage(ctx, page)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, u := range resp.Users {
+ email := strings.TrimSpace(u.Email)
+ if email == "" {
+ continue
+ }
+
+ role := apolloRole(u.Role)
+
+ records = append(records, AccountRecord{
+ Email: email,
+ FullName: apolloFullName(u, email),
+ Roles: apolloRoles(role),
+ IsAdmin: apolloIsAdmin(role),
+ MFAStatus: coredata.MFAStatusUnknown,
+ AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
+ AccountType: coredata.AccessReviewEntryAccountTypeUser,
+ ExternalID: strings.TrimSpace(u.ID),
+ })
+ }
+
+ if resp.Pagination.TotalPages <= page || len(resp.Users) == 0 {
+ return records, nil
+ }
+ }
+
+ return nil, fmt.Errorf("cannot list all apollo users: %w", ErrPaginationLimitReached)
+}
+
+func (d *ApolloDriver) fetchUsersPage(ctx context.Context, page int) (*apolloUsersResponse, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, apolloUsersEndpoint, nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create apollo users request: %w", err)
+ }
+
+ q := req.URL.Query()
+ q.Set("page", strconv.Itoa(page))
+ q.Set("per_page", strconv.Itoa(apolloUsersPageSize))
+ req.URL.RawQuery = q.Encode()
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("cannot execute apollo users request: %w", err)
+ }
+
+ defer func() {
+ _ = httpResp.Body.Close()
+ }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return nil, fmt.Errorf("cannot fetch apollo users: unexpected status %d", httpResp.StatusCode)
+ }
+
+ var resp apolloUsersResponse
+ if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
+ return nil, fmt.Errorf("cannot decode apollo users response: %w", err)
+ }
+
+ return &resp, nil
+}
+
+func apolloFullName(u apolloUser, fallback string) string {
+ if name := strings.TrimSpace(u.Name); name != "" {
+ return name
+ }
+
+ combined := strings.TrimSpace(strings.TrimSpace(u.FirstName) + " " + strings.TrimSpace(u.LastName))
+ if combined != "" {
+ return combined
+ }
+
+ return fallback
+}
+
+// apolloRole extracts a role label from Apollo's `role` field, which is
+// normally a plain string. It also tolerates a `{"name": ...}` object and a
+// null/absent value so an unexpected shape on some plans yields an empty
+// role rather than failing the whole page decode.
+func apolloRole(raw json.RawMessage) string {
+ if len(raw) == 0 {
+ return ""
+ }
+
+ var s string
+ if err := json.Unmarshal(raw, &s); err == nil {
+ return strings.TrimSpace(s)
+ }
+
+ var obj struct {
+ Name string `json:"name"`
+ }
+ if err := json.Unmarshal(raw, &obj); err == nil {
+ return strings.TrimSpace(obj.Name)
+ }
+
+ return ""
+}
+
+// apolloRoles wraps a single Apollo permission-profile name as the account's
+// role set, returning an empty slice when no profile is present.
+func apolloRoles(role string) []string {
+ if role == "" {
+ return []string{}
+ }
+
+ return []string{role}
+}
+
+// apolloIsAdmin reports whether a permission-profile name is Apollo's
+// built-in super-admin profile ("Admin"). Apollo exposes no boolean admin
+// flag and lets customers name custom profiles freely, so the match is exact
+// (case-insensitive), not a substring: a profile merely containing "admin"
+// (e.g. "Billing Admin") is not auto-classified — its Role is still surfaced
+// for the reviewer to judge.
+func apolloIsAdmin(role string) bool {
+ return strings.EqualFold(strings.TrimSpace(role), "Admin")
+}
diff --git a/pkg/accessreview/drivers/apollo_test.go b/pkg/accessreview/drivers/apollo_test.go
new file mode 100644
index 000000000..f3a0da97b
--- /dev/null
+++ b/pkg/accessreview/drivers/apollo_test.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestApolloDriver(t *testing.T) {
+ t.Parallel()
+
+ rec := newRecorder(t, "testdata/apollo", "APOLLO_API_KEY")
+ // Apollo authenticates via the x-api-key header, not Authorization.
+ client := newVCRClientWithHeader(rec, "x-api-key", os.Getenv("APOLLO_API_KEY"))
+
+ driver := NewApolloDriver(client)
+ records, err := driver.ListAccounts(context.Background())
+ require.NoError(t, err)
+ require.Len(t, records, 4)
+
+ admin := records[0]
+ assert.Equal(t, "5f0000000000000000000001", admin.ExternalID)
+ assert.Equal(t, "alice@example.com", admin.Email)
+ assert.Equal(t, "Alice Admin", admin.FullName)
+ assert.Equal(t, []string{"Admin"}, admin.Roles)
+ assert.True(t, admin.IsAdmin)
+ assert.Equal(t, coredata.AccessReviewEntryAccountTypeUser, admin.AccountType)
+
+ rep := records[1]
+ assert.Equal(t, []string{"Sales Rep"}, rep.Roles)
+ assert.False(t, rep.IsAdmin)
+
+ manager := records[2]
+ assert.Equal(t, []string{"Billing and Seat Manager"}, manager.Roles)
+ assert.False(t, manager.IsAdmin)
+
+ // No name and no first/last: the display name falls back to the email.
+ noName := records[3]
+ assert.Equal(t, "dave@example.com", noName.Email)
+ assert.Equal(t, "dave@example.com", noName.FullName)
+}
+
+func TestApolloIsAdmin(t *testing.T) {
+ t.Parallel()
+
+ assert.True(t, apolloIsAdmin("Admin"))
+ assert.True(t, apolloIsAdmin("admin"))
+ // Exact match only: profiles that merely contain "admin" are not admins.
+ assert.False(t, apolloIsAdmin("Master Admin"))
+ assert.False(t, apolloIsAdmin("Billing Admin"))
+ assert.False(t, apolloIsAdmin("Sales Rep"))
+}
diff --git a/pkg/accessreview/drivers/clickhouse.go b/pkg/accessreview/drivers/clickhouse.go
new file mode 100644
index 000000000..b325f1e66
--- /dev/null
+++ b/pkg/accessreview/drivers/clickhouse.go
@@ -0,0 +1,224 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+const clickhouseAPIBaseURL = "https://api.clickhouse.cloud"
+
+// ClickHouseDriver lists the members of a single ClickHouse Cloud
+// organization. A key/secret pair (HTTP Basic) is scoped to exactly one
+// organization, so the driver discovers that organization via
+// GET /v1/organizations and then lists its members — no org ID needs to be
+// configured. The Basic credential is applied by the connection transport.
+type ClickHouseDriver struct {
+ httpClient *http.Client
+}
+
+var _ Driver = (*ClickHouseDriver)(nil)
+
+type clickhouseOrgsResponse struct {
+ Result []struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ } `json:"result"`
+}
+
+type clickhouseMembersResponse struct {
+ Result []clickhouseMember `json:"result"`
+}
+
+type clickhouseMember struct {
+ UserID string `json:"userId"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Role string `json:"role"`
+ JoinedAt string `json:"joinedAt"`
+ AssignedRoles []struct {
+ RoleName string `json:"roleName"`
+ } `json:"assignedRoles"`
+}
+
+func NewClickHouseDriver(httpClient *http.Client) *ClickHouseDriver {
+ return &ClickHouseDriver{httpClient: httpClient}
+}
+
+func (d *ClickHouseDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
+ organizationID, err := d.resolveOrganizationID(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ members, err := d.fetchMembers(ctx, organizationID)
+ if err != nil {
+ return nil, err
+ }
+
+ records := make([]AccountRecord, 0, len(members))
+
+ for _, m := range members {
+ email := strings.TrimSpace(m.Email)
+ if email == "" {
+ continue
+ }
+
+ record := AccountRecord{
+ Email: email,
+ FullName: clickhouseFullName(m, email),
+ Roles: clickhouseRoles(m),
+ IsAdmin: clickhouseIsAdmin(m),
+ MFAStatus: coredata.MFAStatusUnknown,
+ AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
+ AccountType: coredata.AccessReviewEntryAccountTypeUser,
+ ExternalID: strings.TrimSpace(m.UserID),
+ }
+
+ if m.JoinedAt != "" {
+ if t, err := time.Parse(time.RFC3339, m.JoinedAt); err == nil {
+ record.CreatedAt = &t
+ }
+ }
+
+ records = append(records, record)
+ }
+
+ return records, nil
+}
+
+func (d *ClickHouseDriver) resolveOrganizationID(ctx context.Context) (string, error) {
+ endpoint, err := url.JoinPath(clickhouseAPIBaseURL, "v1", "organizations")
+ if err != nil {
+ return "", fmt.Errorf("cannot build clickhouse organizations URL: %w", err)
+ }
+
+ var resp clickhouseOrgsResponse
+ if err := d.getJSON(ctx, endpoint, "clickhouse organizations", &resp); err != nil {
+ return "", err
+ }
+
+ if len(resp.Result) == 0 || strings.TrimSpace(resp.Result[0].ID) == "" {
+ return "", fmt.Errorf("cannot determine clickhouse organization: API key is not associated with any organization")
+ }
+
+ return strings.TrimSpace(resp.Result[0].ID), nil
+}
+
+func (d *ClickHouseDriver) fetchMembers(ctx context.Context, organizationID string) ([]clickhouseMember, error) {
+ endpoint, err := url.JoinPath(clickhouseAPIBaseURL, "v1", "organizations", url.PathEscape(organizationID), "members")
+ if err != nil {
+ return nil, fmt.Errorf("cannot build clickhouse members URL: %w", err)
+ }
+
+ var resp clickhouseMembersResponse
+ if err := d.getJSON(ctx, endpoint, "clickhouse members", &resp); err != nil {
+ return nil, err
+ }
+
+ return resp.Result, nil
+}
+
+func (d *ClickHouseDriver) getJSON(ctx context.Context, endpoint, what string, out any) error {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return fmt.Errorf("cannot create %s request: %w", what, err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("cannot execute %s request: %w", what, err)
+ }
+
+ defer func() {
+ _ = httpResp.Body.Close()
+ }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return fmt.Errorf("cannot fetch %s: unexpected status %d", what, httpResp.StatusCode)
+ }
+
+ if err := json.NewDecoder(httpResp.Body).Decode(out); err != nil {
+ return fmt.Errorf("cannot decode %s response: %w", what, err)
+ }
+
+ return nil
+}
+
+func clickhouseFullName(m clickhouseMember, fallback string) string {
+ if name := strings.TrimSpace(m.Name); name != "" {
+ return name
+ }
+
+ return fallback
+}
+
+// clickhouseRoles prefers the custom/system roles in assignedRoles (the live
+// source of truth) and falls back to the deprecated `role` field, which is
+// frozen for organizations migrated to custom roles.
+func clickhouseRoles(m clickhouseMember) []string {
+ names := make([]string, 0, len(m.AssignedRoles))
+
+ for _, r := range m.AssignedRoles {
+ if name := strings.TrimSpace(r.RoleName); name != "" {
+ names = append(names, name)
+ }
+ }
+
+ if len(names) > 0 {
+ return names
+ }
+
+ switch strings.ToLower(strings.TrimSpace(m.Role)) {
+ case "admin":
+ return []string{"Admin"}
+ case "developer":
+ return []string{"Developer"}
+ default:
+ if r := strings.TrimSpace(m.Role); r != "" {
+ return []string{r}
+ }
+
+ return []string{}
+ }
+}
+
+func clickhouseIsAdmin(m clickhouseMember) bool {
+ // assignedRoles is the live source of truth. The deprecated role field is
+ // frozen at its pre-migration value for organizations that moved to custom
+ // roles, so consult it only when there are no assigned roles — otherwise a
+ // stale "admin" could misclassify a since-demoted member.
+ if len(m.AssignedRoles) > 0 {
+ for _, r := range m.AssignedRoles {
+ if strings.EqualFold(strings.TrimSpace(r.RoleName), "admin") {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ return strings.EqualFold(strings.TrimSpace(m.Role), "admin")
+}
diff --git a/pkg/accessreview/drivers/clickhouse_test.go b/pkg/accessreview/drivers/clickhouse_test.go
new file mode 100644
index 000000000..a1db0a6b2
--- /dev/null
+++ b/pkg/accessreview/drivers/clickhouse_test.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestClickHouseDriver(t *testing.T) {
+ t.Parallel()
+
+ rec := newRecorder(t, "testdata/clickhouse", "CLICKHOUSE_API_KEY")
+ // ClickHouse Cloud authenticates with HTTP Basic (keyId:keySecret). The
+ // matcher ignores Authorization, so replay needs no auth.
+ client := newVCRClient(rec, basicAuthUserPass(os.Getenv("CLICKHOUSE_API_KEY")))
+
+ driver := NewClickHouseDriver(client)
+ records, err := driver.ListAccounts(context.Background())
+ require.NoError(t, err)
+ require.Len(t, records, 3)
+
+ admin := records[0]
+ assert.Equal(t, "u-0000-0001", admin.ExternalID)
+ assert.Equal(t, "admin@example.com", admin.Email)
+ assert.Equal(t, "Admin User", admin.FullName)
+ assert.Equal(t, []string{"Admin"}, admin.Roles)
+ assert.True(t, admin.IsAdmin)
+ assert.NotNil(t, admin.CreatedAt)
+ assert.Equal(t, coredata.AccessReviewEntryAccountTypeUser, admin.AccountType)
+
+ dev := records[1]
+ assert.Equal(t, []string{"Developer"}, dev.Roles)
+ assert.False(t, dev.IsAdmin)
+
+ // assignedRoles wins over the deprecated role field; a custom role
+ // merely containing "Admin" is not promoted to admin.
+ custom := records[2]
+ assert.Equal(t, "custom@example.com", custom.FullName)
+ assert.Equal(t, []string{"Billing Admin"}, custom.Roles)
+ assert.False(t, custom.IsAdmin)
+}
diff --git a/pkg/accessreview/drivers/deepgram.go b/pkg/accessreview/drivers/deepgram.go
new file mode 100644
index 000000000..74df2db6c
--- /dev/null
+++ b/pkg/accessreview/drivers/deepgram.go
@@ -0,0 +1,244 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+const deepgramAPIBaseURL = "https://api.deepgram.com"
+
+// DeepgramDriver lists the members of every Deepgram project the API key
+// can access. The key (presented in the `Authorization: Token `
+// scheme by the connection transport) is scoped to a single account, whose
+// members may span several projects; the driver aggregates them and dedupes
+// by member_id, unioning each member's per-project scopes.
+type DeepgramDriver struct {
+ httpClient *http.Client
+}
+
+var _ Driver = (*DeepgramDriver)(nil)
+
+type deepgramProject struct {
+ ProjectID string `json:"project_id"`
+ Name string `json:"name"`
+}
+
+type deepgramProjectsResponse struct {
+ Projects []deepgramProject `json:"projects"`
+}
+
+type deepgramMember struct {
+ MemberID string `json:"member_id"`
+ Email string `json:"email"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Scopes []string `json:"scopes"`
+}
+
+type deepgramMembersResponse struct {
+ Members []deepgramMember `json:"members"`
+}
+
+func NewDeepgramDriver(httpClient *http.Client) *DeepgramDriver {
+ return &DeepgramDriver{httpClient: httpClient}
+}
+
+func (d *DeepgramDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
+ projects, err := d.fetchProjects(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ // Aggregate members across projects, preserving first-seen order and
+ // unioning the scopes a member holds in each project.
+ order := make([]string, 0)
+ merged := make(map[string]*deepgramMember)
+
+ for _, project := range projects {
+ members, err := d.fetchProjectMembers(ctx, project.ProjectID)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, m := range members {
+ existing, ok := merged[m.MemberID]
+ if !ok {
+ copied := m
+ merged[m.MemberID] = &copied
+ order = append(order, m.MemberID)
+
+ continue
+ }
+
+ existing.Scopes = deepgramUnionScopes(existing.Scopes, m.Scopes)
+ }
+ }
+
+ records := make([]AccountRecord, 0, len(order))
+
+ for _, id := range order {
+ m := merged[id]
+
+ email := strings.TrimSpace(m.Email)
+ if email == "" {
+ continue
+ }
+
+ records = append(records, AccountRecord{
+ Email: email,
+ FullName: deepgramFullName(*m, email),
+ Roles: deepgramRoles(m.Scopes),
+ IsAdmin: deepgramIsAdmin(m.Scopes),
+ MFAStatus: coredata.MFAStatusUnknown,
+ AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
+ AccountType: coredata.AccessReviewEntryAccountTypeUser,
+ ExternalID: strings.TrimSpace(m.MemberID),
+ })
+ }
+
+ return records, nil
+}
+
+func (d *DeepgramDriver) fetchProjects(ctx context.Context) ([]deepgramProject, error) {
+ endpoint, err := url.JoinPath(deepgramAPIBaseURL, "v1", "projects")
+ if err != nil {
+ return nil, fmt.Errorf("cannot build deepgram projects URL: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create deepgram projects request: %w", err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("cannot execute deepgram projects request: %w", err)
+ }
+
+ defer func() {
+ _ = httpResp.Body.Close()
+ }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return nil, fmt.Errorf("cannot fetch deepgram projects: unexpected status %d", httpResp.StatusCode)
+ }
+
+ var resp deepgramProjectsResponse
+ if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
+ return nil, fmt.Errorf("cannot decode deepgram projects response: %w", err)
+ }
+
+ return resp.Projects, nil
+}
+
+func (d *DeepgramDriver) fetchProjectMembers(ctx context.Context, projectID string) ([]deepgramMember, error) {
+ endpoint, err := url.JoinPath(deepgramAPIBaseURL, "v1", "projects", url.PathEscape(projectID), "members")
+ if err != nil {
+ return nil, fmt.Errorf("cannot build deepgram members URL: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create deepgram members request: %w", err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("cannot execute deepgram members request: %w", err)
+ }
+
+ defer func() {
+ _ = httpResp.Body.Close()
+ }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return nil, fmt.Errorf("cannot fetch deepgram members: unexpected status %d", httpResp.StatusCode)
+ }
+
+ var resp deepgramMembersResponse
+ if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
+ return nil, fmt.Errorf("cannot decode deepgram members response: %w", err)
+ }
+
+ return resp.Members, nil
+}
+
+func deepgramFullName(m deepgramMember, fallback string) string {
+ fullName := strings.TrimSpace(strings.TrimSpace(m.FirstName) + " " + strings.TrimSpace(m.LastName))
+ if fullName != "" {
+ return fullName
+ }
+
+ return fallback
+}
+
+func deepgramUnionScopes(a, b []string) []string {
+ seen := make(map[string]bool, len(a))
+ out := make([]string, 0, len(a)+len(b))
+
+ for _, scopes := range [][]string{a, b} {
+ for _, s := range scopes {
+ if seen[s] {
+ continue
+ }
+
+ seen[s] = true
+
+ out = append(out, s)
+ }
+ }
+
+ return out
+}
+
+// deepgramRoles derives a role label from a member's scopes. Deepgram has no
+// dedicated role field; ownership/administration is expressed through the
+// scope list. Unknown scope sets fall back to "Member".
+func deepgramRoles(scopes []string) []string {
+ switch {
+ case deepgramHasScope(scopes, "owner"):
+ return []string{"Owner"}
+ case deepgramHasScope(scopes, "admin"):
+ return []string{"Admin"}
+ default:
+ return []string{"Member"}
+ }
+}
+
+func deepgramIsAdmin(scopes []string) bool {
+ return deepgramHasScope(scopes, "owner") || deepgramHasScope(scopes, "admin")
+}
+
+func deepgramHasScope(scopes []string, want string) bool {
+ for _, s := range scopes {
+ if strings.EqualFold(strings.TrimSpace(s), want) {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/pkg/accessreview/drivers/deepgram_test.go b/pkg/accessreview/drivers/deepgram_test.go
new file mode 100644
index 000000000..ad4cbe2a5
--- /dev/null
+++ b/pkg/accessreview/drivers/deepgram_test.go
@@ -0,0 +1,85 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestDeepgramDriver(t *testing.T) {
+ t.Parallel()
+
+ rec := newRecorder(t, "testdata/deepgram", "DEEPGRAM_API_KEY")
+ // Deepgram authenticates with the `Token` scheme. The matcher ignores
+ // Authorization, so replay needs no auth; the value matters only when
+ // re-recording.
+ auth := ""
+ if token := os.Getenv("DEEPGRAM_API_KEY"); token != "" {
+ auth = "Token " + token
+ }
+
+ client := newVCRClient(rec, auth)
+
+ driver := NewDeepgramDriver(client)
+ records, err := driver.ListAccounts(context.Background())
+ require.NoError(t, err)
+ // owner@example.com appears in both projects and must be deduped.
+ require.Len(t, records, 3)
+
+ // owner@example.com is first seen with only ["member"] in project-1 and
+ // gains ["owner"] in project-2. The Owner role / admin flag therefore
+ // depend on the cross-project scope union actually taking effect.
+ owner := records[0]
+ assert.Equal(t, "m-0000-0000-0001", owner.ExternalID)
+ assert.Equal(t, "owner@example.com", owner.Email)
+ assert.Equal(t, "Olivia Owner", owner.FullName)
+ assert.Equal(t, []string{"Owner"}, owner.Roles)
+ assert.True(t, owner.IsAdmin)
+ assert.Equal(t, coredata.AccessReviewEntryAccountTypeUser, owner.AccountType)
+
+ member := records[1]
+ assert.Equal(t, "member@example.com", member.Email)
+ assert.Equal(t, []string{"Member"}, member.Roles)
+ assert.False(t, member.IsAdmin)
+
+ dev := records[2]
+ assert.Equal(t, "dev@example.com", dev.Email)
+ assert.Equal(t, []string{"Member"}, dev.Roles)
+}
+
+func TestDeepgramRoles(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, []string{"Owner"}, deepgramRoles([]string{"owner"}))
+ assert.Equal(t, []string{"Admin"}, deepgramRoles([]string{"admin", "read:transcripts"}))
+ assert.Equal(t, []string{"Member"}, deepgramRoles([]string{"read:transcripts"}))
+ assert.True(t, deepgramIsAdmin([]string{"owner"}))
+ assert.True(t, deepgramIsAdmin([]string{"admin"}))
+ assert.False(t, deepgramIsAdmin([]string{"member"}))
+}
+
+func TestDeepgramUnionScopes(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, []string{"member", "owner"}, deepgramUnionScopes([]string{"member"}, []string{"owner"}))
+ assert.Equal(t, []string{"a", "b"}, deepgramUnionScopes([]string{"a", "b"}, []string{"a"}))
+ assert.Empty(t, deepgramUnionScopes(nil, nil))
+}
diff --git a/pkg/accessreview/drivers/langfuse.go b/pkg/accessreview/drivers/langfuse.go
new file mode 100644
index 000000000..71c976eb5
--- /dev/null
+++ b/pkg/accessreview/drivers/langfuse.go
@@ -0,0 +1,154 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+// LangfuseDriver lists the members of a single Langfuse organization via the
+// organization-scoped public API. The organization API key (HTTP Basic,
+// publicKey:secretKey) is bound to one organization on the configured host,
+// so GET /api/public/organizations/memberships returns every member with no
+// tenant selector. The Basic credential is applied by the connection
+// transport; the base URL spans the regional cloud hosts and self-hosting.
+type LangfuseDriver struct {
+ httpClient *http.Client
+ baseURL string
+}
+
+var _ Driver = (*LangfuseDriver)(nil)
+
+type langfuseMembershipsResponse struct {
+ Memberships []langfuseMembership `json:"memberships"`
+}
+
+type langfuseMembership struct {
+ UserID string `json:"userId"`
+ Role string `json:"role"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+}
+
+func NewLangfuseDriver(httpClient *http.Client, baseURL string) *LangfuseDriver {
+ return &LangfuseDriver{
+ httpClient: httpClient,
+ baseURL: baseURL,
+ }
+}
+
+func (d *LangfuseDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
+ baseURL, err := url.Parse(d.baseURL)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse langfuse base URL: %w", err)
+ }
+
+ endpoint := baseURL.JoinPath("api", "public", "organizations", "memberships")
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create langfuse memberships request: %w", err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("cannot execute langfuse memberships request: %w", err)
+ }
+
+ defer func() {
+ _ = httpResp.Body.Close()
+ }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return nil, fmt.Errorf("cannot fetch langfuse memberships: unexpected status %d", httpResp.StatusCode)
+ }
+
+ var resp langfuseMembershipsResponse
+ if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
+ return nil, fmt.Errorf("cannot decode langfuse memberships response: %w", err)
+ }
+
+ records := make([]AccountRecord, 0, len(resp.Memberships))
+
+ for _, m := range resp.Memberships {
+ email := strings.TrimSpace(m.Email)
+ if email == "" {
+ continue
+ }
+
+ records = append(records, AccountRecord{
+ Email: email,
+ FullName: langfuseFullName(m, email),
+ Roles: langfuseRoles(m.Role),
+ IsAdmin: langfuseIsAdmin(m.Role),
+ MFAStatus: coredata.MFAStatusUnknown,
+ AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
+ AccountType: coredata.AccessReviewEntryAccountTypeUser,
+ ExternalID: strings.TrimSpace(m.UserID),
+ })
+ }
+
+ return records, nil
+}
+
+func langfuseFullName(m langfuseMembership, fallback string) string {
+ if name := strings.TrimSpace(m.Name); name != "" {
+ return name
+ }
+
+ return fallback
+}
+
+// langfuseRoles maps the Langfuse organization MembershipRole
+// (OWNER/ADMIN/MEMBER/VIEWER, and the RBAC NONE) to a stable display label,
+// preserving unknown future roles verbatim.
+func langfuseRoles(role string) []string {
+ switch strings.ToUpper(strings.TrimSpace(role)) {
+ case "OWNER":
+ return []string{"Owner"}
+ case "ADMIN":
+ return []string{"Admin"}
+ case "MEMBER":
+ return []string{"Member"}
+ case "VIEWER":
+ return []string{"Viewer"}
+ case "NONE":
+ return []string{"None"}
+ default:
+ if r := strings.TrimSpace(role); r != "" {
+ return []string{r}
+ }
+
+ return []string{}
+ }
+}
+
+func langfuseIsAdmin(role string) bool {
+ switch strings.ToUpper(strings.TrimSpace(role)) {
+ case "OWNER", "ADMIN":
+ return true
+ default:
+ return false
+ }
+}
diff --git a/pkg/accessreview/drivers/langfuse_test.go b/pkg/accessreview/drivers/langfuse_test.go
new file mode 100644
index 000000000..e31dbc5fd
--- /dev/null
+++ b/pkg/accessreview/drivers/langfuse_test.go
@@ -0,0 +1,74 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestLangfuseDriver(t *testing.T) {
+ t.Parallel()
+
+ rec := newRecorder(t, "testdata/langfuse", "LANGFUSE_API_KEY")
+ // Langfuse authenticates with HTTP Basic (publicKey:secretKey). The
+ // matcher ignores Authorization, so replay needs no auth.
+ client := newVCRClient(rec, basicAuthUserPass(os.Getenv("LANGFUSE_API_KEY")))
+
+ driver := NewLangfuseDriver(client, "https://cloud.langfuse.com")
+ records, err := driver.ListAccounts(context.Background())
+ require.NoError(t, err)
+ require.Len(t, records, 4)
+
+ owner := records[0]
+ assert.Equal(t, "lf-user-1", owner.ExternalID)
+ assert.Equal(t, "owner@example.com", owner.Email)
+ assert.Equal(t, "Olivia Owner", owner.FullName)
+ assert.Equal(t, []string{"Owner"}, owner.Roles)
+ assert.True(t, owner.IsAdmin)
+ assert.Equal(t, coredata.AccessReviewEntryAccountTypeUser, owner.AccountType)
+
+ admin := records[1]
+ assert.Equal(t, []string{"Admin"}, admin.Roles)
+ assert.True(t, admin.IsAdmin)
+
+ member := records[2]
+ assert.Equal(t, []string{"Member"}, member.Roles)
+ assert.False(t, member.IsAdmin)
+
+ viewer := records[3]
+ assert.Equal(t, []string{"Viewer"}, viewer.Roles)
+ assert.False(t, viewer.IsAdmin)
+ // name is empty in the payload, so the email is used as the display name.
+ assert.Equal(t, "viewer@example.com", viewer.FullName)
+}
+
+func TestLangfuseRoles(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, []string{"Owner"}, langfuseRoles("OWNER"))
+ assert.Equal(t, []string{"Admin"}, langfuseRoles("ADMIN"))
+ assert.Equal(t, []string{"Member"}, langfuseRoles("MEMBER"))
+ assert.Equal(t, []string{"Viewer"}, langfuseRoles("VIEWER"))
+ assert.Equal(t, []string{"None"}, langfuseRoles("NONE"))
+ assert.True(t, langfuseIsAdmin("OWNER"))
+ assert.True(t, langfuseIsAdmin("ADMIN"))
+ assert.False(t, langfuseIsAdmin("MEMBER"))
+}
diff --git a/pkg/accessreview/drivers/mercury.go b/pkg/accessreview/drivers/mercury.go
new file mode 100644
index 000000000..cb92aa21d
--- /dev/null
+++ b/pkg/accessreview/drivers/mercury.go
@@ -0,0 +1,173 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+const (
+ mercuryUsersEndpoint = "https://api.mercury.com/api/v1/users"
+ // mercuryUsersPageSize is the page size requested from GET /api/v1/users.
+ // The API caps `limit` at 1000; 500 keeps responses small while still
+ // returning every member of typical organizations in one page.
+ mercuryUsersPageSize = 500
+)
+
+// MercuryDriver lists the users of a single Mercury organization. The
+// access token (Bearer) is bound to one organization, so GET /api/v1/users
+// returns every member of that organization with no tenant selector.
+type MercuryDriver struct {
+ httpClient *http.Client
+}
+
+var _ Driver = (*MercuryDriver)(nil)
+
+type mercuryUser struct {
+ UserID string `json:"userId"`
+ FirstName string `json:"firstName"`
+ LastName string `json:"lastName"`
+ Email string `json:"email"`
+ OrganizationRole string `json:"organizationRole"`
+}
+
+type mercuryUsersResponse struct {
+ Users []mercuryUser `json:"users"`
+ Page struct {
+ NextPage *string `json:"nextPage"`
+ } `json:"page"`
+}
+
+func NewMercuryDriver(httpClient *http.Client) *MercuryDriver {
+ return &MercuryDriver{httpClient: httpClient}
+}
+
+func (d *MercuryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
+ var (
+ records []AccountRecord
+ startAfter string
+ )
+
+ for range maxPaginationPages {
+ resp, err := d.fetchUsersPage(ctx, startAfter)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, u := range resp.Users {
+ email := strings.TrimSpace(u.Email)
+ if email == "" {
+ continue
+ }
+
+ records = append(records, AccountRecord{
+ Email: email,
+ FullName: mercuryFullName(u, email),
+ Roles: mercuryRoles(u.OrganizationRole),
+ IsAdmin: u.OrganizationRole == "administrator",
+ MFAStatus: coredata.MFAStatusUnknown,
+ AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
+ AccountType: coredata.AccessReviewEntryAccountTypeUser,
+ ExternalID: strings.TrimSpace(u.UserID),
+ })
+ }
+
+ if resp.Page.NextPage == nil || *resp.Page.NextPage == "" {
+ return records, nil
+ }
+
+ startAfter = *resp.Page.NextPage
+ }
+
+ return nil, fmt.Errorf("cannot list all mercury users: %w", ErrPaginationLimitReached)
+}
+
+func (d *MercuryDriver) fetchUsersPage(ctx context.Context, startAfter string) (*mercuryUsersResponse, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, mercuryUsersEndpoint, nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create mercury users request: %w", err)
+ }
+
+ q := req.URL.Query()
+ q.Set("limit", strconv.Itoa(mercuryUsersPageSize))
+
+ if startAfter != "" {
+ q.Set("start_after", startAfter)
+ }
+
+ req.URL.RawQuery = q.Encode()
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("cannot execute mercury users request: %w", err)
+ }
+
+ defer func() {
+ _ = httpResp.Body.Close()
+ }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return nil, fmt.Errorf("cannot fetch mercury users: unexpected status %d", httpResp.StatusCode)
+ }
+
+ var resp mercuryUsersResponse
+ if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
+ return nil, fmt.Errorf("cannot decode mercury users response: %w", err)
+ }
+
+ return &resp, nil
+}
+
+func mercuryFullName(u mercuryUser, fallback string) string {
+ fullName := strings.TrimSpace(strings.TrimSpace(u.FirstName) + " " + strings.TrimSpace(u.LastName))
+ if fullName != "" {
+ return fullName
+ }
+
+ return fallback
+}
+
+// mercuryRoles maps Mercury's organizationRole enum
+// (administrator/bookkeeper/customUser/cardOnlyUser/employee) to a stable
+// display label, preserving unknown future roles verbatim.
+func mercuryRoles(role string) []string {
+ switch role {
+ case "administrator":
+ return []string{"Administrator"}
+ case "bookkeeper":
+ return []string{"Bookkeeper"}
+ case "customUser":
+ return []string{"Custom User"}
+ case "cardOnlyUser":
+ return []string{"Card Only User"}
+ case "employee":
+ return []string{"Employee"}
+ default:
+ if role != "" {
+ return []string{role}
+ }
+
+ return []string{}
+ }
+}
diff --git a/pkg/accessreview/drivers/mercury_test.go b/pkg/accessreview/drivers/mercury_test.go
new file mode 100644
index 000000000..cec6d6f7b
--- /dev/null
+++ b/pkg/accessreview/drivers/mercury_test.go
@@ -0,0 +1,78 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestMercuryDriver(t *testing.T) {
+ t.Parallel()
+
+ rec := newRecorder(t, "testdata/mercury", "MERCURY_API_TOKEN")
+ client := newVCRClient(rec, bearerAuth(os.Getenv("MERCURY_API_TOKEN")))
+
+ driver := NewMercuryDriver(client)
+ records, err := driver.ListAccounts(context.Background())
+ require.NoError(t, err)
+ require.Len(t, records, 3)
+
+ admin := records[0]
+ assert.Equal(t, "8f1a6f1e-0000-4000-8000-000000000001", admin.ExternalID)
+ assert.Equal(t, "ada@example.com", admin.Email)
+ assert.Equal(t, "Ada Admin", admin.FullName)
+ assert.Equal(t, []string{"Administrator"}, admin.Roles)
+ assert.True(t, admin.IsAdmin)
+ assert.Equal(t, coredata.AccessReviewEntryAccountTypeUser, admin.AccountType)
+ assert.Equal(t, coredata.MFAStatusUnknown, admin.MFAStatus)
+
+ bookkeeper := records[1]
+ assert.Equal(t, []string{"Bookkeeper"}, bookkeeper.Roles)
+ assert.False(t, bookkeeper.IsAdmin)
+
+ employee := records[2]
+ assert.Equal(t, []string{"Employee"}, employee.Roles)
+ assert.False(t, employee.IsAdmin)
+}
+
+func TestMercuryRoles(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ in string
+ want []string
+ }{
+ {"administrator", []string{"Administrator"}},
+ {"bookkeeper", []string{"Bookkeeper"}},
+ {"customUser", []string{"Custom User"}},
+ {"cardOnlyUser", []string{"Card Only User"}},
+ {"employee", []string{"Employee"}},
+ {"unknown_future_role", []string{"unknown_future_role"}},
+ }
+
+ for _, c := range cases {
+ t.Run(c.in, func(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, c.want, mercuryRoles(c.in))
+ })
+ }
+}
diff --git a/pkg/accessreview/drivers/testdata/apollo.yaml b/pkg/accessreview/drivers/testdata/apollo.yaml
new file mode 100644
index 000000000..179de141f
--- /dev/null
+++ b/pkg/accessreview/drivers/testdata/apollo.yaml
@@ -0,0 +1,37 @@
+---
+# Hand-authored fixture for GET /api/v1/users/search against an Apollo.io
+# account (teammates, not the prospect DB). The user object shape (id, name,
+# first_name, last_name, email, role) and the pagination wrapper mirror the
+# documented response. Synthetic IDs/emails only.
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.apollo.io
+ form:
+ page:
+ - "1"
+ per_page:
+ - "100"
+ headers:
+ Accept:
+ - application/json
+ url: https://api.apollo.io/api/v1/users/search?page=1&per_page=100
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"users":[{"id":"5f0000000000000000000001","name":"Alice Admin","first_name":"Alice","last_name":"Admin","email":"alice@example.com","role":"Admin"},{"id":"5f0000000000000000000002","name":"Bob Rep","first_name":"Bob","last_name":"Rep","email":"bob@example.com","role":"Sales Rep"},{"id":"5f0000000000000000000003","name":"Carol Manager","first_name":"Carol","last_name":"Manager","email":"carol@example.com","role":"Billing and Seat Manager"},{"id":"5f0000000000000000000004","name":"","first_name":"","last_name":"","email":"dave@example.com","role":"Sales Rep"}],"pagination":{"page":1,"per_page":100,"total_entries":4,"total_pages":1}}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 140ms
diff --git a/pkg/accessreview/drivers/testdata/clickhouse.yaml b/pkg/accessreview/drivers/testdata/clickhouse.yaml
new file mode 100644
index 000000000..a6f3b8ada
--- /dev/null
+++ b/pkg/accessreview/drivers/testdata/clickhouse.yaml
@@ -0,0 +1,58 @@
+---
+# Hand-authored fixture for the ClickHouse Cloud member-listing flow: GET
+# /v1/organizations (key maps to exactly one org) then GET
+# /v1/organizations/{id}/members. Responses use the {status,requestId,result}
+# envelope. assignedRoles is the live role source; the deprecated `role` field
+# is the fallback. Synthetic IDs/emails only.
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.clickhouse.cloud
+ headers:
+ Accept:
+ - application/json
+ url: https://api.clickhouse.cloud/v1/organizations
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"status":200,"requestId":"11111111-1111-4111-8111-111111111111","result":[{"id":"aaaaaaaa-0000-4000-8000-000000000001","name":"Example Org","createdAt":"2025-12-01T08:00:00Z"}]}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 110ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.clickhouse.cloud
+ headers:
+ Accept:
+ - application/json
+ url: https://api.clickhouse.cloud/v1/organizations/aaaaaaaa-0000-4000-8000-000000000001/members
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"status":200,"requestId":"22222222-2222-4222-8222-222222222222","result":[{"userId":"u-0000-0001","name":"Admin User","email":"admin@example.com","role":"admin","joinedAt":"2026-01-02T10:00:00Z","assignedRoles":[{"roleId":"33333333-3333-4333-8333-333333333333","roleName":"Admin","roleType":"system"}]},{"userId":"u-0000-0002","name":"Dev User","email":"dev@example.com","role":"developer","joinedAt":"2026-02-03T11:00:00Z","assignedRoles":[]},{"userId":"u-0000-0003","name":"","email":"custom@example.com","role":"developer","joinedAt":"2026-03-04T12:00:00Z","assignedRoles":[{"roleId":"44444444-4444-4444-8444-444444444444","roleName":"Billing Admin","roleType":"custom"}]}]}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 110ms
diff --git a/pkg/accessreview/drivers/testdata/deepgram.yaml b/pkg/accessreview/drivers/testdata/deepgram.yaml
new file mode 100644
index 000000000..f39323305
--- /dev/null
+++ b/pkg/accessreview/drivers/testdata/deepgram.yaml
@@ -0,0 +1,82 @@
+---
+# Hand-authored fixture for the Deepgram account-listing flow: GET /v1/projects
+# then GET /v1/projects/{project_id}/members for each project. owner@example.com
+# belongs to both projects (with different scopes), exercising the dedup +
+# scope-union path. Synthetic IDs/emails only.
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.deepgram.com
+ headers:
+ Accept:
+ - application/json
+ url: https://api.deepgram.com/v1/projects
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"projects":[{"project_id":"proj-1111-1111","name":"Production"},{"project_id":"proj-2222-2222","name":"Staging"}]}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 90ms
+ - id: 1
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.deepgram.com
+ headers:
+ Accept:
+ - application/json
+ url: https://api.deepgram.com/v1/projects/proj-1111-1111/members
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"members":[{"member_id":"m-0000-0000-0001","email":"owner@example.com","first_name":"Olivia","last_name":"Owner","scopes":["member"]},{"member_id":"m-0000-0000-0002","email":"member@example.com","first_name":"Mia","last_name":"Member","scopes":["member"]}]}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 90ms
+ - id: 2
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.deepgram.com
+ headers:
+ Accept:
+ - application/json
+ url: https://api.deepgram.com/v1/projects/proj-2222-2222/members
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"members":[{"member_id":"m-0000-0000-0001","email":"owner@example.com","first_name":"Olivia","last_name":"Owner","scopes":["owner"]},{"member_id":"m-0000-0000-0003","email":"dev@example.com","first_name":"Dan","last_name":"Dev","scopes":["member"]}]}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 90ms
diff --git a/pkg/accessreview/drivers/testdata/langfuse.yaml b/pkg/accessreview/drivers/testdata/langfuse.yaml
new file mode 100644
index 000000000..2583bdf9a
--- /dev/null
+++ b/pkg/accessreview/drivers/testdata/langfuse.yaml
@@ -0,0 +1,32 @@
+---
+# Hand-authored fixture for GET /api/public/organizations/memberships against a
+# Langfuse organization (org-scoped API key). The membership object shape
+# (userId, role, email, name) and the {memberships:[...]} wrapper mirror the
+# documented response. Synthetic IDs/emails only.
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: cloud.langfuse.com
+ headers:
+ Accept:
+ - application/json
+ url: https://cloud.langfuse.com/api/public/organizations/memberships
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"memberships":[{"userId":"lf-user-1","role":"OWNER","email":"owner@example.com","name":"Olivia Owner"},{"userId":"lf-user-2","role":"ADMIN","email":"admin@example.com","name":"Adam Admin"},{"userId":"lf-user-3","role":"MEMBER","email":"member@example.com","name":"Mia Member"},{"userId":"lf-user-4","role":"VIEWER","email":"viewer@example.com","name":""}]}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 100ms
diff --git a/pkg/accessreview/drivers/testdata/mercury.yaml b/pkg/accessreview/drivers/testdata/mercury.yaml
new file mode 100644
index 000000000..a4e298b89
--- /dev/null
+++ b/pkg/accessreview/drivers/testdata/mercury.yaml
@@ -0,0 +1,35 @@
+---
+# Hand-authored fixture for GET /api/v1/users against a Mercury organization.
+# The user object shape (userId, firstName, lastName, email, organizationRole)
+# mirrors the documented UserDetails schema; the page cursor is absent so the
+# driver stops after one page. Synthetic IDs/emails only.
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: api.mercury.com
+ form:
+ limit:
+ - "500"
+ headers:
+ Accept:
+ - application/json
+ url: https://api.mercury.com/api/v1/users?limit=500
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"users":[{"userId":"8f1a6f1e-0000-4000-8000-000000000001","firstName":"Ada","lastName":"Admin","email":"ada@example.com","organizationRole":"administrator"},{"userId":"8f1a6f1e-0000-4000-8000-000000000002","firstName":"Ben","lastName":"Books","email":"ben@example.com","organizationRole":"bookkeeper"},{"userId":"8f1a6f1e-0000-4000-8000-000000000003","firstName":"Eve","lastName":"Employee","email":"eve@example.com","organizationRole":"employee"}],"page":{"nextPage":null,"previousPage":null}}'
+ headers:
+ Content-Type:
+ - application/json
+ status: 200 OK
+ code: 200
+ duration: 120ms
diff --git a/pkg/accessreview/drivers/vcr_test.go b/pkg/accessreview/drivers/vcr_test.go
index e66b6f3c1..24f64370e 100644
--- a/pkg/accessreview/drivers/vcr_test.go
+++ b/pkg/accessreview/drivers/vcr_test.go
@@ -114,6 +114,20 @@ func basicAuth(username string) string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"))
}
+// basicAuthUserPass returns the HTTP Basic auth header value for a credential
+// that already holds the "username:password" pair ("Basic
+// base64()"), or "" if the credential is empty. ClickHouse
+// Cloud (keyId:keySecret) and Langfuse (publicKey:secretKey) present such
+// a credential. The matcher ignores Authorization, so this only matters
+// when re-recording.
+func basicAuthUserPass(credential string) string {
+ if credential == "" {
+ return ""
+ }
+
+ return "Basic " + base64.StdEncoding.EncodeToString([]byte(credential))
+}
+
// newVCRClient creates an *http.Client backed by the recorder's transport,
// with an optional Authorization header injected into requests (for recording
// mode). The authValue should be the complete header value, e.g.
diff --git a/pkg/connector/provider/apollo.go b/pkg/connector/provider/apollo.go
new file mode 100644
index 000000000..5ec9a622e
--- /dev/null
+++ b/pkg/connector/provider/apollo.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider
+
+import (
+ "context"
+ "net/http"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func apolloRegistration() *Registration {
+ return &Registration{
+ Provider: coredata.ConnectorProviderApollo,
+ DisplayName: "Apollo.io",
+ SupportsAPIKey: true,
+ // Apollo's REST API authenticates with a master API key in the
+ // x-api-key header; it rejects Authorization: Bearer (and, since
+ // Sept 2024, query/body key params). APIKeyHeader makes the
+ // APIKeyConnection send x-api-key instead of Bearer. There is no
+ // OAuth2 flow needed: the customer supplies a master key, which is
+ // bound to one Apollo account, so there is nothing to pick
+ // (Pattern 3): no settings struct, no picker.
+ APIKeyHeader: "x-api-key",
+ // ProbeURL lets the connection-status check confirm the key with a
+ // lightweight GET; the transport attaches x-api-key, and a missing,
+ // dead, or non-master key returns 401/403.
+ ProbeURL: "https://api.apollo.io/api/v1/users/search?page=1&per_page=1",
+ //
+ // No NewNameResolver: Apollo exposes no stable account-name
+ // endpoint reachable with the master key, so the source keeps its
+ // generic name.
+ NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
+ return drivers.NewApolloDriver(c), nil
+ },
+ }
+}
diff --git a/pkg/connector/provider/builtin.go b/pkg/connector/provider/builtin.go
index b2bf5b8bf..bc140fff4 100644
--- a/pkg/connector/provider/builtin.go
+++ b/pkg/connector/provider/builtin.go
@@ -23,15 +23,18 @@ func NewBuiltinRegistry() *Registry {
r := NewRegistry()
for _, reg := range []*Registration{
anthropicRegistration(),
+ apolloRegistration(),
asanaRegistration(),
betterStackRegistration(),
bitbucketRegistration(),
brexRegistration(),
clerkRegistration(),
+ clickhouseRegistration(),
clickupRegistration(),
cloudflareRegistration(),
cursorRegistration(),
datadogRegistration(),
+ deepgramRegistration(),
docusignRegistration(),
grafanaRegistration(),
githubRegistration(),
@@ -40,7 +43,9 @@ func NewBuiltinRegistry() *Registry {
herokuRegistration(),
hubspotRegistration(),
intercomRegistration(),
+ langfuseRegistration(),
linearRegistration(),
+ mercuryRegistration(),
metabaseRegistration(),
microsoft365Registration(),
mondayRegistration(),
diff --git a/pkg/connector/provider/clickhouse.go b/pkg/connector/provider/clickhouse.go
new file mode 100644
index 000000000..3fc02737f
--- /dev/null
+++ b/pkg/connector/provider/clickhouse.go
@@ -0,0 +1,52 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider
+
+import (
+ "context"
+ "net/http"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func clickhouseRegistration() *Registration {
+ return &Registration{
+ Provider: coredata.ConnectorProviderClickHouse,
+ DisplayName: "ClickHouse Cloud",
+ SupportsAPIKey: true,
+ // ClickHouse Cloud's control-plane API authenticates with HTTP Basic
+ // auth where the credential is keyId:keySecret. APIKeyBasicAuthUserPass
+ // makes the APIKeyConnection base64 the verbatim "keyId:keySecret"
+ // the operator pastes (the empty-password APIKeyBasicAuth cannot
+ // carry the secret). There is no OAuth2 flow; a key/secret pair is
+ // scoped to exactly one organization, which the driver discovers via
+ // GET /v1/organizations, so there is nothing to pick or configure
+ // (Pattern 3): no settings struct, no picker.
+ APIKeyBasicAuthUserPass: true,
+ // ProbeURL lets the connection-status check confirm the key/secret
+ // with a lightweight GET; the transport attaches the Basic
+ // credential and a dead key/secret returns 401/403.
+ ProbeURL: "https://api.clickhouse.cloud/v1/organizations",
+ //
+ // No NewNameResolver: the organization name is available but would
+ // duplicate the driver's discovery call; the source keeps its
+ // generic name.
+ NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
+ return drivers.NewClickHouseDriver(c), nil
+ },
+ }
+}
diff --git a/pkg/connector/provider/deepgram.go b/pkg/connector/provider/deepgram.go
new file mode 100644
index 000000000..97772bed7
--- /dev/null
+++ b/pkg/connector/provider/deepgram.go
@@ -0,0 +1,49 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider
+
+import (
+ "context"
+ "net/http"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func deepgramRegistration() *Registration {
+ return &Registration{
+ Provider: coredata.ConnectorProviderDeepgram,
+ DisplayName: "Deepgram",
+ SupportsAPIKey: true,
+ // Deepgram authenticates with an API key under the `Token` scheme
+ // (`Authorization: Token `), not Bearer. APIKeyAuthScheme makes
+ // the APIKeyConnection use that scheme. There is no third-party
+ // OAuth2 flow; the customer supplies an owner/admin key bound to one
+ // account, so there is nothing to pick (Pattern 3): no settings
+ // struct, no picker.
+ APIKeyAuthScheme: "Token",
+ // ProbeURL lets the connection-status check confirm the key with a
+ // lightweight GET; the transport attaches the `Token` credential and
+ // a dead key returns 401/403.
+ ProbeURL: "https://api.deepgram.com/v1/projects",
+ //
+ // No NewNameResolver: an account may span several projects, so there
+ // is no single instance name; the source keeps its generic name.
+ NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
+ return drivers.NewDeepgramDriver(c), nil
+ },
+ }
+}
diff --git a/pkg/connector/provider/langfuse.go b/pkg/connector/provider/langfuse.go
new file mode 100644
index 000000000..0bb8f7a8d
--- /dev/null
+++ b/pkg/connector/provider/langfuse.go
@@ -0,0 +1,89 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func langfuseRegistration() *Registration {
+ return &Registration{
+ Provider: coredata.ConnectorProviderLangfuse,
+ DisplayName: "Langfuse",
+ SupportsAPIKey: true,
+ // Langfuse's organization-scoped public API authenticates with HTTP
+ // Basic auth where the credential is publicKey:secretKey.
+ // APIKeyBasicAuthUserPass base64s the verbatim "publicKey:secretKey" the
+ // operator pastes (the empty-password APIKeyBasicAuth cannot carry
+ // the secret). The org API key is bound to one organization, so
+ // there is nothing to pick; only the regional/self-hosted base URL
+ // is per-tenant and is surfaced as an extra setting.
+ APIKeyBasicAuthUserPass: true,
+ ExtraSettings: []ExtraSetting{
+ {Key: "baseUrl", Label: "Base URL", Required: true},
+ },
+ // BuildProbeURL derives the probe endpoint from the per-connection
+ // base URL (the host is regional/self-hosted, so a static ProbeURL
+ // cannot express it); the transport attaches the Basic credential
+ // and a dead key returns 401/403.
+ BuildProbeURL: buildLangfuseProbeURL,
+ //
+ // No NewNameResolver: the memberships endpoint carries no
+ // organization name, so the source keeps its generic name.
+ NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
+ settings, err := coredata.ConnectorSettings[coredata.LangfuseConnectorSettings](conn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read langfuse connector settings: %w", err)
+ }
+
+ baseURL, err := normalizeLangfuseBaseURL(settings.BaseURL)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create langfuse driver: %w", err)
+ }
+
+ return drivers.NewLangfuseDriver(c, baseURL), nil
+ },
+ }
+}
+
+func normalizeLangfuseBaseURL(raw string) (string, error) {
+ baseURL := strings.TrimSpace(raw)
+ if baseURL == "" {
+ return "", fmt.Errorf("base_url is required")
+ }
+
+ u, err := url.Parse(baseURL)
+ if err != nil {
+ return "", fmt.Errorf("base_url must be a valid URL: %w", err)
+ }
+
+ if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
+ return "", fmt.Errorf("base_url must be an http(s) URL")
+ }
+
+ u.Path = strings.TrimRight(u.Path, "/")
+ u.RawQuery = ""
+ u.Fragment = ""
+
+ return u.String(), nil
+}
diff --git a/pkg/connector/provider/langfuse_test.go b/pkg/connector/provider/langfuse_test.go
new file mode 100644
index 000000000..6034e3252
--- /dev/null
+++ b/pkg/connector/provider/langfuse_test.go
@@ -0,0 +1,108 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider_test
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.gearno.de/kit/httpclient"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/connector/provider"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestLangfuseRegistrationMetadata(t *testing.T) {
+ t.Parallel()
+
+ r := provider.NewBuiltinRegistry()
+ reg, ok := r.Get(coredata.ConnectorProviderLangfuse)
+ require.True(t, ok, "langfuse provider must be registered")
+
+ assert.Equal(t, "Langfuse", reg.DisplayName)
+ assert.True(t, reg.SupportsAPIKey)
+ // Langfuse presents publicKey:secretKey as a full HTTP Basic credential.
+ assert.True(t, reg.APIKeyBasicAuthUserPass)
+ assert.Empty(t, reg.APIKeyHeader)
+ assert.Empty(t, reg.APIKeyAuthScheme)
+ require.Len(t, reg.ExtraSettings, 1)
+ assert.Equal(t, "baseUrl", reg.ExtraSettings[0].Key)
+ assert.Equal(t, "Base URL", reg.ExtraSettings[0].Label)
+ assert.True(t, reg.ExtraSettings[0].Required)
+ // Single-tenant API-key provider: no picker, no name resolver.
+ assert.Nil(t, reg.NewNameResolver, "langfuse must not wire a name resolver")
+ assert.Nil(t, reg.SetOrganizationSettings, "langfuse must not wire a picker store")
+}
+
+func TestLangfuseNewDriver(t *testing.T) {
+ t.Parallel()
+
+ r := provider.NewBuiltinRegistry()
+ reg, ok := r.Get(coredata.ConnectorProviderLangfuse)
+ require.True(t, ok, "langfuse provider must be registered")
+ require.NotNil(t, reg.NewDriver, "langfuse NewDriver closure must be wired")
+
+ t.Run("creates driver with valid base_url", func(t *testing.T) {
+ t.Parallel()
+
+ raw, err := json.Marshal(&coredata.LangfuseConnectorSettings{
+ BaseURL: "https://cloud.langfuse.com",
+ })
+ require.NoError(t, err)
+
+ conn := &coredata.Connector{
+ Provider: coredata.ConnectorProviderLangfuse,
+ RawSettings: raw,
+ }
+
+ drv, err := reg.NewDriver(context.Background(), httpclient.DefaultClient(httpclient.WithSSRFProtection()), conn, nil)
+ require.NoError(t, err)
+ assert.IsType(t, &drivers.LangfuseDriver{}, drv)
+ })
+
+ t.Run("errors when base_url is missing", func(t *testing.T) {
+ t.Parallel()
+
+ conn := &coredata.Connector{
+ Provider: coredata.ConnectorProviderLangfuse,
+ RawSettings: []byte(`{}`),
+ }
+
+ _, err := reg.NewDriver(context.Background(), httpclient.DefaultClient(httpclient.WithSSRFProtection()), conn, nil)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "base_url is required")
+ })
+
+ t.Run("errors when base_url is invalid", func(t *testing.T) {
+ t.Parallel()
+
+ raw, err := json.Marshal(&coredata.LangfuseConnectorSettings{
+ BaseURL: "ftp://cloud.langfuse.com",
+ })
+ require.NoError(t, err)
+
+ conn := &coredata.Connector{
+ Provider: coredata.ConnectorProviderLangfuse,
+ RawSettings: raw,
+ }
+
+ _, err = reg.NewDriver(context.Background(), httpclient.DefaultClient(httpclient.WithSSRFProtection()), conn, nil)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "base_url must be an http(s) URL")
+ })
+}
diff --git a/pkg/connector/provider/mercury.go b/pkg/connector/provider/mercury.go
new file mode 100644
index 000000000..3d3837d7a
--- /dev/null
+++ b/pkg/connector/provider/mercury.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider
+
+import (
+ "context"
+ "net/http"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func mercuryRegistration() *Registration {
+ return &Registration{
+ Provider: coredata.ConnectorProviderMercury,
+ DisplayName: "Mercury",
+ SupportsAPIKey: true,
+ // Mercury authenticates with a self-serve API token presented as
+ // Authorization: Bearer, the default APIKeyConnection scheme. There
+ // is no third-party OAuth2 flow for the Users API. The token is
+ // bound to one Mercury organization, so there is nothing to pick
+ // (Pattern 3): no settings struct, no picker, no
+ // SetOrganizationSettings.
+ //
+ // ProbeURL lets the connection-status check confirm the token is
+ // live with a lightweight GET; the transport attaches the Bearer
+ // token and a dead token returns 401/403.
+ ProbeURL: "https://api.mercury.com/api/v1/users?limit=1",
+ //
+ // No NewNameResolver: GET /api/v1/users carries no organization
+ // name and a read-only token may lack other scopes, so the source
+ // keeps its generic name (the source-name worker degrades
+ // gracefully).
+ NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
+ return drivers.NewMercuryDriver(c), nil
+ },
+ }
+}
diff --git a/pkg/connector/provider/probe.go b/pkg/connector/provider/probe.go
index 537e08506..816eb51aa 100644
--- a/pkg/connector/provider/probe.go
+++ b/pkg/connector/provider/probe.go
@@ -314,6 +314,25 @@ func buildMetabaseProbeURL(conn *coredata.Connector) (string, error) {
return endpoint.String(), nil
}
+func buildLangfuseProbeURL(conn *coredata.Connector) (string, error) {
+ s, err := coredata.ConnectorSettings[coredata.LangfuseConnectorSettings](conn)
+ if err != nil {
+ return "", fmt.Errorf("cannot read langfuse connector settings: %w", err)
+ }
+
+ baseURL, err := normalizeLangfuseBaseURL(s.BaseURL)
+ if err != nil {
+ return "", err
+ }
+
+ u, err := url.Parse(baseURL)
+ if err != nil {
+ return "", fmt.Errorf("cannot parse langfuse base URL: %w", err)
+ }
+
+ return u.JoinPath("api", "public", "organizations", "memberships").String(), nil
+}
+
func buildSigNozProbeURL(conn *coredata.Connector) (string, error) {
s, err := coredata.ConnectorSettings[coredata.SigNozConnectorSettings](conn)
if err != nil {
diff --git a/pkg/connector/provider/probe_test.go b/pkg/connector/provider/probe_test.go
index 7b030c398..257199988 100644
--- a/pkg/connector/provider/probe_test.go
+++ b/pkg/connector/provider/probe_test.go
@@ -78,6 +78,19 @@ func TestBuildOktaProbeURL(t *testing.T) {
assert.Equal(t, "https://acme.okta.com/api/v1/users?limit=1", probeURL)
}
+func TestBuildLangfuseProbeURL(t *testing.T) {
+ t.Parallel()
+
+ conn := &coredata.Connector{Provider: coredata.ConnectorProviderLangfuse}
+ require.NoError(t, conn.SetSettings(&coredata.LangfuseConnectorSettings{
+ BaseURL: "https://us.cloud.langfuse.com",
+ }))
+
+ probeURL, err := buildLangfuseProbeURL(conn)
+ require.NoError(t, err)
+ assert.Equal(t, "https://us.cloud.langfuse.com/api/public/organizations/memberships", probeURL)
+}
+
func TestBuildPostHogProbeURL(t *testing.T) {
t.Parallel()
diff --git a/pkg/coredata/connector_provider.go b/pkg/coredata/connector_provider.go
index 5bfafc27c..2d3fd561b 100644
--- a/pkg/coredata/connector_provider.go
+++ b/pkg/coredata/connector_provider.go
@@ -65,6 +65,11 @@ const (
ConnectorProviderQovery ConnectorProvider = "QOVERY"
ConnectorProviderRender ConnectorProvider = "RENDER"
ConnectorProviderNeon ConnectorProvider = "NEON"
+ ConnectorProviderMercury ConnectorProvider = "MERCURY"
+ ConnectorProviderApollo ConnectorProvider = "APOLLO"
+ ConnectorProviderDeepgram ConnectorProvider = "DEEPGRAM"
+ ConnectorProviderClickHouse ConnectorProvider = "CLICKHOUSE"
+ ConnectorProviderLangfuse ConnectorProvider = "LANGFUSE"
)
var (
@@ -117,6 +122,11 @@ func ConnectorProviders() []ConnectorProvider {
ConnectorProviderQovery,
ConnectorProviderRender,
ConnectorProviderNeon,
+ ConnectorProviderMercury,
+ ConnectorProviderApollo,
+ ConnectorProviderDeepgram,
+ ConnectorProviderClickHouse,
+ ConnectorProviderLangfuse,
}
}
@@ -164,7 +174,12 @@ func (v ConnectorProvider) IsValid() bool {
ConnectorProviderZendesk,
ConnectorProviderQovery,
ConnectorProviderRender,
- ConnectorProviderNeon:
+ ConnectorProviderNeon,
+ ConnectorProviderMercury,
+ ConnectorProviderApollo,
+ ConnectorProviderDeepgram,
+ ConnectorProviderClickHouse,
+ ConnectorProviderLangfuse:
return true
}
diff --git a/pkg/coredata/connector_settings.go b/pkg/coredata/connector_settings.go
index 9fb123be3..fe5bf73fb 100644
--- a/pkg/coredata/connector_settings.go
+++ b/pkg/coredata/connector_settings.go
@@ -156,6 +156,15 @@ type (
NeonConnectorSettings struct {
OrganizationID string `json:"organization_id"`
}
+
+ // LangfuseConnectorSettings carries the Langfuse API base URL, which
+ // spans the regional cloud hosts (cloud.langfuse.com /
+ // us.cloud.langfuse.com / …) and self-hosted instances. The
+ // organization-scoped API key is bound to a single organization on
+ // that host, so the base URL is the only per-tenant setting.
+ LangfuseConnectorSettings struct {
+ BaseURL string `json:"base_url"`
+ }
)
// GrantType returns the OAuth2 grant type recorded on the connector's
diff --git a/pkg/coredata/migrations/20260612T263841Z.sql b/pkg/coredata/migrations/20260612T263841Z.sql
new file mode 100644
index 000000000..db5f0fe45
--- /dev/null
+++ b/pkg/coredata/migrations/20260612T263841Z.sql
@@ -0,0 +1,15 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+-- PERFORMANCE OF THIS SOFTWARE.
+
+ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'LANGFUSE';
diff --git a/pkg/coredata/migrations/20260612T318094Z.sql b/pkg/coredata/migrations/20260612T318094Z.sql
new file mode 100644
index 000000000..c388fbefd
--- /dev/null
+++ b/pkg/coredata/migrations/20260612T318094Z.sql
@@ -0,0 +1,15 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+-- PERFORMANCE OF THIS SOFTWARE.
+
+ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'APOLLO';
diff --git a/pkg/coredata/migrations/20260612T489176Z.sql b/pkg/coredata/migrations/20260612T489176Z.sql
new file mode 100644
index 000000000..e89c7088c
--- /dev/null
+++ b/pkg/coredata/migrations/20260612T489176Z.sql
@@ -0,0 +1,15 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+-- PERFORMANCE OF THIS SOFTWARE.
+
+ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'CLICKHOUSE';
diff --git a/pkg/coredata/migrations/20260612T642517Z.sql b/pkg/coredata/migrations/20260612T642517Z.sql
new file mode 100644
index 000000000..ff34a4c16
--- /dev/null
+++ b/pkg/coredata/migrations/20260612T642517Z.sql
@@ -0,0 +1,15 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+-- PERFORMANCE OF THIS SOFTWARE.
+
+ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'MERCURY';
diff --git a/pkg/coredata/migrations/20260612T705263Z.sql b/pkg/coredata/migrations/20260612T705263Z.sql
new file mode 100644
index 000000000..c425707d9
--- /dev/null
+++ b/pkg/coredata/migrations/20260612T705263Z.sql
@@ -0,0 +1,15 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+-- PERFORMANCE OF THIS SOFTWARE.
+
+ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'DEEPGRAM';
diff --git a/pkg/server/api/console/v1/connector_settings.go b/pkg/server/api/console/v1/connector_settings.go
index c3e37b886..08d10dd89 100644
--- a/pkg/server/api/console/v1/connector_settings.go
+++ b/pkg/server/api/console/v1/connector_settings.go
@@ -178,6 +178,17 @@ func apiKeyConnectorSettings(input types.CreateAPIKeyConnectorInput) (json.RawMe
}
return json.Marshal(&coredata.NeonConnectorSettings{OrganizationID: *input.NeonOrganizationID})
+ case coredata.ConnectorProviderLangfuse:
+ if input.LangfuseBaseURL == nil || *input.LangfuseBaseURL == "" {
+ return nil, fmt.Errorf("cannot create langfuse connector: langfuseBaseUrl is required")
+ }
+
+ u, err := url.Parse(*input.LangfuseBaseURL)
+ if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
+ return nil, fmt.Errorf("cannot create langfuse connector: langfuseBaseUrl must be an http(s) URL")
+ }
+
+ return json.Marshal(&coredata.LangfuseConnectorSettings{BaseURL: *input.LangfuseBaseURL})
}
return nil, nil
diff --git a/pkg/server/api/console/v1/graphql/connector.graphql b/pkg/server/api/console/v1/graphql/connector.graphql
index 9b007c298..ac8adc3fc 100644
--- a/pkg/server/api/console/v1/graphql/connector.graphql
+++ b/pkg/server/api/console/v1/graphql/connector.graphql
@@ -71,6 +71,17 @@ enum ConnectorProvider
QOVERY @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderQovery")
RENDER @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderRender")
NEON @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderNeon")
+ MERCURY
+ @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderMercury")
+ APOLLO @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderApollo")
+ DEEPGRAM
+ @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderDeepgram")
+ CLICKHOUSE
+ @goEnum(
+ value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderClickHouse"
+ )
+ LANGFUSE
+ @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderLangfuse")
}
type ConnectorProviderInfo {
@@ -152,6 +163,7 @@ input CreateAPIKeyConnectorInput {
qoveryOrganizationId: String
renderWorkspaceId: String
neonOrganizationId: String
+ langfuseBaseUrl: String
}
type CreateAPIKeyConnectorPayload {