196 Commits

Author SHA1 Message Date
Sacha Al Himdani
6a4f124adb Replace third-party owners with administrators
Some checks failed
github / Analyze (go) (push) Has been cancelled
github / Analyze (actions) (push) Has been cancelled
github / Analyze (javascript-typescript) (push) Has been cancelled
make / build-apps (push) Has been cancelled
make / probod binary (darwin/amd64) (push) Has been cancelled
make / probod binary (freebsd/amd64) (push) Has been cancelled
make / probod binary (linux/amd64) (push) Has been cancelled
make / probod binary (openbsd/amd64) (push) Has been cancelled
make / probod binary (windows/amd64) (push) Has been cancelled
make / probod binary (darwin/arm64) (push) Has been cancelled
make / probod binary (freebsd/arm64) (push) Has been cancelled
make / probod binary (linux/arm64) (push) Has been cancelled
make / probod binary (openbsd/arm64) (push) Has been cancelled
make / probo-agent (darwin/amd64) (push) Has been cancelled
make / probo-agent (freebsd/amd64) (push) Has been cancelled
make / probo-agent (linux/amd64) (push) Has been cancelled
make / probo-agent (windows/amd64) (push) Has been cancelled
make / probo-agent (darwin/arm64) (push) Has been cancelled
make / probo-agent (freebsd/arm64) (push) Has been cancelled
make / probo-agent (linux/arm64) (push) Has been cancelled
make / probo-agent (windows/arm64) (push) Has been cancelled
make / docker (amd64) (push) Has been cancelled
make / docker (arm64) (push) Has been cancelled
make / snapshot-scan (push) Has been cancelled
make / build-probod (push) Has been cancelled
make / build-probo-agent (push) Has been cancelled
make / lint-go (push) Has been cancelled
make / lint-js (push) Has been cancelled
make / lint-swift (push) Has been cancelled
make / lint-shell (push) Has been cancelled
make / test (push) Has been cancelled
make / test-e2e (push) Has been cancelled
trufflehog / scan (push) Has been cancelled
Migrate business and security owners into a shared administrators list across GraphQL, MCP, CLI, n8n, and the console.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-31 16:48:36 +02:00
Ludovic Vielle
7731566c68 Add soft delete for revoked devices
Admins could only revoke devices, so never-enrolled and revoked
inventory rows piled up with no way to remove them. Soft-delete
is limited to REVOKED devices (revoke first), and ITAM GC now
hard-deletes PENDING/REVOKED orphans with no API key, postures,
or valid enrollment token—including user tombstones without
history.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-30 16:34:16 +02:00
Émile Ré
b10fc55b7f Rename user archive action to deactivate
"Archive" was misleading for users: the action sets a profile to
DEACTIVATED while keeping the person in the organization. Rename it to
"deactivate" across the API, CLI, MCP, n8n, and console UI.

Consolidate the two overlapping operations into a single deactivateUser
backed by the fuller, guarded logic (SCIM guard, last-active-owner
guard, invitation expiry, signature cancellation, membership update,
webhook) and authorized via iam:membership-profile:deactivate. Remove
the archiveUser surface and the thin state-only deactivate path.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-30 09:19:42 +02:00
Émile Ré
4a276e3ef7 Split inactive profile state
Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.

Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.

A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-30 09:19:42 +02:00
Sacha Al Himdani
cd6c46212a Add log export for audit logs and SCIM events
Route audit-log and SCIM-event exports through export_jobs with typed
arguments, an iam BuildAndUploadExport/SendExportEmail implementation,
and a concurrent export-job worker with stale recovery. Stream JSONL via
page.WalkAll into S3, and expose the request flow on console, connect,
MCP, and CLI.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-29 18:57:31 +02:00
Cursor Agent
255bea4738 Make risk delete prove document mapping cleanup
Removing junction rows before deleteRisk hid missing soft-delete
cleanup in e2e. Assert deleteRisk succeeds only after deleteDocument
clears mappings, and document links must be removed first.

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

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:34 +02:00
Cursor Agent
24d137e536 Assert risk documents clear after document delete
The regression test previously passed even when junction rows remained,
because unlink and deleteRisk masked missing soft-delete cleanup.
Query the risk documents connection before and after deleteDocument.

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

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:34 +02:00
Cursor Agent
f451e94c0b Clear document mappings on soft delete
Document soft delete cleared generated-document references but left
control, risk, and measure junction rows pointing at deleted documents.
That blocked risk deletion and made unlinkRisk fail when the document
was already gone.

