Commit Graph

709 Commits

Author SHA1 Message Date
Cursor Agent
fe215efbd1 Add audit start and end dates to audits
ISO audits often span a window distinct from certificate validity.
Store optional audit_start_date and audit_end_date on the audit
record and expose them through GraphQL, MCP, CLI, n8n, and console.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:33 +02:00
Cursor Agent
3f545652d5 Use business campaign rules instead of status errors
Introduce accessreview.Campaign with draft/deletable predicates and
operation-specific client errors. Drop errUnlessDraftCampaign and
status-to-sentinel switches in the service layer.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
09f131087c Map cancel campaign terminal errors to invalid
Return GraphQL invalid when cancel hits completed or cancelled
campaign status sentinels from the service layer.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
c187ea7124 Remove CampaignStatusErrors slice
List campaign status sentinels explicitly at resolver AnyOf
call sites instead of a shared slice.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
f8dcb6c014 Add errorx.AnyOf for sentinel error checks
Centralize errors.Is OR chains in access review campaign resolvers
and expose CampaignStatusErrors for the four non-draft status
sentinels.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
b3e60da4d5 Use status-specific errors for non-draft campaigns
Replace ErrCampaignCannotStart and ErrCampaignCannotUpdate with
sentinels per campaign status, mapped through CampaignStatusError
and wrapped with operation-specific fmt.Errorf prefixes.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
3d606dd0e8 Replace ErrCampaignNotDraft with operation-specific errors
Use ErrCampaignCannotStart and ErrCampaignCannotUpdate with clear
actionable messages instead of a generic draft-status sentinel
wrapped in noisy fmt.Errorf chains.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
eabd0fb659 Rename ErrCampaignNoSourcesSelected to ErrCampaignMissingSources
The failure is about unconfigured campaign sources, not an empty UI
selection.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
eebc957d36 Treat invalid campaign sources as not found
Drop ErrCampaignSourceOrganizationMismatch. When a source ID is
missing or belongs to another organization, return
coredata.ErrResourceNotFound so clients get a generic not-found
response instead of leaking cross-organization details.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
1dcfa31dab Rename ErrCampaignNoScopeSources to ErrCampaignNoSourcesSelected
Align the sentinel name with the validation it represents.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
0870e1dd15 Use sentinel errors for campaign validation failures
Follow the cookiebanner pattern: grouped var Err* sentinels in the
service package, wrapped with fmt.Errorf where context is needed, and
explicit errors.Is checks in GraphQL resolvers.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
a2b29b3d80 Use typed campaign errors instead of string matching
Introduce sentinel and structured errors for access review campaign
validation failures, and map them to INVALID in GraphQL resolvers via
errors.Is rather than matching error message prefixes.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:07 +00:00
Cursor Agent
ffaf637397 Let n8n create and update campaigns with scope sources
Replace the separate Add Source operation with multi-select source
fields on create and update. Create passes accessReviewSourceIds to
the existing GraphQL input; update gains the same omittable field and
backend source sync so workflows can configure sources in one step.

Load source options from the organization in the n8n UI, and keep
surfacing INVALID errors when start fails for missing sources.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:06 +00:00
Cursor Agent
2d93b1d793 Add scope source operation to access review n8n node
Campaigns cannot be started without at least one scope source, but the
n8n node had no way to attach sources after creation. Expose the
addAccessReviewCampaignSource mutation as an Add Source operation so
workflows can configure sources before starting.

Return INVALID instead of INTERNAL when start fails due to missing
sources or invalid status, so n8n surfaces the real error message.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:06 +00:00
Sacha Al Himdani
d61ec8dd65 Add search and status/role/type filters to People
Makes it practical to find people in larger orgs across GraphQL, MCP, CLI, and n8n, with page size raised to 100.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-27 17:20:18 +02:00
Steven4Hooisma
e62e6cce43 Add UpCloud integration with account listing and details retrieval
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
2026-07-27 11:08:24 +02:00
Aurélien Sibiril
fb68e98941 Split connector extra settings per credential path
Registration.ExtraSettings was a single flat list, but the API-key and
client-credentials connect dialogs need different fields whenever a
provider offers both paths, because a different create resolver and a
different driver sits behind each. Replace it with
APIKeyExtraSettings and ClientCredentialsExtraSettings, and split the
GraphQL surface to match so a client cannot render one path's settings
on the other.

This fixes two connectors that could not be connected at all.

