Introduce /cookie-banner/v1/{bannerID}/config endpoint for the JS SDK.
The custom CORS middleware validates each request origin against the
specific banner being requested, preventing cross-customer leakage.
Signed-off-by: Émile Ré <emile@getprobo.com>
Strip www. prefix and trailing slash from origin when creating or
updating a cookie banner so CORS lookups match regardless of whether
the customer's site redirects www to the apex domain.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move OIDCProviderInfo type into base.graphql alongside its query field
in both connect and trust APIs, removing orphan oidc.graphql files.
Split connect profile.graphql into three domain files: profile (with
user mutations), membership, and invitation.
Signed-off-by: Émile Ré <emile@getprobo.com>
The make relay target merges split graphql files into a single
schema.graphql before running the Relay compiler. All CI workflows,
the generate target, and documentation now use make relay.
Signed-off-by: Émile Ré <emile@getprobo.com>
Relay v20 treats extend type in server schema as client extensions,
causing all mutation fields to have "text": null in generated artifacts.
Replace the cat-based schema concatenation with a merge script that
collects all extend type Mutation blocks and produces a single
type Mutation definition. The split graphql/ files remain the source
of truth for gqlgen; the merged schema.graphql is generated for Relay.
Signed-off-by: Émile Ré <emile@getprobo.com>
Update GNUmakefile targets to use wildcard graphql/*.graphql
prerequisites instead of the deleted schema.graphql files, and
remove v1_resolver.go from build prerequisites.
Also update .gitattributes, package.json graphql.schema path,
and AGENTS.md.
Signed-off-by: Émile Ré <emile@getprobo.com>
Change gqlgen filename_template from {name}.resolvers.go to
{name}_resolvers.go across all three APIs for consistent Go naming.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move Identity, Organization, Viewer, PageInfo, OrderDirection,
CountryCode, OIDCProviderInfo, File, and ReauthenticationReason out
of base.graphql into their own dedicated files across all three APIs.
base.graphql now only contains directives, scalars, Node interface,
Query type, and an empty Mutation type (required by Relay
schemaExtensions). Entity files use extend type Mutation for their
mutations.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move Organization, Identity, TrustCenter, and Viewer definitions to
include all their connection fields directly, removing all extend type
blocks for these hub types from entity files.
This eliminates the Relay schemaExtensions constraint where extend type
could only target types defined in the main schema file. Entity files
now only define their own standalone types and extend type Mutation.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add inherentRiskScore and residualRiskScore to the mutation fragment so
the Relay store gets updated scores. On the backend, use RETURNING to
get the recomputed values from the database generated columns.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Use the shared getSeverity helper from @probo/helpers in SeverityBadge
so the list view displays the same labels (Low/High/Critical) and
thresholds (0/5/15) as the detail view. Also fix the RisksChart legend
and getLevel thresholds to match.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
The dev config uses http://localhost:8080 (plain HTTP), but both session
cookies had secure: true, which tells browsers to only send cookies over
HTTPS. This caused a sign-in loop where authentication succeeded but the
browser dropped the cookie, redirecting back to login. This change sets
secure: false for both cookie configurations in the dev config only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Emily Moir <emi@emimoir.com>
The controls tab used useRefetchableFragment which only showed the
first 20 controls with no "Show more" button, while the tab badge
displayed the actual total count. Switch to usePaginationFragment
to enable proper pagination via the SortableTable component.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Replace the three separate draft mutations (createDraftDocumentVersion,
updateDocumentVersion, deleteDraftDocumentVersion) with automatic draft
lifecycle management inside updateDocument. The backend now auto-creates
a draft when a published document is edited, updates the existing draft
on subsequent edits, and auto-deletes the draft when content reverts to
match the published version.
A new deleteDocumentDraft mutation provides explicit draft deletion.
Backend:
- Merge version-level fields (content, title, classification,
documentType) into UpdateDocumentRequest
- Convert CreateDraft, UpdateVersion, DeleteDraft into private
transaction helpers called from Update
- Update returns (*Document, *DocumentVersion, error) with the version
present only when a draft exists
Frontend:
- Remove all create/update/delete draft mutations from components
- Auto-save via updateDocument with layout refetch on draft status
transitions while preserving editor cursor (data-generation key)
- Title, type, and classification editable on published versions
(backend auto-creates draft)
- Forms use react-hook-form values option to stay synced with Relay
fragment data across draft/publish transitions
API surface (GraphQL, MCP, CLI, n8n) updated consistently:
- Removed: createDraftDocumentVersion, updateDocumentVersion,
deleteDraftDocumentVersion
- Added: deleteDocumentDraft (document-level)
- Updated: updateDocument accepts content, classification, documentType
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Consolidate the two per-app relay configs (console and trust)
into a single multi-project relay.config.json at the repo root
with three projects: core, iam, and trust. Bump all relay
packages from v19 to v20.1.1 and move relay-compiler to the
root devDependencies. Replace per-workspace relay scripts with
a single root-level npm run relay command and update the
GNUmakefile, CI workflows, and docs accordingly.
Signed-off-by: Émile Ré <emile@getprobo.com>
Every other LoadAll* method in coredata takes a Scoper parameter
for tenant isolation. LoadAllByCookieBannerID was the only one
that omitted it, making the isolation invariant depend entirely
on callers first loading the banner with a scoped query.
Add scope.SQLFragment to the WHERE clause to match the pattern
used by the paginated sibling LoadByCookieBannerID.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The connector initiate and complete HTTP handlers used panic for
operational errors (network, DB, provider failures). No recovery
middleware exists on the console chi router, so these panics
produced incomplete responses instead of proper HTTP 500 errors.
Use the same log-and-render pattern already established in
loadExistingConnector error handling.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
- Return 400 instead of panicking on invalid organization_id
- Use generic error message for internal failures
- Drop duplicate validation from initiate handler (kept in tx)
- Make preserveConnectionFields mutate in place
- Remove as type assertions in GoogleWorkspaceConnector
- Use sort.Slice instead of sort.SliceStable
- Consistent error prefixes in Slack sender
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
ApplyProviderDefaults now sets redirect-uri, auth-url,
token-url, extra-auth-params, token-endpoint-auth, and
scopes at startup. Only client-id, client-secret, and
provider-specific settings are needed in the YAML.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Check the ok and error fields from Slack's token
response and reject flows with a missing access token.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Switch to useMutation + useToast with onCompleted and
onError callbacks, matching the pattern used across
the rest of the console app.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The 500 response was wrapping the underlying error with
%w, exposing internal details to the client. Log the
full error, return a generic message.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
sendMessage and updateMessage used LoadAllByOrganizationIDProtocol
AndProvider and then picked connectors[0], which is ordered by
created_at ASC. On an organization with multiple Slack installs
only the oldest install ever received messages — re-installing
Slack to switch channels silently broke because the sort kept
the old row winning.
Switch to LoadOneByOrganizationIDAndProvider, which picks the
widest-scope row with updated_at DESC as the tiebreak. For Slack
the scope sets are typically identical across installs, so the
effective behavior becomes "most recent install wins" — the
expected behavior.
While here, prefix the remaining error messages in this file with
"cannot" to match the project convention.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The initiate handler now looks up the existing connector for the
target (organization, provider) pair, reads its stored scope set
through Connection.Scopes, and unions it with the scopes the caller
passed in the query string. The union is what gets requested on the
OAuth authorization URL, so reconnects never drop a previously
granted scope.
When an existing connector is found the handler also flags the
flow as a reconnect via InitiateOptions.ConnectorID, so the
OAuth2 state carries the id and the callback updates the row in
place. When the provider supports it (Google Workspace), the auth
URL also carries include_granted_scopes=true and the user sees
only the delta on the consent screen.
There is no short-circuit: every initiate click runs the full
OAuth flow even if stored scopes already cover the request, because
scope coverage is an unsafe proxy for token liveness. Revoked
tokens or leftover connectors from deleted access sources would
otherwise be silently reused.
The handler body is extracted to its own file to keep NewMux
readable.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Reconnect now takes a ReconnectConnectorRequest carrying the expected
OrganizationID and Provider. It validates inside the same transaction
that the loaded connector belongs to the requested org, provider and
OAUTH2 protocol before mutating the row. This blocks cross-org and
cross-provider corruption via a crafted connector_id reaching the
OAuth callback through the HMAC-signed state token.
preserveConnectionFields copies fields from the existing connection
onto the new one when the new one omits them:
- OAuth2 refresh_token: Google drops it on incremental-auth reuse
when prompt=consent is skipped.
- Slack webhook URL, channel and channel ID: access review Slack
reconnects without the incoming-webhook scope return a token
response with no incoming_webhook field.
GetByOrganizationIDAndProvider now routes through the widest-scope
coredata loader, and GetWithConnection exposes a by-ID load that
returns the fully decrypted connector so the initiate handler can
read the stored scope set.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
LoadOneByOrganizationIDAndProvider returns the effective OAuth2
connector for an (organization, provider) pair. When legacy rows
leave multiple candidates behind (pre-auto-reconnect installs), it
picks the row with the widest stored scope set and tiebreaks by
most recent updated_at.
This is correct for Google Workspace where the SCIM bridge's
4-scope row must win over the access review's 2-scope row so the
caller always sees a token that can run SCIM operations. The
selector reads the decrypted scope set through Connection.Scopes,
so it is robust against token-refresh updated_at churn that would
mislead a pure SQL ORDER BY.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Extend the OAuth2 connector so a reconnect can request the union of
previously granted and newly requested scopes without losing either.
Four related changes:
- Connection gains Scopes() []string so callers no longer need a type
switch to reach the scope set. OAuth2Connection and APIKeyConnection
implement it; SlackConnection inherits via embedding.
- OAuth2State carries RequestedScopes and CompleteWithState falls
back to it when the provider omits the scope field (RFC 6749 §5.1
allows this when granted equals requested). Without the fallback
the stored Scope would be empty and the next reconnect would have
no diff base.
- providerDefinition gains SupportsIncrementalAuth, set only for
Google Workspace. When the flag is true and the caller passes
InitiateOptions.IncludeGrantedScopes, the auth URL carries
include_granted_scopes=true and the prompt=consent param is
dropped so reuse flows see only the delta consent screen.
- InitiateOptions gains ConnectorID so the reconnect case is passed
explicitly instead of relying on the caller to mutate r.URL.Query.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
ParseSlackTokenResponse treated the incoming_webhook field as
mandatory, which blocked any Slack OAuth2 flow that did not request
the incoming-webhook scope. Access review Slack connects only ask
for users:read and users:read.email and would fail at token parsing.
Treat incoming_webhook as optional: populate SlackSettings when it
is present, leave them empty otherwise. The existing compliance-page
webhook URL is preserved through Reconnect in a later commit.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
ParseScopeString, FormatScopeString, UnionScopes and ScopesCover
encapsulate OAuth2 scope-set arithmetic. ParseScopeString accepts
both the RFC 6749 space-separated form and GitHub's comma-separated
non-compliant form in one pass, so callers can stay format-agnostic.
These primitives are the foundation for scope-preserving reconnect:
later commits compute the union of stored and requested scopes so
a reconnect never drops a previously granted scope.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Slack has two OAuth2 use cases in the app: the compliance page
integration (chat:write, channels:join, incoming-webhook) and the
access review driver that lists workspace members via users.list
(users:read, users:read.email). The per-caller scope refactor
already handled the compliance page but missed the access review
path, leaving it silently broken (zero scopes → missing_scope from
Slack API).
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The provider is always the prefix before "/" in the model ID,
so storing it as a separate field is redundant. Replace the
field with a Provider() method.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The model registry fetches from an external API so it should
not run as part of the regular generate/build pipeline. Replace
the automatic generate dependency with a standalone genmodels
target.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Build registries from inline map literals instead of relying
on generated OpenRouter data that can change between runs.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Replaces []ModelDefinition with map[string]ModelDefinition so
model ID uniqueness is enforced by the data structure itself
and lookups do not require a linear scan.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Always write both true and false values in the generated
registry so unsupported parameters are visible at a glance
instead of relying on Go zero-value defaults.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Lookup by canonical ID, bare name, and normalized form.
Capability assertions for Claude, GPT-5.4, and o3. Edge
cases for empty string and provider-prefix-only inputs.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>