Remove entity mappings in SoftDelete and BulkSoftDelete, drop mappings
before deleting a risk, tolerate missing documents when unlinking, and
backfill orphaned junction rows for soft-deleted documents.

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

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:32 +02:00
Ludovic Vielle
ad615a47a0 Show posture values and report history
Pass/fail was the main device UI signal, but operators need
the agent's observed value. Expose a formatted value per
check, show current postures on the device page, and replace
the Postures tab with paginated report history grouped by
agent push time. Status stays in the model for later rulesets.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-29 16:27:23 +02:00
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
Émile Ré
d08e4c3314 Expect verified email in OAuth2 ID token e2e
E2E owners confirm email during setup so password re-login works,
so ID token claim assertions must expect email_verified true.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-28 15:11:06 +02:00
Émile Ré
5d0882778f Gate password sign-in on email verification
Unverified password identities were able to open sessions after
signing out. Reject sign-in with EMAIL_NOT_VERIFIED and add a
resend-confirmation flow so users can complete verification.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-28 15:11:05 +02:00
Émile Ré
d0c9327e99 Require explicit portal access request IDs
Drop the request-all shortcut so callers always name the
documents, reports, and files to request. TopBar Get Access
now only signs in; bulk selection is the multi-resource path.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-27 18:27:21 +02: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
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
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
É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
Bryan Frimin
c770935433 Update e2e tests for compliance portal
Rename trust center coverage to compliance
portal so console, MCP, and trust suites assert
against the new API surface.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:26 +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
ebe6192a0c Update e2e tests and n8n node for the portal
Follow the new domain model in tests: drop organization profile
assertions, add a trust center profile test, and hit the dedicated HTTPS
listener with SNI for the visitor API. Mirror the custom link rename and
profile field moves in the n8n node operations.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:09 +02:00
Cursor Agent
d2286aa1ac Tear down workflows when archiving documents
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>
2026-07-20 12:13:41 +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
f604c48686 Archive SCIM users with in-use profiles instead of 500ing
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>
2026-07-15 15:44:29 +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
d5102eac63 Add name resolver and Crisp verification tests
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>
2026-07-11 18:41:40 +02:00
Sacha Al Himdani
86c45875a4 Whitelist ownership grants via allow policies
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.
2026-07-08 18:41:13 +02:00
Sacha Al Himdani
ff9cb881e8 Enforce owner-only member removal and ownership grants via policy
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>
2026-07-08 17:59:45 +02:00
Sacha Al Himdani
36c9636168 Add blank line before assignment in user test
Satisfy wsl_v5 which requires whitespace between a var block and
the following assignment statement.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-03 17:44:20 +02:00
Sacha Al Himdani
2ffeb7f3e8 Require set-owner authorization to create OWNER membership
An organization ADMIN could mint an OWNER membership via createUser, which
only gated iam:membership-profile:create and bypassed the owner-only
iam:membership-role:set-owner check that updateMembership already enforces.

Gate the requested role in both createUser entry points (connect resolver
and the MCP CreateUserTool) with an additional set-owner authorization when
the role is OWNER, mirroring updateMembership. Add a regression test that
locks the ADMIN/OWNER privilege boundary the fix relies on.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-03 17:37:26 +02:00
Bryan Frimin
bea410fbb7 Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-03 11:53:57 +02:00
Bryan Frimin
b03dad70e0 Add e2e tests
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-03 10:13:00 +02:00
Ludovic Vielle
888fa4d63a Return conflict when removing a referenced person
Deleting a profile still referenced elsewhere (for example as an
asset owner) surfaced an internal error. PostgreSQL reports ON DELETE
RESTRICT blocks as SQLSTATE 23001, not 23503; map both in profile
delete and propagate ErrProfileInUse through removeUser as CONFLICT.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-30 14:11:46 +02:00
Cursor Agent
9e47aba2b6 Fix access review test whitespace
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-06-30 14:11:10 +02:00
Cursor Agent
79285d97df Remove access review framework controls
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-06-30 14:11:09 +02:00
Ludovic Vielle
2ba8464d4e Fix advertised scopes for oauth protected resources
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-25 10:23:30 +02:00
Sacha Al Himdani
612e9cbc22 Move pending signature requests on minor publish
When a new minor version is published, still-REQUESTED signature requests
on the previous version now move onto the newly published version, keeping
the same signature row so the notification schedule (count and last-notified
time) is preserved. SIGNED signatures are left untouched.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-06-24 19:43:29 +02:00
Ludovic Vielle
897ca031e2 Fix e2e tests following oauth2 scopes
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-23 19:17:56 +02:00
Bryan Frimin
79c284a894 Cover resource aliases in end-to-end tests
Replace the trust center alias console and MCP end-to-end tests with
resource alias equivalents exercising the new mutations and tools.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-06-22 11:40:17 +02:00
Bryan Frimin
9a56311d60 Add end-to-end tests for trust center aliases
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-06-22 11:38:51 +02:00
Bryan Frimin
5b0d3e5052 Add OAuth2 Client ID Metadata Document support
MCP connectors such as ChatGPT and Claude register via HTTPS
client_id URLs instead of pre-provisioned GIDs. Fetch and cache
their metadata documents, upsert clients on first use, and
advertise CIMD in OIDC discovery when allowed URLs are configured.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-06-19 17:13:37 +02:00
Ludovic Vielle
fd2e0903ee Register API scopes on prb CLI OAuth client
Device logins only requested OIDC scopes while the authorizer now
gates API calls on v1:* scopes. Register the full scope set on the
well-known prb client, request it at login via CLIClientScopes, and
cover the device flow in e2e.