1Password declared accountId and region only, which are the
client-credentials shape. The API-key dialog therefore rendered those
two fields, mapAPIKeyExtraSettingToField returned nil for both so
buildExtraFields discarded them, and the SCIM-bridge driver failed on an
empty SCIMBridgeURL. The console already mapped scimBridgeUrl, but no
registration declared that key, so the branch was dead. It now declares
scimBridgeUrl on the API-key path and accountId + region on client
credentials.

Langfuse declared baseUrl as required, but mapAPIKeyExtraSettingToField
had no LANGFUSE case, so buildExtraFields dropped the value the customer
typed and the mutation failed with "langfuseBaseUrl is required". Every
other extra-settings provider had a case. The GraphQL input field, the
settings struct, the probe builder and the driver were all already
correct; only the console mapping was missing.

buildExtraFields now takes the settings list explicitly instead of
reading it off the provider, so each dialog passes its own path's list
and cannot silently iterate the other one.

Register rejects a settings list for a path the provider does not offer,
and an empty or duplicate setting key within one list. A key repeated
across the two lists is allowed: that is how a dual-path provider
declares a setting both dialogs need.

The new resolver tests walk the whole chain the console walks, from the
key a Registration declares through the mutation input field to the
persisted settings struct, so a key renamed on one side and not the
other fails in CI instead of at connect time.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 15:56:10 +02:00
Aurélien Sibiril
4cbc35e79f Fix the Segment region input and invite duplicates
The region is a two-value allow-list the server resolves to an API host,
but it rendered as a free-text field: only PostHog is special-cased in
the API-key dialog, everything else falls through to a generic Field.
Typing "EU1" — the region Segment's own UI shows for the EU workspace —
passed the non-empty check, then failed the mutation, and the dialog's
generic error blamed the API key. It is a select now, so the label no
longer has to spell the accepted values out.

An invite that has already been accepted can still be listed, and the
member and the invite were keyed differently (user ID vs email), so the
same person surfaced as two rows — one active with roles, one inactive
without. Invites for an email already seen among members are dropped.

Per-user permission errors now name the user, and the probe URL builds
its query with url.Values rather than a hand-written string.

The region-to-host mapping is the only API-key setting that derives a
value instead of storing input verbatim, and it had no test; a typo in
either host would only have surfaced as a live 404.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:05 +02:00
Aurélien Sibiril
60628645ae Add Google Analytics, Dotfile, Segment and Square access-review connectors
Two OAuth2 and two API-key connectors:

- Google Analytics (GA4): OAuth2 with both analytics.readonly and
  analytics.manage.users.readonly (readonly alone 403s on the accounts
  list); v1alpha accessBindings enumerated at account and property level
  and merged by email; manual account picker (Pattern 1) with a
  per-connection probe and name resolver; distinct from Google Workspace.
- Dotfile: API key in the X-DOTFILE-API-KEY header (Pattern 3); GET
  /v1/users (owner/admin, suspended_at) with a static probe.
- Segment (Twilio): Public API token as Bearer with a required Region
  setting (US or EU) mapped to the regional host; GET /users plus per-user
  GET /users/{id} for roles and /invites for pending members; per-connection
  BuildProbeURL.
- Square: OAuth2 (EMPLOYEES_READ) or a personal access token (Pattern 3);
  POST /v2/team-members/search returns email/status/is_owner directly, so no
  role resolution; custom probe and name resolver.

Google Analytics and Square are confidential OAuth clients, wired into the
bootstrap OAuth provider list and .env.example. Segment carries a required
extra setting, so the console add-source dialog maps region onto its
segmentRegion API-key input; without that mapping the value is silently
dropped and the create is rejected.

Cassette-backed driver tests plus unit tests for the Segment probe URL and
the bootstrap OAuth provider list.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:04 +02:00
Ludovic Vielle
6cac6a8775 Split employee devices from DeviceConnection
viewer.enrolledDevices shared DeviceConnection with the admin
org list, so totalCount had to authorize with both
employee-device:list and device:list. Mirror
EmployeeDocumentConnection: a dedicated EmployeeDeviceConnection
without totalCount, and keep DeviceConnection.totalCount for
the org fleet only. Cover assumed-session device:get IDOR in e2e.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-24 15:08:38 +02:00
Ludovic Vielle
238c19d509 Add pre-assume enrolled device status query
The /enroll wait UI polled device state via node(), which
requires an assumed org session, so confirmation never
succeeded for unassumed viewers. Expose viewer.enrolledDevice
behind itam:employee-device:get (own-device, skip assumption)
and point the poller at it.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-24 15:08:34 +02:00
Ludovic Vielle
e767dd8377 Add device enrollment API and agent protocol
Expose ITAM REST endpoints for agents, console GraphQL for device
management, and wire probod bootstrap with enrollment e2e coverage.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-24 15:08:23 +02:00
Aurélien Sibiril
b1a67aba00 Link connector docs in the Add Source dialog
The access review Add Source dialog listed each connector with no
path to its setup documentation. Connectors that have a published
docs page on probo.com now surface a "Documentation" link on the
card, opening the page in a new tab; connectors without a page show
nothing extra.

