The risk overview cards label the combined likelihood × impact
value as Severity; use Score instead for consistency with the
rest of the risk page.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
resolveGateRedirect only supplied window.location.href to gateRedirectPath
from two call sites; drop the extra indirection and call gateRedirectPath
directly in the root and page error boundaries.
Signed-off-by: Émile Ré <emile@probo.com>
Route the full-name and NDA gates from the request-access flows to their
gate pages (deep-linking with the deferred continue URL) instead of a
dead-end toast, so signing or naming resumes the original request; the
shared gate-to-route mapping now lives in one helper reused by the route
boundaries and both request hooks.
Fix the NDA page redirecting to home while also redirecting to the
continue URL once the signature is sealed, surface consent/accept
failures so the sign button isn't silently inert, and build the
request-all continue URL before clearing its marker.
On the backend, return success from updateFullName when the identity has
no organization profile instead of dereferencing a nil profile, which
crashed external trust-center visitors completing the full-name gate.
Signed-off-by: Émile Ré <emile@probo.com>
Handle NDA_SIGNATURE_REQUIRED and FULL_NAME_REQUIRED the way the trust
app does: the Relay fetch throws the typed errors and the route error
boundaries redirect to /full-name or a new self-contained /nda page,
carrying a continue URL. The request-access hooks move both gates to
onError accordingly (NDA is toast-only, matching trust, since the query
boundary is its primary path).
The NDA page is styled like the document viewer: a header band with the
title, org subtitle, consent, and sign action, over the NDA PDF, with the
same page-navigation and zoom controls. It records the signing events,
accepts the electronic signature, polls until sealed, then returns to the
continue URL.
Also fall back to the email in the top-bar user menu when a member has no
full name yet, and hoist the shared PDF-viewer control labels into the
app common namespace (deduplicating the document viewer and NDA copies,
and collapsing a duplicated common block in the locales).
Signed-off-by: Émile Ré <emile@probo.com>
The document, report, and file "Get Access" buttons were inert. Wire
them to the per-resource access mutations, and gate unauthenticated
requests behind the sign-in dialog: a signed-out click defers the
request in the continue URL and resumes it after sign-in, mirroring the
top-bar request-all flow. Extend useResumeAccessRequest to complete the
deferred per-resource requests and route through the full-name gate.
Signed-off-by: Émile Ré <emile@probo.com>
The form and its Relay operation were named after the Figma "Login Form"
layer, but the rest of the feature speaks "sign in" (SignInDialog,
auth.signIn.*, the visible copy). Align the name so the dialog and its
form share one prefix.
Signed-off-by: Émile Ré <emile@probo.com>
The skeleton frame carries the dialog's shadow-6 elevation, which only
reads correctly against the dimmed overlay. Render the story over a
simulated backdrop so the preview matches how the dialog appears.
Signed-off-by: Émile Ré <emile@probo.com>
Cover the new v2 kit primitives in Storybook: the Dialog (uncontrolled
trigger, controlled open state, and skeleton) and the Toaster (title-only
toasts as the common case, plus a title+description variant).
Signed-off-by: Émile Ré <emile@probo.com>
The compliance portal's "Get Access" button was inert and the portal
had no way to authenticate or request trust-center access. Add a modal
sign-in flow (magic link + OIDC) that gates the requestAllAccesses
mutation, mirroring the trust app's flow but as a dialog instead of a
full /connect page.
Introduce the two v2 UI-kit primitives this depends on: a headless
Base UI Dialog and a styled Toaster (mutation toasts had no host yet).
Wire the top-bar button to open the dialog, resume the deferred access
request once authenticated, and add standalone routes for magic-link
verification and the full-name gate.
Signed-off-by: Émile Ré <emile@probo.com>
Groups with no cards render nothing, but the section still reserved its
eyebrow slot for the first group by index and kept its padded wrapper,
leaving a stray "Security Commitments" label misplacement and an empty
gap when every group was empty.
Filter out cardless groups before rendering so the eyebrow lands on the
first visible group and the whole section collapses when nothing renders.
Signed-off-by: Émile Ré <emile@probo.com>
Let admins reorder commitment groups and the cards within each group from
the console Commitments tab using up/down buttons, driven by the existing
rank-aware update mutations.
Make the (parent, rank) unique constraints on the commitment tables
DEFERRABLE INITIALLY DEFERRED. Reordering shifts several rows in one
UPDATE, which transiently duplicates a rank and tripped the immediately
enforced constraint with a 23505 error. This matches the other
rank-ordered tables (references, compliance frameworks).
Signed-off-by: Émile Ré <emile@probo.com>
The compliance portal home page rendered security-commitment cards from
a hardcoded placeholder POJO. Back them with real, per-organization data
that admins configure in the console and the portal loads over the trust
center GraphQL API.
Model two entities under the trust center: a commitment group (title,
description, rank) and a commitment card (icon, eyebrow, title,
description, rank). The card icon is a curated enum mapped to a Phosphor
icon in the portal. New entities adopt the compliance_portal_ prefix as
the start of the broader rename away from trust_center_ naming.
Expose the groups and cards read-only on the public trust API and with
full CRUD on the console API, add a Commitments tab to the compliance
page, and replace the placeholder section with a Relay-driven one.
Signed-off-by: Émile Ré <emile@probo.com>
The MCP publishDocument tool required callers to pass approver_ids and
to distinguish an omitted list (rejected) from an empty one (direct
publish), a null-vs-empty subtlety that is awkward for an LLM to get
right.
Drop approver_ids from the MCP tool and resolve a major publish's
approvers from the document's default approvers instead: an approval is
requested when the document has default approvers, otherwise the version
is published directly. Default approvers are configured with addDocument
or updateDocument.
Replace a document's default approvers on every major publish that
supplies an explicit list, even when the list is empty, so a direct
publish through the GraphQL API clears stale approvers instead of
leaving them behind (previously the empty case skipped the update).
Expose the default-approver behaviour as a separate entry point,
PublishVersionWithDefaultApprovers, that loads the defaults and
delegates to PublishVersion. PublishVersion keeps its explicit-approver
contract for the GraphQL API.
Require only the publish permission to publish a version, whether or not
it opens an approval quorum, and drop the now-unused request-approval
action.
Fold the publish steps into the publishMinor and publishMajor primitives
shared by both the single and bulk publish paths, and drop the redundant
InTx suffix from RequestApproval and emitDocumentEvent, which already
take a transaction argument.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Dispose the viewer query when the alias changes so navigating between
documents shows the skeleton instead of the previous document. Guard the
export completion against the current id so a slow export can't overwrite
a newer document's preview. Split the documents tab state into requested
and loaded refs so an out-of-order or failed refetch can't leave the list
showing a different tab than the toolbar.
Signed-off-by: Émile Ré <emile@probo.com>
Add a full-page viewer at /documents/:alias that resolves the aliased
node, exports its watermarked bytes, and renders them: PDFs via react-pdf
with page navigation and zoom, images inline, and a download fallback for
other file types. Unauthorized visitors see a locked state.
Wire the documents list "View" action to link into the viewer (fragments
now select alias) and drop the previous open-in-new-tab helpers, since the
viewer owns the export.
Bound MainLayout to the viewport so the top bar and footer stay fixed and
the page area scrolls on its own; the viewer then keeps its toolbar fixed
while the PDF body scrolls, matching the design.
Add the react-pdf dependency with the pdf.js worker bundled via Vite (for
CSP safety) and a headless v2 Separator kit component for the toolbar.
Signed-off-by: Émile Ré <emile@probo.com>
Fold the review feedback from the documents page work into the shared
components and helpers:
- Toaster: give the close control the kit's focus-visible ring, and wrap
the title/description in Toast.Content so stacked toasts get Base UI's
height measurement and overflow handling.
- TabsSkeleton: spread rest before the fixed aria-hidden so the
decorative subtree can't be exposed to assistive tech.
- DocumentsPage: reconcile the active tab against the tab the preloaded
query actually loaded with, so a tab change during the initial preload
no longer shows the wrong slice.
- Grouping: drop the duplicated bucketing helpers and reuse the shared
groupBy from @probo/helpers, keeping field-specific sorting/labeling in
the callers.
- Documents list items: extract a shared DocumentEntry row and a
useExportAndOpen hook so the three item components stop repeating the
export/access behavior while keeping their own fragments.
- Subprocessors skeleton: reuse SelectSkeleton/TextFieldSkeleton instead
of hand-rolled placeholders.
- useDocumentTab: derive DocumentTab and URL validation from a single
DOCUMENT_TABS source of truth.
Signed-off-by: Émile Ré <emile@probo.com>
Give the toolbar a 64px min-height so its controls are vertically
centered, and make the header band flush at the bottom. This widens the
gap above the toolbar and trims the oversized space below it to match
the Figma hero. Mirror the header in the skeleton so it does not jump
when data loads.
Signed-off-by: Émile Ré <emile@probo.com>
The results container hand-wrote its layout classes and toggled the
dimmed state with a string-interpolated conditional. Move both the page
shell and the busy/dimmed state into a documentsLayout tv variant, and
reuse it in the skeleton so the loading and loaded layouts share one
source of truth.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the minimal placeholder Toaster with the richer implementation
already built on compliance-portal-auth (per-type icons plus warning and
info variants, and its stories). Both branches now share the exact same
component and app-root wiring, so they converge instead of colliding
when they merge. Align App.tsx provider ordering to match.
Signed-off-by: Émile Ré <emile@probo.com>
Build the Trust Center documents page: a unified list of published
documents, uploaded files, and audit reports, grouped into category
sections. An All/Public/Private tab bar filters the list by trust
center visibility.
Expose that filter over the trust v1 API by adding a
TrustCenterVisibility enum and a shared TrustCenterVisibilityFilter
input, wiring it through the documents, audits, and trustCenterFiles
connections down to the existing coredata SQL filters. "All" keeps the
default public+private slice; the other tabs pin a single visibility.
Access controls are display-only for now (auth is handled separately):
authorized or public entries open their exported PDF via the export
mutations, requested entries show a pending state, and everything else
shows an inert Get Access affordance.
Add the v2 Tabs and Toaster kit components (Base UI headless) needed by
the page and mount a toast provider at the app root for mutation
feedback.
Signed-off-by: Émile Ré <emile@probo.com>
Upgrade the indirect golang.org/x/text dependency from v0.38.0 to
v0.39.0 to remediate CVE-2026-56852. Running go mod tidy also carried
along golang.org/x/tools and golang.org/x/telemetry to their matching
versions.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
- Log delete failures in deleteSubscription instead of swallowing them
- Correct codex node prefix to @probo/n8n-nodes-probo.proboTrigger
- Replace unsupported codex categories with Development and Utility
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
When a SCIM hard delete targets a profile that is still referenced
(e.g. completed document version signatures, FK RESTRICT), profile.Delete
fails with 23503 and poisons the surrounding transaction. The existing
deactivate fallback then ran on the aborted transaction and failed with
25P02, surfacing to the connector as an opaque 500 and eventually
disabling the bridge.
Wrap profile.Delete in a savepoint so the FK violation only rolls back
the delete attempt, leaving the outer transaction healthy for the
deactivate/archive fallback. Also map FK violations in Membership.Delete
to ErrResourceInUse for consistency with MembershipProfile.Delete.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Update webhook events now carry a top-level "updatedFrom" field
alongside "data", containing a full snapshot of the entity as it was
before the update. This lets subscribers diff old vs new state (for
example the prior membership role on user:updated) without tracking
prior state themselves. It is a complete snapshot with the same shape as
"data", not a partial diff, so consumers select whatever fields they
need. The field is omitted for non-update events.
The webhook_data table gains a nullable updated_from JSONB column, and
webhook.InsertUpdateData enqueues both snapshots; InsertData delegates to
it with a nil updatedFrom so non-update callers are unaffected. Each
*:updated emission site snapshots the entity right after load, before
mutation: obligation, third-party, user (org and SCIM flows), document,
document-version, and document-version-approval-quorum. The document
emit helpers gained an optional updatedFrom argument threaded through to
the payload.
For document-version-approval-quorum:updated the snapshot requires an
extra query, so it is now gated behind the same subscription-existence
check the emitter uses: when no subscriber is configured the load is
skipped entirely rather than running (and potentially failing the
approval) for an event nobody receives.
Add integration tests (against a real Postgres, skipped when none is
reachable) covering the updated_from round-trip, the SQL NULL behavior
when no snapshot is provided, and the no-op when no subscription matches,
plus a unit test asserting updatedFrom is omitted from the payload when
absent.
Document the new field in the probod and n8n changelogs and the n8n
README.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Guard ErrorBoundary against a falsy thrown value: track a separate
hasError flag and accept the caught value as unknown, so throwing null,
0, or "" still renders the fallback instead of looping back into the
failing subtree.
Add role="alert" to InlineError and ErrorState so assistive tech
announces errors that appear dynamically after a fetch or query failure.
Fix the RecentUpdatesSection doc example, which showed onRetry={reset}
with @throwOnFieldError even though reset cannot clear a field error;
use a reload and point to the retry table.
Signed-off-by: Émile Ré <emile@probo.com>
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 nOAuth fix set trustProviderEmail to false, requiring the
email_verified claim. Microsoft never emits that claim, so the check
rejected every legitimate Microsoft login before the xms_edov check
was reached.
Restore trustProviderEmail to true and keep the required xms_edov
claim, which is the actual nOAuth mitigation: Azure sets it only after
verifying the issuing tenant owns the email's domain, so a token
lacking it is still rejected before any identity is matched.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Stop trusting the email on its own: set trustProviderEmail to false so
email_verified is required, and additionally require the "xms_edov"
claim, which Azure sets only after verifying the issuing tenant owns
the email's domain. A token that lacks it is rejected before any
identity is matched.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
RequiresManagedResourceID only has meaning for a ManagedAPIKey provider:
ManagedConnectorReady consults it exclusively on that path. A non-managed
provider that set it would advertise normally with the requirement
silently doing nothing. Reject the combination at registration, matching
the ManagedAPIKey/SupportsAPIKey mutual-exclusion guard already in
Register, and cover it with a TestRegistry_Register subtest.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.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>
Cover gaps the connectors shipped without: the Railway and Crisp name
resolvers had no tests despite real branch logic (single vs multiple vs
zero workspaces, best-effort on error, empty-website short-circuit,
tier header), and the crispVerificationCode query was only unit-tested
at the HMAC layer, never through the live schema and authorization
stack.
Add TestRailwayNameResolver and TestCrispNameResolver alongside the
existing resolver tests, pin the Crisp driver's hardcoded MFA Unknown
and nil Active, and add an e2e TestCrispVerificationCode asserting the
code shape, determinism, organization binding, blank-input INVALID, and
viewer FORBIDDEN. The verification-code query needs no Crisp credentials
(only the always-set token secret and organization authorization), so it
runs against the default e2e deployment.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The Crisp ownership check returned a generic internal error when the
managed plugin token was unset without logging server side, unlike the
sibling plugin-ID branch and every other internal path in the file. A
deployment with the token unconfigured but the provider somehow surfaced
would produce an undiagnosable error. Log the condition first, mirroring
the plugin-ID branch.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The Crisp and Railway connectors each repeated their HTTP plumbing
across the driver, the name resolver, and (for Crisp) the subscription
settings fetcher: the same JoinPath/headers/Do for Crisp GETs and the
same marshal/POST/headers for Railway GraphQL. The Crisp and Scaleway
drivers also carried byte-identical owner/member role mapping and admin
checks.
Extract crispGet and railwayPost as package-private request helpers so
each call site owns only status handling, and lift the owner/member role
mapping into shared ownerMemberRoles/isOwnerRole helpers beside
activeFromStatus in driver.go. Name the Crisp base URL and tier header
as consts in probe.go's const block rather than inlining the literals,
matching the file's existing convention. Behavior is unchanged.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Match the Crisp and Yousign drivers: move the inline owner check out of
the AccountRecord literal into scalewayIsAdmin, alongside the existing
scalewayRoles/scalewayActive helpers. Behaviour is unchanged (only the
organization owner is an administrator).
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
IsAdmin was the one derived AccountRecord field computed inline in the
struct literal, while Crisp's roles and full name already go through
helpers and the sibling Yousign driver uses a yousignIsAdmin helper. Move
it to crispIsAdmin for consistency; behaviour is unchanged (only the
website owner is an administrator).
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>
A managed (Model B) connector like Crisp needs both the Probo-held key
and a resource ID (the plugin ID) to connect, but the driver catalog
gated visibility on the key alone. A deployment that set the key without
the plugin ID (reachable through raw JSON config; the bootstrap env path
already requires both) would show Crisp as connectable and then fail
every attempt with an internal error.
Add a RequiresManagedResourceID flag to the registration and a
Registry.ManagedConnectorReady check that requires both before a managed
provider enters the catalog, so a half-configured provider stays hidden
instead of dead-ending at connect.
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>