Collapse API scopes under an accordion on the consent screen and
document scope sync for future namespace additions.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-18 20:11:55 +02:00
Ludovic Vielle
26c5002932 Add identity-scoped OAuth token management
Let users create, list, and revoke manual bearer tokens from
/me/oauth-tokens, scoped to their identity rather than an
organization. Manual tokens store a null client_id and are
authorized with a self-manage IAM policy.

Wire Connect GraphQL on Identity (list, create, revoke), add
console UI with scoped create flow and credentials dialog, and
cover the flow in e2e tests. Fix list pagination ordering and
keep the Relay connection in sync after create.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-18 20:08:49 +02:00
Ludovic Vielle
3ebb221a9b Add OAuth2 API scope registration and enforcement
Register v1 API scopes in coredata, advertise them in OIDC discovery
and protected-resource metadata, show them on the consent screen, and
enforce scope-to-action mapping in the IAM Authorizer before policy
evaluation.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-18 19:07:25 +02:00
Émile Ré
cf8a9dc635 Seed attribution in common tracker pattern helper
The common_tracker_patterns table now carries a NOT NULL attribution
column, but the e2e seed helper still inserted without it, breaking
TestThirdParty_ImportFromCommon and the catalog-link tracker pattern
test with a not-null constraint violation. Seed the column with the
UNDETERMINED verdict to match a freshly catalogued row.

Signed-off-by: Émile Ré <emile@probo.com>
2026-06-18 17:23:37 +02:00
Sacha Al Himdani
f462b124e6 Batch signature and approval notifications via debounced worker
Replace the immediate per-document approval email and the manual
"send signing notifications" action with a single debounced worker that
batches pending requests per recipient and organization.

The worker (go.gearno.de/kit/worker) polls on an interval (default 5m)
and claims one (organization, recipient) group at a time, sending one
consolidated signing email and/or one approval email per recipient/org
that lists every document awaiting their signature or approval. The
claim is a conditional UPDATE that doubles as concurrency-safe dedup, so
several workers never email the same group twice.

Each request is notified once it has been pending past the debounce
delay (default 15m), then reminded at 1x, 2x and 3x the reminder
interval (default 1 day) after the previous email, after which it stops.
New last_notified_at and notification_count columns on signatures and
approval decisions drive the debounce, the widening reminder cadence and
the four-email cap.

Email copy lists each document with its title, type and a deep link to
the employee page. Removed the inline approval-on-publish email, the
SendSigningNotifications service method/mutation/MCP tool, its IAM action,
and the related console UI and n8n operation.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-06-18 15:39:04 +02:00
Émile Ré
829771b7cc Supply enrichment_attempts in tracker pattern seed
The migration dropped the DEFAULT on
common_tracker_patterns.enrichment_attempts, so the column is now
NOT NULL with no default and inserts must set it explicitly. The e2e
seedCommonTrackerPattern helper still did a raw insert without it,
which broke TestThirdParty_ImportFromCommon and
TestTrackerPattern_CommonTrackerPatternID with a 23502 violation. Pass
0 for the seeded rows.

Signed-off-by: Émile Ré <emile@probo.com>
2026-06-16 17:44:57 +02:00
Sacha Al Himdani
bf20ca1a90 Add esign to document signatures
Employee document signatures recorded an acknowledgment with no
cryptographic proof, unlike document approvals which already create
and accept an electronic signature on every decision.

Mirror the approval flow on the sign path: generate the signed
document PDF, create-and-accept an esign record, and persist its id
on the document_version_signatures row through a new
electronic_signature_id column. Capture the signer IP and user agent
in the resolver, and re-check the published/archived preconditions
inside the transaction so the seal cannot race document state.

Make the consent wording a single backend source of truth shared by
the text that is sealed and the text shown in the UI. Define
DocumentSignatureConsentText and DocumentApprovalConsentText in the
probo service package and the NDA copy in the trust service, each
owned by the flow that uses it, and stop esign from appending the
generic clause to caller-provided consent text so approvals no
longer seal a duplicated sentence.

Expose the resolved consent text through GraphQL on
EmployeeDocumentVersion and DocumentVersionApprovalDecision, and have
the signing, approval, and NDA pages render it from the API instead
of hard-coded strings, mirroring how the NDA page already worked.

Align the wording with the actual interaction: the buttons read
"Review and sign" and "Review and approve", the clauses reference
those actions, and the inaccurate "typing my full name" phrasing is
removed everywhere.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-06-15 19:18:42 +02:00