The link is data-driven from the connector registry: a new
DocumentationURL on the provider Registration, populated for the 12
documented providers via a single accessReviewDocsURL helper, is
surfaced as a nullable documentationUrl on ConnectorProviderInfo and
rendered by the console only when present. This keeps the registry
the single source of truth and adds no client-side provider map.

The links resolve once the probo.com access-review docs pages are
deployed; until then they 404, so deploy the docs alongside this
change.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-24 14:50:18 +02:00
Aurélien Sibiril
a0ead155db Trim access-review connector comments
Cut verbose inline comments across the access-review connector changes:
the mechanical fact stays at the read site, while incident backstory
("millions of error logs in prod") and provider-specific rationale (why
Clerk reviews the wrong population) move to the commit history where they
belong. Also tighten a loose "a 4xx" to "an auth/not-found 4xx" so the
terminal-classification contract is not overstated. No behavior change.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
d18127503d Pin Vercel OAuth callback team parameter name
The Vercel connector reads the team from the camelCase teamId callback
parameter, unlike the snake_case params most providers use. Nothing
guarded the name, so a regression back to team_id would silently drop
the team on every Vercel connect and leave the source resolving no
users.

Extract the read into vercelCallbackTeamID and pin the exact parameter
name with a test, since the surrounding handler needs a live connector
registry and database to exercise directly.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
2971637c72 Default access source org across GraphQL and MCP
Org-defaulting for picker providers only ran in the GraphQL resolver,
so a picker-provider source created or updated through the MCP API
connected fine but resolved no users until the org was picked. Move
the defaulting into the accessreview service as
AutoSelectDefaultOrganization and call it from both surfaces, moving
the providerOrgConfigs picker dispatch alongside it (the three console
picker resolvers now dispatch through service accessors, behavior
unchanged).

Also harden the moved path: resolve the provider from cheap connector
metadata before building the authenticated HTTP client, so the ~50
non-picker providers no longer pay a decrypt/refresh/DB-write on every
create/update; bound the outbound ListOrgs call with a 10s timeout so
a hung provider cannot stall the mutation; and re-check inside the
ConfigureAccessReviewSource tx (OnlyIfUnset) so an org the user picks
while ListOrgs is in flight is not overwritten by the first listed
org.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
f711e9d816 Reset access source name sync on connection change
Once the source-name worker sets name_synced_at, nothing cleared it,
so a source that hit a terminal failure (Brex 403, wrong Sentry org
slug) kept its generic name forever — even after the user reconnected
with the right scope or picked the correct org, contradicting the
Brex guidance to reconnect.

Clear name_synced_at whenever the connection changes: on UpdateSource
when a connector is (re)set, inside ConfigureAccessReviewSource when
the org is (re)selected, and on the OAuth reconnect path via the new
ResetSourceNameSyncForConnector service method. The worker then
re-claims the row and re-resolves the display name.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
aeb7a1c8e6 Default first workspace for picker access sources
Picker providers (GitHub, Sentry, GitLab, Bitbucket, Heroku, Asana,
Netlify, ClickUp, DocuSign) require the user to pick an org/workspace in
a follow-up step after connecting. When that step is skipped, the source
stays connected but unconfigured, and its first campaign silently
resolves no users because the driver needs an org — the same "connects
fine, campaign fetches nobody" symptom seen on Sentry.

When a connector is linked to a source, auto-select the first workspace
the connection can list, so the source is usable immediately. An org the
user already chose is never overridden, and the picker stays visible
(the frontend shows the selector whenever an org is selected) so they
can switch when several are listed.

Best-effort: a provider that is unreachable or lists nothing leaves the
source in its existing needs-configuration state rather than failing the
create/update mutation.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
0da9e3c5c4 Read Vercel team from the teamId callback param
Vercel appends the customer's team to the OAuth completion callback as
teamId (camelCase), but the handler read query.Get("team_id"). The
lookup always missed, so every team install fell through to the
/v2/user personal-account fallback -- which returns 404 for a
team-scoped integration token -- leaving TeamID empty. The Vercel
driver then refused to build ("team_id is required") and every
access-review campaign targeting a Vercel team fetched zero accounts.

