The supervisor was a hand-rolled polling, semaphore, and wait-group
loop predating the project's adoption of the shared worker kit. Two
sibling workers in pkg/probo already use the kit, and go-worker.md
documents it as the project convention.
This commit introduces agentRunHandler, which implements
worker.Handler[coredata.AgentRun] and worker.StaleRecoverer, and
reduces AgentRunSupervisor to a thin wrapper that owns the handler
plus a worker.Worker and bridges ctx cancellation into a handler-
level shutdown broadcast via context.AfterFunc. The agent stop
channel is now closed by a per-Process forwarder goroutine when the
broadcast fires, so in-flight runs checkpoint at the next turn
boundary and drain through wg.Wait before Run returns.
The stop_requested column, struct field, supporting SQL, and the
LoadRunningStopRequestedIDs function are removed end-to-end. None
of it was ever wired to an external surface; it existed purely to
let the supervisor find runs the operator wanted to halt. With the
kit handling the polling cadence and the AfterFunc bridging
shutdown, per-row flagging is dead weight.
The supervisor's public API (NewAgentRunSupervisor, Run, the With*
option helpers, and the error sentinels) stays intact so probod.go
needs no change. The integration test now triggers stop by
cancelling the supervisor context, which is the actual production
path through SIGTERM rather than a synthetic DB flag. Prometheus
counters and OTel spans labelled worker="agent-run-supervisor"
come for free.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Create agent_runs table with lease-based concurrency control.
AgentRun entity follows standard coredata patterns with Scoper,
StrictNamedArgs, and cursor pagination. PGCheckpointStore implements
agent.CheckpointStore backed by the checkpoint JSONB column with
version validation and 10 MiB size guard. Register AgentRunEntityType
as entity type 75.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Include a `bid` field in the consent cookie so it explicitly
identifies which cookie banner it belongs to, making validation
direct instead of relying on the visitor ID as an implicit
discriminator. Existing cookies without `bid` self-heal on the
next load by falling through to the API fetch.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a required cookie_policy_url alongside the existing
privacy_policy_url (now optional) so banners can link directly to a
dedicated cookie policy — a compliance best practice recommended by
CNIL, ICO, and the EDPB. Existing rows are seeded from their current
privacy_policy_url value.
Both {{cookie_policy_link}} and {{privacy_policy_link}} placeholders
are supported independently in banner description translations.
Signed-off-by: Émile Ré <emile@getprobo.com>
Origin is a fundamental identity property of a banner tied to consent
records for a specific site. Changing it would break the audit trail
and violate GDPR consent specificity requirements.
Signed-off-by: Émile Ré <emile@getprobo.com>
When DNS verification, CAA checks, or HTTP challenge completion
fail during certificate provisioning, the error is now stored on
the custom domain record and exposed via GraphQL. The console
displays it in both the domain card and domain detail dialog so
users can diagnose configuration issues without checking logs.
Previously these failures returned an error that was only logged
server-side, leaving users with no visibility into why their
domain was stuck in a pending state.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Add a partial unique index ensuring only one category per banner can
have posthog_consent enabled. Default it to the analytics category on
banner creation, clear the previous mapping before setting a new one,
and restrict the toggle to NORMAL categories in both the service layer
and the console UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add PostHog opt-in/opt-out consent support mirroring the existing Google
Consent Mode integration: database column, GraphQL field, console UI
toggle, and client-side posthog-js calls.
Extract both GCM and PostHog logic from CookieBannerClient into a
ConsentIntegration plugin interface so future integrations can be added
without modifying the client core.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include gcm_consent_types in the published banner config
so the cookie banner SDK can read category-to-GCM mappings
from the config endpoint.
Signed-off-by: Émile Ré <emile@getprobo.com>
Stores the Google Consent Mode v2 consent type mapping
per cookie category so each category can declare which
GCM signals it controls (e.g. analytics_storage,
ad_storage).
Signed-off-by: Émile Ré <emile@getprobo.com>
Address locale normalization for region-tagged values, guard
language detection for non-DOM runtimes, validate DefaultLanguage
on update, pass translated texts through the deactivation flow,
handle slug collisions in migration, add organizations FK, fix
consent migration from name-keyed to slug-keyed data, render all
template placeholders in previews, and wrap helper text for i18n.
Signed-off-by: Émile Ré <emile@getprobo.com>
The category slug provides a stable, URL-safe key used as the
data-cookie-consent attribute value and consent data key, replacing
the fragile category name. This prevents breakage when categories
are renamed.
- Add slug column with unique-per-banner constraint and backfill migration
- Add Slug validator (lowercase alphanumeric + hyphens)
- Propagate slug through GraphQL schema, service layer, and snapshot
- Update console UI with slug field in create/edit forms
- Switch cookie-banner widget to use slug as consent data keys
Signed-off-by: Émile Ré <emile@getprobo.com>
Add cookie_banner_translations table to store per-language
translations as JSONB, and a default_language column on
cookie_banners. Extend the version snapshot types to carry
translated UI strings and category content per language.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move PDF generation from synchronous publish flow to a background polling
job. Published versions with file_id IS NULL are picked up by the job,
which generates the PDF, uploads to S3, and links the file. Export PDF
now serves stored files for published versions (with optional signature
page and watermark) and generates on the fly for drafts.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
When a user's email is renamed in the identity provider (e.g. Google
Workspace), the external ID stays the same but the email changes. The
SCIM CreateUser now falls back to external ID lookup when no profile is
found by identity, and reassociates the existing profile to the new
identity instead of failing with a 409 uniqueness error.
Also removes user emails from bridge sync error messages to avoid
logging PII, using external IDs instead.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
- Add scope parameter to UpdateShowBranding to prevent cross-tenant updates
- Use cmd.Context() instead of context.Background() in proboctl CLI
- Drop SQL column default after backfill in migration
- Add bounds check for int-to-int32 conversion in PG_POOL_SIZE
- Update branding link to getprobo.com homepage
Signed-off-by: Émile Ré <emile@getprobo.com>
Internal-only flag (defaults to true) that controls whether
"Powered by Probo" branding appears on the cookie banner.
Read directly from the live row (not the version snapshot)
since branding is cosmetic, not consent-relevant.
Signed-off-by: Émile Ré <emile@getprobo.com>
Subsequent fetches of the same source re-upsert every access entry.
Without a guard, a reviewer's (or an agent's) decision, its note, and
the flags that drove it would be silently replaced on every poll.
The ON CONFLICT DO UPDATE SET clause now refreshes only the columns
that track live source state (email, full_name, role, is_admin, MFA,
etc.) and deliberately omits the verdict bundle -- flags, flag_reasons,
decision, decision_note, decided_by, decided_at -- so the audit trail
stays consistent with whatever the reviewer or agent last recorded.
Tighten the signature from pg.Querier to pg.Tx to match the rest of
the coredata package -- every caller already runs inside WithTx.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Remove assets from the snapshot system and replace with a publish-based
document workflow that generates versioned ProseMirror documents.
- Remove snapshot_id/source_id from asset and asset_vendor models
- Delete AssetFilter (no longer needed without snapshot filtering)
- Add PublishAssetList service, GraphQL mutation, MCP tool, CLI command,
and n8n operation
- Add asset_list_document_id column to generated_documents table
- Generate ProseMirror documents with asset inventory tables
(name, type, amount, data types stored, owner, vendors)
- Add AssetListDocument resolver on Organization type
- Update frontend to remove snapshot routes/params and add publish dialog
- Add e2e tests for asset publish (immediate, with approvers, reuse, RBAC)
- Add migration script for converting legacy asset snapshots to documents
- Exclude ASSETS from snapshot type lists and e2e snapshot tests
- Move generated_documents SQL to coredata methods on Datum and Asset
- Clear generated document and SOA references on soft delete and archive
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Two bugs caused SCIM sync failures:
1. buildUserPayload conditionally omitted empty fields. When a field was
cleared in the identity provider, the PUT payload didn't include it,
so the SCIM handler never cleared the stored value. The bridge kept
detecting a mismatch every sync cycle, causing a perpetual PUT loop.
Fix: always include all fields unconditionally.
2. ListUsers ignored the startIndex parameter — the cursor always started
from nil, so every page returned the same first N users. Organizations
with more than 100 SCIM-managed users never got a full listing; users
beyond the first page appeared missing, causing CreateUser calls that
failed with 409 (uniqueness conflict) and eventually disabled the
bridge. Fix: replace cursor-based pagination with OFFSET/LIMIT to
honor SCIM's 1-based startIndex.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Address PR review comments: capture Exec result and check
RowsAffected() == 0 to return ErrResourceNotFound in
Cookie.Update, CookieCategory.Update, and CookieCategory.UpdateRank.
Also update coredata and relay contributor docs accordingly.
Signed-off-by: Émile Ré <emile@getprobo.com>
Drop the boolean implemented/not-implemented state in favor of a
mandatory CMMI maturity level enum (NONE, INITIAL, MANAGED, DEFINED,
QUANTITATIVELY_MANAGED, OPTIMIZING) stored as a Postgres enum type.
The migration backfills existing rows (NOT_IMPLEMENTED → NONE,
IMPLEMENTED → INITIAL), makes the column NOT NULL, and drops the old
implemented column and its enum type.
- maturityLevel is required on CreateControlInput and non-nullable (!)
in the GraphQL schema
- CLI displays human-readable CMMI labels instead of raw enum tokens
- SOA table and published document use a single Maturity column in
place of the old Implemented + Maturity columns
- Remove ControlImplementationState type and all implemented references
across backend, frontend, CLI, MCP, n8n, and E2E tests
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Adds an optional CMMI 0-5 maturity level field to Control to support
ISO 27001 clause 9.1 effectiveness measurement and HITRUST CSF maturity
requirements. The field is nullable, framework-agnostic, and exposed
across all four API surfaces (GraphQL, MCP, CLI, n8n) plus the
generated SoA document.
Signed-off-by: Alejandro Juan <alejandrojuan@alejandrojuan.com>
Replace the `required` boolean column on cookie_categories with a `kind`
enum (NORMAL, NECESSARY, UNCATEGORISED). The Necessary category remains
undeletable and always-on for consent; the new Uncategorised category is
also undeletable but users can opt out of it.
When a category is deleted, its cookies are merged into the Uncategorised
category (lazy-created for legacy banners that don't have one yet).
Signed-off-by: Émile Ré <emile@getprobo.com>
Category reordering previously required two separate
updateCookieCategory calls to swap ranks, which was not
atomic. Replace with a single reorderCookieCategory mutation
that shifts all affected ranks in one SQL statement, and
remove the rank field from UpdateCookieCategoryInput.
Signed-off-by: Émile Ré <emile@getprobo.com>
Drop meetings and meeting_attendees tables, remove all meeting-related
code across GraphQL, MCP, CLI, N8N, webhooks, frontend, and e2e tests.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Delete duplicate connectors per (organization_id, provider), keeping the
oldest one, then add a unique index to prevent future duplicates.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Mirror the SOA-to-document migration for the data list. Remove data
from the snapshot system and add a publish workflow that generates a
ProseMirror document for the full organization data inventory.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Implement a full OAuth2 2.0 and OpenID Connect 1.0 authorization
server with support for authorization code flow (with PKCE),
refresh token rotation, device authorization grant, dynamic
client registration, token introspection, and token revocation.
Includes database schema, coredata layer, service logic, HTTP
handlers, OIDC discovery endpoint, and JWKS publishing.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Helper for callers that need to know whether a given connector row is
still referenced by any access_sources. Used by the SCIM disconnect
flow, which would otherwise fail with a foreign-key violation when the
connector is shared with an access source.
Mirrors the shape of CountByOrganizationID.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Statements of Applicability are no longer exported as one-off PDFs.
Instead, each SOA owns a persistent document that accumulates versions
over time, following the same publish/approve lifecycle as authored
documents.
Publishing without approvers publishes immediately; publishing with
approvers creates a draft pending approval via the existing quorum
system. SOAs can also store default approvers that are pre-populated in
the publish dialog.
The SOA is removed from the snapshot system — applicability statements
are now queried directly (snapshot_id IS NULL) rather than through
snapshot copies.
A standalone migration script (cmd/migrate-soa-snapshots-to-documents)
converts existing SOA snapshots into documents with proper ProseMirror
content, preserving version history and approval decisions.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Replace the draft-only unique index with one covering both DRAFT and
PENDING_APPROVAL statuses, ensuring a document can have at most one
active version at a time. Deduplicate any existing data before creating
the index, prioritizing PENDING_APPROVAL over DRAFT.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Move clientIP extraction into a reusable pkg/server/api/clientip
package with RFC 7239 Forwarded header support. Add
pkg/server/jsonutil with helpers for common HTTP error responses
(RenderForbidden, RenderInternalServerError, RenderNotFound,
RenderBadRequest) and use them in the cookie banner handlers.
Signed-off-by: Émile Ré <emile@getprobo.com>
Parse only the first IP from X-Forwarded-For to prevent
the full chain from bypassing anonymization. Add secondary
sort key for deterministic consent selection. Reject origins
with empty port suffix in the validator.
Signed-off-by: Émile Ré <emile@getprobo.com>
Implement config, consent retrieval, and consent recording
endpoints for the JS SDK. IP addresses are anonymized (last
octet zeroed for IPv4, /48 mask for IPv6) before storage.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce /cookie-banner/v1/{bannerID}/config endpoint for the JS SDK.
The custom CORS middleware validates each request origin against the
specific banner being requested, preventing cross-customer leakage.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add inherentRiskScore and residualRiskScore to the mutation fragment so
the Relay store gets updated scores. On the backend, use RETURNING to
get the recomputed values from the database generated columns.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Every other LoadAll* method in coredata takes a Scoper parameter
for tenant isolation. LoadAllByCookieBannerID was the only one
that omitted it, making the isolation invariant depend entirely
on callers first loading the banner with a scoped query.
Add scope.SQLFragment to the WHERE clause to match the pattern
used by the paginated sibling LoadByCookieBannerID.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
- Return 400 instead of panicking on invalid organization_id
- Use generic error message for internal failures
- Drop duplicate validation from initiate handler (kept in tx)
- Make preserveConnectionFields mutate in place
- Remove as type assertions in GoogleWorkspaceConnector
- Use sort.Slice instead of sort.SliceStable
- Consistent error prefixes in Slack sender
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>