Rewire the console and visitor resolvers onto the management and visitor
services with compliance-portal authorization. Rename the GraphQL and MCP
ComplianceExternalURL type to ComplianceCustomLink, expose trust center
profile fields, default and custom domains, public URL, and the managed
flag, and drop the profile fields from the organization surface.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Rename the compliancepage HTTP package to complianceportal and drop the
slug-based id middleware. Compliance pages are now served exclusively
over SNI-based custom-domain HTTPS, with the SNI middleware resolving the
page by TLS server name and redirecting secondary domains to the
canonical host.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Wire the certificate manager and trust center base domain into IAM so
organization creation provisions a managed default domain and certificate
atomically. Email presenters in IAM and mailman resolve public URLs
through the compliance portal resolver and read profile fields from the
trust center. probod initializes the certmanager service and injects the
new management and visitor services.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Split the compliance portal into an admin-facing management side and a
public-facing visitor side under pkg/complianceportal. Trust-center CRUD,
domains, custom links, frameworks, files and accesses move out of
pkg/probo, and the visitor read logic moves out of pkg/trust. IAM actions
migrate onto compliance-portal scopes.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Rename the compliance external URL concept to compliance custom links,
and move the public-facing profile fields (description, website, email,
headquarter address) off the organization onto the trust center. Backfill
managed default domains for pages that lack them.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Replace the Provisioner and Renewer with poll-based provision and renew
workers orchestrated by a certmanager Service. Certificate operations are
now hostname-centric and driven by the certificates table, decoupled from
custom-domain business logic.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Pull SSL/ACME state out of custom_domains into a dedicated hostname-keyed
certificates table, and repoint cached certificates from the domain to
the certificate. Custom domains now reference a certificate id, keeping
certificate provisioning decoupled from domain business logic.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Custom domains previously hung off the organization. Attach them to the
trust center instead, adding default and custom domain references plus a
managed flag, and backfill existing rows. Domains now belong to a
compliance page rather than the whole organization.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Introduce the trust center base domain setting (default probopage.com)
so managed default domains can be minted for every compliance page.
This configuration is a prerequisite for the domain-ownership migration
and the default-domain provisioning that happens at organization
creation.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Emit lifecycle events for rights requests created through either the
console or compliance portal. Keep webhook subscription choices in sync
across every API and client surface.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Sacha Al Himdani <SachaProbo@users.noreply.github.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
OAuth2 tokens with v1:compliance-page could not create commitment
groups or items because the IAM actions were never listed in
OAuth2ScopeMappings. Document the mapping step so MCP/API work
does not skip it again.
Signed-off-by: Émile Ré <emile@probo.com>
Sync GraphQL commitment group and item operations
to the remaining API surfaces so automation can
manage compliance portal commitments end to end.
Signed-off-by: Émile Ré <emile@probo.com>
Embed and build @probo/compliance-portal for the
/trust path and custom-domain SPA so production
ships the v2 portal. Keep apps/trust in the repo
for local use on port 5175; portal takes 5174.
Signed-off-by: Émile Ré <emile@probo.com>
The previous label implied review plus approval, but the action only
approves. Update the consent text to match the new button wording.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Address PR review: avoid reintroducing cleared URL markers,
treat already-closed sessions as successful logout, and stop
stale subscribe/sign-out completions from racing the UI.
Signed-off-by: Émile Ré <emile@probo.com>
Visitors can subscribe after sign-in via the Updates
CTA or user menu. Also add trust signOut so Log out
works from the menu.
Signed-off-by: Émile Ré <emile@probo.com>
Require a verified viewer email before creating a rights request and
validate the free-text fields with the same SafeText bounds the console
uses, so this public portal mutation stays safe and bounded.
Move myRightsRequests onto the base Query, drop the now-dead count
loaders, and order the RECTIFICATION enum value before PORTABILITY so
the Postgres sort order matches RightsRequestTypes().
Harden the v2 kit primitives: SegmentedControl keeps equal-width cards
(auto-fill), preserves its selection when the active card is toggled,
and forwards an accessible name; Field associates its label and error
by id/aria instead of wrapping the control in a label. Give the type
group an accessible name, require the name field for non-complaint
types, use a timezone-stable reference year, drop the underreporting
header count, and neutralize the response-deadline copy.
Signed-off-by: Émile Ré <emile@probo.com>
Let trust-portal data subjects submit and track GDPR/CCPA rights
requests. The new Data Requests page lists the viewer's own requests
and a dialog submits new ones, scoped server-side to the verified
viewer email so former or inactive users can still exercise their
rights. Submission requires magic-link sign-in (reusing the existing
gate) but not the NDA gate.
Extend the shared rights_request enums with RECTIFICATION, OBJECTION
and COMPLAINT types plus a REJECTED state, and keep the console
GraphQL, @probo/helpers and the MCP specification in sync. Expose a
trust GraphQL surface (myRightsRequests query, createRightsRequest
mutation) backed by a trust service and contact-scoped coredata
loaders.
Add the missing v2 UI kit primitives the dialog needs on top of Base
UI: a SegmentedControl radio-cards group, a form Textarea, and a
Field wrapper.
Signed-off-by: Émile Ré <emile@probo.com>
Archive teardown loaded each document version inside the signature
cancellation loop. Collect version IDs and use LoadByIDs once instead.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Move approval void logic into voidPendingApprovalForLatestVersionInTx
on DocumentApprovalService and group signature cancellation with the
existing cancelPreviousMajorSignatureRequestsInTx helpers. Place
archive-specific InTx orchestration beside Archive and Unarchive.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
LoadRequestedByDocumentID joined document_versions while applying
the unqualified tenant_id scope fragment, which PostgreSQL rejects.
Use the same IN-subquery shape as DeleteRequestedByDocumentID.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Archive and bulk archive now void pending approval quorums and
cancel requested signatures before freezing the document. Unarchive
reverts any leftover pending approval to draft. A migration backfills
archived documents that still carry in-flight workflow rows.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Several risk views already used "Initial" while others still showed
"Inherent". Align user-facing labels across the console, shared UI
components, CLI help, n8n fields, generated documents, and MCP
descriptions. API and database field names are unchanged.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
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>
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>
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>
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>
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>
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>
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>
Crisp is a managed (Model B) connector: Probo holds one plugin token
server-side and each connection carries only a Website ID. Nothing
stops one organization from entering another organization's Website
ID, so prove control of the website before creating the connection.
Probo derives a per-(organization, website) verification code as an
HMAC over the token secret and exposes it through a new
crispVerificationCode query. The customer pastes it into the Probo
plugin's per-website settings; at connect time the resolver reads the
setting back through the managed plugin token and requires a
constant-time match before any row is written. The managed key and
plugin ID come from bootstrap, so the connector stays hidden until the
deployment configures them.
The settings fetch is injected so the create-time gate's branch wiring
is unit-tested (mismatch and not-subscribed reject, internal errors
stay generic, a matching code passes), and the managed-versus-client
key resolution is covered too.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The recorded responses carried only the fields the drivers read.
Rewrite them to match the shape the real Scaleway, Yousign and
Railway APIs return (workspaces, status, account_root_user_id,
timezone-qualified timestamps), so the fixtures exercise the drivers
against realistic payloads. The discriminating assertions (email,
role, is_admin, active, MFA) are unchanged and the driver tests
still pass.
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>
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>
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>
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>
Any self-provisioned trust center visitor could accept another
visitor's NDA signature or inject audit-trail events into it by
supplying its GID, since AcceptSignature and RecordEvent trusted the
client-supplied signature ID without checking it belonged to the
caller (GHSA-22xj-f767-ppw6). SignerEmail/ActorEmail are always
derived from the verified session identity, never client input, so
comparing them against the signature's stored SignerEmail in
pkg/esign/service.go closes the hole at its root without touching the
resolver-level authorization already in place elsewhere.
Adds an e2e regression test that self-provisions two trust center
visitors through the real magic-link flow and confirms one cannot
touch the other's signature.
Signed-off-by: Bryan Frimin <bryan@probo.com>