Personal-account installs were unaffected because they never send a
teamId and legitimately use the /v2/user path, which is why the
breakage only showed up for team installs.

Read the parameter under its real name so the team is captured and
persisted in the connector settings.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Émile Ré
0d832508c9 Replace portal title with entity name
Store a short entity name instead of the full home
heading so orgs can brand portals for sub-entities.
Restore hero i18n composition and keep the English
document title composed from the entity name.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-22 13:08:02 +02:00
Émile Ré
9146a4ac1d Update gqlgen generated comments
Signed-off-by: Émile Ré <emile@probo.com>
2026-07-22 12:53:45 +02:00
Bryan Frimin
3f6795001a Finish compliance portal rename leftovers across portal, trust, and n8n.
Update mailing list queries to currentCompliancePortal, and include regenerated console resolvers plus sorted n8n/frontend imports from the rename.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:27 +02:00
Bryan Frimin
8773a54396 Rename GraphQL APIs to compliance portal
Update console, visitor, MCP, and Slack API
surfaces so schemas and resolvers use the
Compliance Portal naming consistently.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:24 +02:00
Bryan Frimin
b03acbd029 Fix step-ca root CA access on Linux CI
Host chmod cannot open 0700 dirs owned by UID 1000, so
`-f` never sees root_ca.crt. Chmod from inside the
container as root while waiting for the cert.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:22 +02:00
Bryan Frimin
caeb6d9225 Resolve custom domain SSL via certificate
Replace flattened SSL fields with a certificate
relation loaded through certmanager, and resolve
domain slots from IDs already on the trust center
instead of reloading the compliance page.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:21 +02:00
Bryan Frimin
bc128ec516 Port trust center features after main rebase
Move Emile's commitment CRUD into complianceportal management,
wire console and visitor GraphQL, and drop portal magic-link
sign-in in favor of OAuth /initiate while keeping documents,
NDA/full-name gates, and access-request resume markers.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:21 +02:00
Bryan Frimin
7e0d187dcf Flatten compliance portal package layout
Remove the root complianceportal package and the resolver
facade that existed only to break an IAM import cycle. Admin
policies, domain URL helpers, and actions live under
management; visitor OAuth metadata, brand URLs, and public
read paths live under visitor. Drop the duplicate trust API
magic-link mutations now that Connect handles portal auth, and
stop IAM from owning compliance page email branding.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:20 +02:00
Bryan Frimin
ba53c94bdc Move trust center profile onto the page
Store website, email, and headquarters on the trust center so
public and admin surfaces read branding from one place. Drop the
trust API organization type and wire console, MCP, CLI, and apps
through the updated schema.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:10 +02:00
Bryan Frimin
ce1b64b529 Update GraphQL, MCP, and CLI for the portal
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>
2026-07-21 15:44:08 +02:00
Cursor Agent
a3fac4f543 Add rights requests to webhooks
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>
2026-07-21 13:24:23 +02:00
Émile Ré
6623cbc6f2 Add data request pages to compliance portal
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>
2026-07-20 14:31:42 +02:00
Émile Ré
0b146a4054 Add configurable compliance portal commitment cards
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>
2026-07-16 15:07:29 +02:00
Sacha Al Himdani
936162d5c7 Resolve document publish approvers from defaults
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>
2026-07-16 14:17:59 +02:00
Sacha Al Himdani
4c57d201a4 Make license declarations consistently MIT
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>
2026-07-13 16:21:14 +02:00
Aurélien Sibiril
5b83911269 Log missing Crisp managed key before failing
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>
2026-07-11 18:41:28 +02:00
Aurélien Sibiril
557028e327 Hide managed connectors until fully configured
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>
2026-07-11 17:55:33 +02:00
Aurélien Sibiril
37121e7bac Verify Crisp website ownership before connecting
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>
2026-07-11 16:50:32 +02:00
Aurélien Sibiril
b408aab59d Add Scaleway, Yousign, Railway and Crisp access-review connectors
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>
2026-07-10 18:29:54 +02:00
Émile Ré
c9b74d6de0 Filter trust center subprocessors server-side
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>
2026-07-09 09:51:37 -04:00
Sacha Al Himdani
f83b42d2ec Rename signature filter state field to profileState
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>
2026-07-07 16:06:56 +02:00