Add a dedicated SCIMBridge syncError field to the Connect GraphQL API
and populate it from the core bridge model.
Update the connector cards to read bridge state and syncError directly
from the bridge object instead of inferring failures from event
history.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Alias the connector card event selection so it does not conflict with
the SCIM event list fragment querying the same field with
different arguments.
This resolves the Relay compilation failure in CI while keeping the
latest bridge error message available for the error-state callout.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Display SCIM bridge state directly on Google Workspace and Microsoft
365 connector cards so admins can quickly identify failing bridges.
When a bridge is failed, render an inline error callout and surface the
latest provisioning event error message to make troubleshooting visible
without leaving the connector section.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add a ProfileSource schema to the MCP specification and include source\nin the Profile response schema so MCP clients can distinguish\nmanual and SCIM-managed users.\n\nWire the source field in the Profile mapper so list, get, and\ncreate user responses return the persisted profile source.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Validate ArchiveUser organization input against the loaded profile and\nuse the profile organization for owner checks and webhook emission.\n\nAlso disable both PersonPage destructive actions while either archive\nor remove mutation is pending to prevent double-submit races.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add the required blank line before the pending-invitation filter assignment\nin ArchiveUser to satisfy wsl_v5.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Ensure ArchiveUser deactivates pending invitations for the archived\nprofile so invited accounts cannot still activate after archive.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Change ArchiveUser to receive an explicit coredata scope from callers\ninstead of rebuilding scope from organization ID inside the service.\n\nWire connect and MCP resolvers to pass the authorized scope through to\nArchiveUser.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Drop the additional RemoveUser dependency-error handling that was added\noutside this PR scope.\n\nKeep the separate remove and archive operations, but let delete failures\npropagate through existing wrapped errors without the custom helper and\nreferenced-records error type.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Reorder the user operation list so the n8n lint rule requiring\nalphabetized option labels passes after introducing the Archive\noperation.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Align user-removal UX and API surface semantics with the new archive\nbehavior for manually managed users.\n\nFrontend copy and actions now use archive wording, and list rows are\nrefetched after the mutation so archived users reappear as inactive.\n\nMCP removeUser now documents and returns archived_user_id, n8n labels\nand response mapping now use archive semantics, and the CLI gains a\nuser archive command backed by the same mutation.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Add the required blank line before the profile-state conditional in\nOrganizationService.RemoveUser so the wsl_v5 lint rule passes in CI.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Switch remove-user behavior for manually managed profiles from hard\ndelete to archival by deactivating the profile. This matches the\nrequested SCIM-like lifecycle while avoiding dependency errors for\nlinked records such as signatures and assets.\n\nThe remove flow now updates profile state to INACTIVE, updates\nmembership timestamps, and emits a user-updated webhook event instead of\ndelete events. E2E coverage now asserts that remove keeps the profile\nand marks it inactive.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Map remove-user foreign key failures to a dedicated IAM conflict error so\nconsole users no longer receive a generic internal server error when a\nprofile is still referenced by signed documents, tasks, assets, or\nother records.\n\nThe service now detects wrapped dependency errors across both profile\nand membership deletes, and resolvers explicitly map the new error type\nto conflict responses. Added unit tests cover dependency detection and\nthe user-facing error message.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Parse HubSpot archived and deactivated status signals when building
access-review account records. This preserves inactive users as
explicit records instead of dropping them when status fields differ
from the current payload assumptions.
Also accept roleIds payloads and keep records that only expose an
external ID, which is common for archived users with empty email
fields. Add a focused unit test to lock in archived-user handling.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The MCP getSCIMBridge tool authorized on iam:scim-bridge:get, but no
role policy allowed that action, so every call denied — including for
owners and admins.
Owners now get the full iam:scim-bridge:* wildcard (matching the
scim-configuration and scim-event treatment in the same policy).
Admins get read-only access and are explicitly denied create/update/
delete, mirroring how scim-configuration is handled.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Drops the now-unused snapshot_id/source_id columns from every table that
carried them, the snapshots and controls_snapshots tables, the
snapshots_type enum, and the snapshot-scoped indexes. Recreates the
unique indexes that previously gated on snapshot_id IS NULL without the
snapshot scope. The data was already cleaned up in the prior PR.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
CommonThirdParty.AuthorizationAttributes returned an empty map, but
the IAM authorizer requires an entry for every requested resource ID
and treats missing entries as ErrResourceNotFound. With the new
authorize() guard on commonThirdPartyResolver.LogoURL, this surfaced
as "commonThirdParties[*].logoUrl resource not found" from the
CommonThirdPartyComboboxQuery.
Switch AuthorizationAttributes to a SELECT-by-id existence check that
returns an empty attributes map for each row that exists. The catalog
is global with no organization, so omitting organization_id is the
intended shape: the authorizer then loads no membership, evaluates
only identity-scoped policies (CommonThirdPartyCatalogPolicy grants
access), and skips audit logging.
Add an e2e regression test that seeds a CommonThirdParty directly via
the test database and asserts the GraphQL logoUrl path resolves
without error.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
PostHog's `cookieless_mode` keeps `is_capturing()` returning `true`
after rejection: in `"always"` mode `opt_out_capturing()` is a no-op
that just warns, and in `"on_reject"` mode it actually registers a
cookieless distinct_id and re-fires a pageview rather than stopping
captures. The example panel's "Capture test event" button therefore
still shipped events (cookielessly) for visitors who had rejected
consent, contradicting the "only delivered when opted in" hint.
Disable the button whenever the cached status reports opted out, and
keep a defensive `has_opted_out_capturing()` check in the click
handler so the example fails closed if the panel is reused without
its disabled-state wiring. Drop the now-redundant disclaimer next to
the last-sent timestamp.
Signed-off-by: Émile Ré <emile@probo.com>
Add a deferred PostHog wiring under examples/cookie-banner-react that
boots posthog.init() inside the probo-ready handler and derives
cookieless_mode and opt_out_capturing_by_default from the consent
snapshot for the category flagged with posthog_consent.
Driving the init args off the snapshot rather than consent_mode plugs
two cases the simpler "consent_mode alone" rule got wrong:
* OPT_OUT regulation, returning rejector: init would have booted in
on_reject + capture-on, fired a $pageview synchronously, and only
then called opt_out_capturing(). That single captured pageview
(and the posthog cookie) leaked on every page load.
* OPT_IN regulation, returning acceptor: init would have forced
"always" + opt-out, costing the visitor cookies and a one-tick
capture delay even though they had already consented.
The snapshot already encodes the regulation default
(buildDefaultConsentData on the cookie-banner client returns true for
non-necessary categories under OPT_OUT and false under OPT_IN) and any
persisted answer from a prior visit, so a single boolean drives both
init args.
Re-export the public domain types (BannerConfig, Category, Regulation,
ConsentAction, ConsentRecord, CookieItem, VisitorConsent) from
@probo/cookie-banner so the example can type the probo-ready event
detail without duck-typing it.
Adopt the PUBLIC_ env prefix in Vite so the example reads the same env
var names (PUBLIC_COOKIE_BANNER_ID, PUBLIC_COOKIE_BANNER_API_BASE_URL,
PUBLIC_POSTHOG_API_KEY) already used on getprobo.com, and add a
matching .env.example.
Ignore *.tsbuildinfo at the repo root; TypeScript's incremental cache
is machine-local and does not belong in the tree.
Signed-off-by: Émile Ré <emile@probo.com>
Skipping unconnectable providers hides OAuth-only providers when the
deployment has no OAuth credentials configured, which is the case in
e2e. The test asserted SLACK (OAuth-only) is present, so it now fails.
Assert on API-key providers instead, which are connectable regardless
of OAuth configuration.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
ConnectorProviderInfos listed every known provider, including ones
this deployment cannot connect: no OAuth client credentials configured
and no API-key or client-credentials fallback supported. Those
rendered as dead entries the operator has no way to use, so skip them.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Three follow-ups from review of the registry consolidation:
- Build Vercel's authorization URL with net/url instead of a
hand-rolled "{integration_slug}" placeholder resolved by
strings.ReplaceAll. The slug is escaped via url.PathEscape in a
per-provider Registration.BuildAuthURL closure, and the unused
AuthURLParams plumbing on Registration and OAuth2Connector is
removed (OAuth2Connector now carries a typed IntegrationSlug).
- Drop the SettingsInput union type and the per-provider
MarshalSettings closures. The create resolvers now build the typed
coredata.*ConnectorSettings directly from the gqlgen input, the
same way the OAuth callback path already does, so there is no
shared catch-all DTO and no stringly-typed boundary.
- Restore ConnectorProviders() to a plain ordered slice literal; the
intermediate map + slices.Sort added nondeterminism and a sort for
no benefit.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The console previously dispatched per-provider logic through a fan of
init()-side-effect maps (driver names, OAuth2 metadata, probe URLs,
display names, settings switches), spread across pkg/connector,
pkg/accessreview/drivers and the console v1 resolvers. Adding a new
provider required edits in every one of those places and a corresponding
switch arm in CreateConnectorRequest. The same per-provider knowledge
also leaked into Helm templates as hand-rolled environment-variable
blocks per connector.
This commit collapses the dispatch surface into a single typed
*provider.Registry. The registry is constructed once by
NewBuiltinRegistry at probod startup and threaded as an explicit
dependency into every consumer (accessreview service, console v1
resolver, OAuth2 wiring). There is no package-level state. Each
provider lives in one file under pkg/connector/provider/ that exposes
a private xxxRegistration() *Registration constructor; NewBuiltinRegistry
enumerates them.
CreateConnectorRequest loses its per-provider settings fields and
takes a single RawSettings json.RawMessage produced by the
per-provider MarshalSettings closure. The 1Password SCIM bridge URL
is validated at create time (http(s) scheme + non-empty host) so a
malformed value fails fast at the resolver boundary. The Helm chart
gains probo.connectorEnv and probo.connectorSecretEntries templates
so adding a connector requires zero Helm changes. Access-review name
resolution moves into the same Registration value to keep one
authoritative dispatch table.
Tests cover every Registration (DisplayName, NewDriver wired),
Register error paths (nil, empty Provider, empty DisplayName,
duplicate), All / ProviderDisplayName / ProviderOAuth2Scopes /
ProbeURL hit and miss paths, the ApplyOAuth2Defaults templating and
PKCE branches, and ConnectorSettings[T] round-trip plus malformed-JSON
error path. The pre-refactor ApplyProviderDefaults test in
pkg/connector is replaced by the equivalent in
pkg/connector/provider.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
CI's staticcheck does not treat t.Fatal as terminating, so every
`err.Message` deref sitting after a separate `if err == nil { t.Fatal(...) }`
block is flagged SA5011 (possible nil dereference). Collapse all 26
occurrences in TestNoHTML / TestPrintableText / etc. into a guarded
`} else if`, matching the fix already applied to validator_format_test.go
in bca36636f. The deref now lives in a branch where err is provably
non-nil, which satisfies SA5011 regardless of t.Fatal recognition.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>