The home page frames its content in labelled sections, so a bare inline
error floated as loose text. Wrap the home section error fallbacks
(Compliance, Trusted by, Recent updates) in a shared InlineErrorCard so a
failed section reads as intentional content on a soft card surface.
Standalone list pages (subprocessors, updates) have no such framing, so
ListErrorBoundary keeps a bare inline error, which looks cleaner there.
Signed-off-by: Émile Ré <emile@probo.com>
The compliance-framework tiles read an item fragment that lacked
@throwOnFieldError, so a tile field error slipped past the section
boundary and nulled silently. Mark it so the existing boundary catches
it.
Wrap the "Trusted by" section in its own boundary and mark its fragments
@throwOnFieldError so a references failure degrades to an inline error
instead of crashing to the page boundary. The hero contact row is left
as-is: its fields are optional and already hide on null/error.
Signed-off-by: Émile Ré <emile@probo.com>
Contain field errors on the subprocessors and updates lists to an inline
fallback instead of the whole page, and make the retry actually recover.
Introduce ListErrorBoundary, which keeps refetch above the boundary and
resets only after the network refetch settles (a bare boundary reset
re-reads the same errored record and throws again). Wire the subprocessors
and updates lists to refetch network-only on retry, and mark the item
fragments @throwOnFieldError so a row error lands below the boundary.
Fix the home sections, whose reset-only retry could not clear a field
error from the preloaded query, to reload the page instead. Generalize the
retry guidance in the error-handling guide (reset vs refetch vs reload).
Signed-off-by: Émile Ré <emile@probo.com>
React Router routes render and loader errors to the route-level
boundaries, so the App-level boundary above the router could only catch
provider render failures — which today are trivial — while true bootstrap
failures throw at module load before it mounts. Neither console nor trust
wraps App this way.
Rely on the root route boundary instead and remove the BootstrapError
fallback it used.
Signed-off-by: Émile Ré <emile@probo.com>
The recent-updates rows all come from a single connection query, so a
per-row boundary isolated a failure that the rows actually share and left
an unrecoverable row (its retry only re-read the same errored store).
Let a row's field error bubble to the section boundary instead, so the
whole list degrades and recovers as one unit.
Signed-off-by: Émile Ré <emile@probo.com>
Introduce global, page, and section-level error handling for the
compliance portal so a failure is contained at the smallest possible
scope instead of blanking the whole page.
Add a portal-local Relay fetch that throws only request-level errors
(and always redirects on UNAUTHENTICATED) while leaving field-level
errors in the response, so Relay surfaces them at the reading component
through @throwOnFieldError and the nearest boundary. Add a NotFoundError
for node __typename mismatches mapped to a not-found page.
Ship reusable v2 kit primitives (ErrorBoundary, ErrorState, InlineError)
matching the Figma global/local/inline designs, wire the bootstrap and
route boundaries, and demonstrate section and row boundaries on the home
page. Update the error-handling and relay guides accordingly.
Signed-off-by: Émile Ré <emile@probo.com>
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:
- Convert every source-file header to the MIT text across all comment
styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
"MIT License" title line
- Switch the package.json license fields, Docker image label, and
cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
(Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
the comma-separated years to a hyphenated range
Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
The verification-code copy handler called navigator.clipboard.writeText
directly and relied on the promise rejection for the failure toast. In
an insecure context or an unsupported embedded browser navigator.clipboard
is undefined, so the call throws synchronously before .then and neither
toast fires, leaving the user without the manual-copy guidance. Guard the
access and wrap the call in try/catch, mirroring ScopeDiagram, so the
failure toast is always shown.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Two honest-feedback fixes in the API-key connect dialog. The
connection-failure toast told managed providers (Crisp) to check their
API key, but Crisp shows no key field; it now points them to the
settings and verification code instead. The verification-code copy
button showed a success toast before the clipboard write resolved; it
now confirms the write and reports a failure with a manual-copy hint,
matching how other copy buttons behave.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
AddAccessReviewSourceDialog had grown past a thousand lines, mixing
provider search, the API-key, client-credentials and OAuth connect
flows, and their shared mutation logic in one file.
Extract the per-protocol dialogs into _components, the shared
create-source flow into a _hooks hook, and the settings mappers and
OAuth redirect helper into _lib. Each connect dialog now owns its own
Relay operation. The API-key dialog carries the Crisp managed-connector
flow: it fetches the verification code for the entered Website ID and
distinguishes loading, ready and error states, so a failed fetch shows
an actionable error with a retry instead of a permanent spinner and a
superseded response cannot overwrite the current code.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Four API-key, single-tenant (Pattern 3) connectors:
- Scaleway: secret key in the X-Auth-Token header plus an Organization ID
setting; GET /iam/v1alpha1/users (owner/member, status, two-factor),
per-connection BuildProbeURL.
- Yousign: Bearer API key; GET /v3/users (admin/owner/member, is_active);
production host with a static probe.
- Railway: Bearer account token; GraphQL me{workspaces{members}} aggregated
and deduplicated across workspaces; custom probe, since Railway returns
HTTP 200 with an errors body on a rejected token.
- Crisp: plugin token as HTTP Basic (identifier:key) plus a Website ID
setting and the X-Crisp-Tier header; GET /v1/website/{id}/operators/list,
custom probe and name resolver.
Scaleway and Crisp carry a required extra setting, so the console add-source
dialog maps organizationId/websiteId onto their scalewayOrganizationId and
crispWebsiteId API-key inputs; without that mapping the value is silently
dropped and the create is rejected.
Cassette-backed driver tests plus unit tests for the cross-workspace
deduplication, the probe contracts and the role/MFA helpers.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Surface the organization risk assessments list alongside the risk register
by adding a shared Risks/Risk assessments tab bar to both list pages.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
After allow or deny succeeds, replace the consent form with a full-page
redirect message naming the application and a spinner while the browser
navigates to the OAuth callback URL.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Track a pending allow/deny action with Button loading spinners so
users cannot double-submit while the approveConsent mutation runs or
while the browser waits on the OAuth callback redirect.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Forward className on PaginationSkeleton to match the kit skeleton API,
and move the Next arrow to iconEnd per the forward-arrow convention.
Size the updates list skeleton to the page size so the placeholder no
longer jumps when the loaded page renders, and dispose the detail
query on updateId change to avoid a flash of the previous update.
Signed-off-by: Émile Ré <emile@probo.com>
Generalize the cursor Prev/Next pagination hook into a reusable
useCursorPagination in lib/relay, taking the page size as a parameter,
and keep the updates page size (25) as a feature constant.
Move the list card surface and its loading-dim state into tv variants
behind an UpdatesList component, and lift the detail article layout and
its gold metadata styling into shared variants, so the pages carry only
placement classes. Skeletons reuse the same variants.
Relocate the generic pager labels to the app-root namespace and expose
Intl.DateTimeFormat options on the formatDate helper.
Signed-off-by: Émile Ré <emile@probo.com>
Build the public Updates pages in the compliance portal: a
cursor-paginated list of sent mailing-list updates and a detail view
for a single update, replacing the previous stub page.
Add a MailingListUpdate case to the trust API node resolver, guarded so
only SENT updates belonging to the current trust center's mailing list
are exposed, so the detail page can load an update by URL.
Add a Prev/Next Pagination primitive to the v2 UI kit. Page numbers are
omitted because cursor pagination cannot derive an ordinal page index;
each arrow only shows when its page exists while keeping its slot
reserved so a visible arrow never shifts position.
Relocate the shared MailingListUpdateListItem to its own component
folder and wrap each row in a link to the detail page, so both the home
recent-updates section and the list navigate to detail.
Signed-off-by: Émile Ré <emile@probo.com>
Drop items-start from the category grid so its rows stretch again.
Each card is a block grid item that fills the row height, lining up
the bottom edges of cards on the same row regardless of how much text
each one carries.
Signed-off-by: Émile Ré <emile@probo.com>
Drop the region icon top margin so it centers with its label line,
and stop the card grid from stretching rows. Cards now hug their
content, giving every card a consistent 32px bottom padding instead
of leftover space below shorter cards.
Signed-off-by: Émile Ré <emile@probo.com>
Address the review comments on the subprocessors work:
- Reject invalid category/country filter values in the Subprocessors
resolver with an INVALID error instead of relying solely on transport
coercion, so a malformed request fails fast and explicitly.
- Use pgx.StrictNamedArgs in the new distinct facet queries so missing
or extra SQL placeholders stay detectable, matching sibling queries.
- Default a nil ThirdPartyFilter at the service boundary to avoid a nil
dereference in the coredata list/count paths.
- Expose the category group label as an aria heading for assistive tech.
- Add the missing space in the Select "Selected:" story label.
Signed-off-by: Émile Ré <emile@probo.com>
The subprocessors toolbar derived its category and region filter
options client-side from a second, unfiltered subprocessors(first: 250)
fetch, shipping up to 250 rows purely to compute two small facet sets
and silently capping the options at that limit.
Expose subprocessorCategories and subprocessorCountries on TrustCenter,
each backed by a DISTINCT query over the organization's third parties
scoped to show_on_trust_center, and read them directly in the toolbar.
The page now issues one filtered list query plus two tiny arrays, and
the options only ever include values that can actually return results.
Signed-off-by: Émile Ré <emile@probo.com>
The subprocessor list item, commitment card, and empty state each
inlined layout and color Tailwind classes on className, against the v2
UI rules that keep look-and-layout in tailwind-variants slots.
Move the shared soft-Card frame (a backdrop header faded over a body)
into a presentational BackdropCard, and the icon/title/description
placeholder into a generic EmptyState, both under components/. Refactor
CommitmentCard and SubprocessorListItem onto BackdropCard, and
SubprocessorsEmpty onto EmptyState, leaving only component-specific
slots in each variants.ts.
Subprocessors without a website now show the dotted backdrop behind the
fallback icon, matching the commitment card.
Signed-off-by: Émile Ré <emile@probo.com>
Searching subprocessors flipped the list between filtered and unfiltered
results in an infinite loop: useSubprocessorFilters kept a local search
mirror plus a write-back effect, so every component calling it (page,
loader, toolbar, empty state) ran its own effect while only the toolbar
updated the mirror — the stale instances fought the real writer.
Make useSubprocessorFilters a pure URL-state hook and move the debounced
search input into a single-owner useSubprocessorSearch hook mounted only
by the toolbar, guarding the URL-to-input sync with a ref so its own
commits are not echoed back.
Refetch on filter change now runs inside a transition so the toolbar and
current results stay mounted instead of falling back to the whole-page
Suspense skeleton; only the results container dims while loading.
Signed-off-by: Émile Ré <emile@probo.com>
Subprocessor filtering for the compliance portal happens in the backend
rather than the client. Add a SubprocessorFilter (query, category,
country) to the trust API's subprocessors connection, thread it through
the resolver and service, and extend the coredata ThirdParty filter with
category equality and country array membership. The connection stores the
filter so totalCount reflects the filtered set. Add e2e coverage for the
new filtering.
On the frontend, convert the page to a refetchable fragment whose filter
arguments are driven by URL-persisted, debounced toolbar state (category
and region selects plus a search field), populate the dropdowns from an
unfiltered facet selection, and offer to clear filters from the empty
state.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the Subprocessors placeholder with the real trust-center page from
the Figma design: subprocessor cards grouped by their backend category,
each showing a favicon logo over a blurred backdrop, the name, the
description, and the hosting regions. Country codes render through
Intl.DisplayNames and the section labels/descriptions come from a new
page-scoped i18n namespace.
Migrate the route to the per-resource folder layout (pages/subprocessors/
with its own routes.ts, loader, page, skeleton, _components, _lib, and
_locales) and drop the old flat stub. Filtering is added separately.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the deny-based restriction on granting OWNER with role-scoped
allow policies so authorization fails closed: admins may create and
update memberships only when the assigned role is not OWNER, and the
absence of a target role no longer implies permission.
To keep console UI gating accurate without loosening the base grants,
the permission field gains an optional typed options argument
(PermissionOptionsInput) that forwards target_role into the dry-run
authorization. Only the two role-related console calls (create user,
update membership) pass it; the OWNER option stays hidden for admins via
the existing assignable-roles helper.
Add a non-regression test that an admin cannot promote a member to OWNER
while still being able to change members between non-owner roles.
An organization ADMIN could hard-remove members, including OWNERs, because
removeUser (connect and MCP) only checked the weaker iam:membership-profile:delete
gate. Authorize the owner-only iam:membership:delete instead, and expose the
source attribute on MembershipProfile so the owner grant's non-SCIM condition
can match.
Consolidate ownership-grant authorization into policy for both createUser and
updateMembership: each resolver passes the requested role as a target_role
attribute and ADMIN is denied granting ownership via deny-create-owner /
deny-promote-owner. target_role is distinct from resource.role, which is the
target's current role and guards editing existing owners. With no callers left,
the iam:membership-role:set-owner action (grant and OAuth2 scope) is removed.
Also pass the authorized scope through to the RemoveUser/CreateUser services,
gate the console Remove action on iam:membership:delete, and add regression
tests plus a changelog entry.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Add FERPA and PCI DSS framework datasets (controls plus light/dark
logos), their logo components, and register both in the framework
selector so they can be imported like the existing frameworks.
Co-authored-by: Antoine Bouchardy <antoine@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
The DocumentVersionSignatureFilter exposed a field named `state` that
actually filters on the signatory's profile state, which was ambiguous
next to the signature `states` field. Rename it to `profileState`
(GraphQL) / `profile_state` (MCP) across the schema, spec, resolvers,
console app, and n8n node for clarity.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
The OAuth token and consent UIs fall back to the raw scope string when
a label is missing from scopeLabels. Add read and manage labels for
v1:resource-alias so it matches other API scopes.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Third-party compliance report PDFs were rejected above 10MB in the
upload dialog. Increase the client-side Dropzone limit and enforce the
same cap on the backend file validator.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The compliance page rendered a Slack connector as "Connected" whenever
any SLACK connector existed for the org — including one created for an
access review with no channel configured — and only offered Disconnect,
which deletes the shared connector row.
Distinguish channel-configured connections from unconfigured ones and
let users (re)connect in place to pick a channel without first
disconnecting, reusing the existing reconnect-with-union-scopes flow via
connector_id.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Mirror the risks refactor (c78a713): colocate routes.ts, split the
detail layout query so each child route owns its Loader + Page, rename
tabs/*Tab to resource folders with *Page, move dialogs into
_components/, and extract ThirdPartyRow with its own fragment.
Remove outlet context data passing and deprecated
loaderFromQueryLoader. Delete the monolithic ThirdPartyGraph hook,
colocating each GraphQL operation with its consumer: the create
mutation in CreateThirdPartyDialog (now useMutation + useToast) and
the third-party list queries in ThirdPartiesCell and
ThirdPartiesMultiSelectField (now useQueryLoader + usePreloadedQuery).
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Add a resource-oriented set of webhook events for the document
lifecycle. Each event carries the document plus only the sub-resource
it concerns (version, signature or approval).
Events:
- document.created / updated / archived / unarchived / deleted
- document.version.created / updated / published / rejected / deleted
- document.version.signature.requested / signed / cancelled
- document.version.approval.requested / approved / rejected / voided
Wires the new types through the migration, Go enum, GraphQL schema,
CLI, n8n nodes and the console webhooks settings UI.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Add the frameworks (Compliance), security commitments, trusted-by, and
recent updates sections to the home page, plus a site-wide "Powered by
Probo" footer. Frameworks, references, and updates are relay-driven; the
security commitments section uses placeholder POJO data until a backend
exists.
Keep generic primitives in the v2 kit (ProboLogo, a faint Text tone, and
a soft Card variant with padding="none") and place the product-specific
cards under the app's components folder, composing the base Card. Resolve
each framework's themed logo in the graph via Framework.themedLogoUrl,
mirroring the trust center logo resolver.
Signed-off-by: Émile Ré <emile@probo.com>
Fix the valid issues raised in the scaffold review.
UI kit: the Button loading state now replaces only the leading icon
instead of dropping the label, Button consumes the `active` variant so
it no longer leaks onto the DOM, and every v2 skeleton sets aria-hidden
after the prop spread so a consumer cannot override it.
@probo/relay: guard the caller-supplied onCompleted/onError callbacks so
a throwing callback still settles the awaitable mutation promise instead
of leaving it pending.
compliance-portal: normalize external website hrefs and read hostname
via URL.hostname, add a localized catch-all not-found route, and widen
the .gitattributes glob so colocated __generated__ artifacts at any depth
are marked generated.
Docs: correct the forms guide (Base UI passes plain values, Zod v3
flatten API), spread the child fragment in the permissions example, and
drop references to v2 components that do not exist in the ui guide.
Signed-off-by: Émile Ré <emile@probo.com>
Add a TrustCenter.themedLogoUrl Relay live resolver that reads the
light and dark logos from the graph and subscribes to the system
prefers-color-scheme media query, returning the dark logo (falling
back to the light one) when the OS prefers dark.
The TopBar now selects this single field instead of pulling both logo
URLs and branching on useSystemTheme, keeping the theme-to-URL logic in
the graph where any component can reuse it.
Signed-off-by: Émile Ré <emile@probo.com>
DropdownGroupLabel renders Base UI's Menu.GroupLabel, which requires a
Menu.Group ancestor. The user menu rendered the label directly in the
popup, so opening it threw a missing MenuGroupContext error. Wrap the
email label in a DropdownGroup.
Signed-off-by: Émile Ré <emile@probo.com>
The dark-mode script set the .dark class once at load. Subscribe to the
prefers-color-scheme media query so the theme also updates while the page
is open when the OS appearance changes.
Signed-off-by: Émile Ré <emile@probo.com>
TextSkeleton and HeadingSkeleton baked w-full into their shared base.
Under tailwind-variants/lite (no tailwind-merge) that collided with the
caller's width class and won by stylesheet order, so every skeleton
ignored its width: bars stretched full in column contexts and collapsed
in flex rows.
Drop w-full from the base so the single caller width applies, matching
ButtonSkeleton/AvatarSkeleton. Size the compliance-portal TopBar and
Hero skeleton bars to roughly match their English copy, and add a rule
so skeleton widths are revisited when en-US strings change.
Signed-off-by: Émile Ré <emile@probo.com>
Add a synchronous head script to the compliance portal that sets the
.dark class on <html> when the OS prefers a dark scheme, so the v2 Radix
theme resolves to dark values before first paint with no flash. Declare
a color-scheme meta so native UI matches the active scheme.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the remaining hard-coded UI copy in the top bar, user menu, home
hero, and the documents/subprocessors/updates/requests pages with i18next
keys, and supply the en-US and fr-FR catalog entries for them. The home
hero title uses value interpolation rather than string concatenation so
word order stays correct across locales.
Signed-off-by: Émile Ré <emile@probo.com>
Wire i18next into the compliance portal with a custom backend built on
import.meta.glob, so each _locales/*.json becomes its own lazily loaded
chunk keyed by a namespace derived from the folder path. The active
language is resolved from the browser, collapsing any fr*/en* tag to
fr-FR/en-US with en-US as the ultimate fallback; fallbackLng then only
covers individual missing keys.
Add an app-level default namespace catalog and switch the documents
page title to a translation key to exercise the lazy-loading path.
Signed-off-by: Émile Ré <emile@probo.com>
On a hard reload the root route briefly showed a generic centered pulse
(PageSkeleton) while the layout chunk downloaded, then snapped to the
topbar skeleton. Point the root Fallback at MainLayoutSkeleton so the
chunk-load fallback matches the loader's own skeleton and the transition
is seamless. PageSkeleton is now unused and removed.
Signed-off-by: Émile Ré <emile@probo.com>
Build the Trust Center landing hero and the nav page headers on a shared
HeaderBand shell. The home page loads its own query and feeds the Hero
(org headline, description, and contact info) via a colocated fragment;
the Documents, Subprocessors, Updates, and Data Requests pages use a
PageHeader (size-7 title, optional count, inline actions). Switch the
layout body to the grey sand-2 canvas so the white header bands read as
surfaces on top.
Split the header UI into focused components rather than one
over-configurable Hero: HeaderBand owns the band, Hero the landing
content, PageHeader the nav-page content. Add a hostname helper under
lib/url and standardize the 1024px container on the max-w-5xl token.
The Documents/Subprocessors filter/search/tabs toolbars and item counts
are deferred until the matching v2 components and queries exist.
Signed-off-by: Émile Ré <emile@probo.com>