Pattern merges and adoptions that stay entirely within the
uncategorised category never change the visitor-facing snapshot.
Only trigger ensureDraftVersionForBanner when a merge group
belongs to a consent category.
Signed-off-by: Émile Ré <emile@getprobo.com>
Translations are no longer part of the version snapshot, so the
banner_description text must be validated at write time to ensure
the {{cookie_policy_link}} placeholder is present. Without it
the cookie policy URL silently disappears from the rendered banner.
Signed-off-by: Émile Ré <emile@getprobo.com>
Decouple detection from the banner's active state so admins
can discover cookies before making the banner visible. The
client now starts the detector even when the config endpoint
returns 404, and the detector stops itself if the report
endpoint returns 404 (wrong or deleted banner ID).
Signed-off-by: Émile Ré <emile@getprobo.com>
The uncategorised category is an admin-side inbox for detected
cookies and should not be part of the visitor-facing consent
contract. Filter it out of snapshots so changes to uncategorised
patterns no longer trigger version bumps.
Signed-off-by: Émile Ré <emile@getprobo.com>
Translation changes are cosmetic, not consent-contract changes,
so they should not trigger a version bump. Translations are now
loaded live from the database at serve time instead of being
frozen in the snapshot.
Signed-off-by: Émile Ré <emile@getprobo.com>
A central snapshot-equality guard in ensureDraftVersion now returns
the latest version unchanged when the candidate snapshot matches it,
so no-op admin saves no longer force visitors to re-consent. Per-
operation short-circuits in UpdateCookieBanner, UpdateCookieCategory,
UpdateCookiePattern, DeleteCookiePattern, MoveCookiePatternToCategory,
ReorderCookieCategory, and UpsertCookieBannerTranslation skip the row
update and version bump when nothing visitor-facing changes (excluded
patterns, identical values, identical translation JSON).
Rank is now treated as admin-only metadata: buildSnapshot sorts
categories by (Kind weight, ID byte order) instead of relying on the
implicit rank-driven slice order, and ReorderCookieCategory no longer
calls ensureDraftVersionForBanner. Default banners keep their
visitor-facing order (insertion order matches Kind+ID); banners with
admin-customised ranks see a one-time order shift to insertion order
on the next snapshot rebuild.
Reusable equality helpers (Ptr generic + JSON canonicalisation) move
to a new pkg/equal package; snapshotsEqual stays in service.go as the
documented chokepoint for visitor-identical snapshot comparison.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add excluded boolean to the MCP CookiePattern schema and
UpdateCookiePatternInput. Expose it in CLI cookie-pattern
view, list, and update commands.
Signed-off-by: Émile Ré <emile@getprobo.com>
Expose excluded on the CookiePattern type and accept it as
an optional input on UpdateCookiePatternInput so the console
can toggle pattern exclusion inline.
Signed-off-by: Émile Ré <emile@getprobo.com>
Filter excluded and non-exact patterns in SQL when loading
patterns for the pattern analysis worker. Both merge group
building and uncategorised adoption only see non-excluded
exact patterns, so excluded patterns are preserved as
punch-out overrides.
Signed-off-by: Émile Ré <emile@getprobo.com>
In ReportDetectedCookies, silently skip cookies that match an
excluded pattern instead of recording them. Filter excluded
patterns in SQL when building version snapshots so they never
appear in the published banner config. Add Excluded field to
UpdateCookiePatternRequest.
Signed-off-by: Émile Ré <emile@getprobo.com>
Adds an `excluded` boolean column to the cookie_patterns table
so operators can mark patterns to be omitted from the consent
banner without deleting them. Includes the migration, struct
field, updated SQL queries, and filter support.
Signed-off-by: Émile Ré <emile@getprobo.com>
- Validate __typename in all list command pagination callbacks
- Serialize PostHogConsent=false as explicit false (not nil)
- Fix maxAgeSeconds factory to only include when value is an int
- Rename cookie-banner versions to latest-version subcommand
Signed-off-by: Émile Ré <emile@getprobo.com>
Wire cookiebanner.Service into the MCP resolver and expose 24 tools
covering full CRUD, activation, versioning, translations, and consent
record queries with pagination and filtering support.
Signed-off-by: Émile Ré <emile@getprobo.com>
Users authenticated via Google/Microsoft OIDC or magic link previously
relied on a fall-through in the password-only org check. Make the rule
explicit so SSO-only users can access password-only organizations
without being bounced to the password login form they cannot satisfy.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
The adoptUncategorisedPatterns method loaded all patterns for a
banner then filtered in Go. This adds a CookiePatternFilter
(match_type + cookie_category_id) and wires it into
LoadAllByCookieBannerID so the two targeted loads only fetch
the rows they need.
Signed-off-by: Émile Ré <emile@getprobo.com>
When a cookie pattern already exists for a banner, the insert
conflict caused a continue that silently dropped the detected
cookie instead of linking it to the existing pattern. Load the
existing pattern to obtain its ID and proceed with cookie
insertion.
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the ad-hoc patternAnalysisTask struct with coredata.CookieBanner
as the worker type parameter, matching the pattern used by other workers
(esign, accessreview). SQL methods move back to coredata on *CookieBanner.
Signed-off-by: Émile Ré <emile@getprobo.com>
Use InsertIfNotExists instead of Insert with error check, since
a unique-violation aborts the PostgreSQL transaction even when caught.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move the DurationInput component from the console app into @probo/ui
for reuse, add duration formatting helpers to @probo/helpers, and
update pattern merge to group by both category ID and prefix.
Signed-off-by: Émile Ré <emile@getprobo.com>
- Fix DurationInput fallback unit from "minutes" to "seconds" and add
seconds as a selectable unit to prevent silent duration inflation
- Use parseFloat instead of parseInt for duration input to preserve
fractional values
- Scope prefix merge groups by category ID to prevent cross-category
merging
- Relink cookies and delete exact patterns even when prefix pattern
already exists
- Prefer exact matches and longest prefix in pattern selection query
- Fix wrong error type in GetCookiePattern (ErrCookiePatternNotFound)
- Handle singular/plural in humanizeSeconds fallback branch
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the free-form duration TEXT column with a nullable
max_age_seconds INTEGER on both cookies and cookie_patterns
tables. The SDK detector now sends raw seconds instead of
humanized strings, eliminating locale-dependent comparisons
in the pattern merge worker. Humanization happens at display
time in the widget and console UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
The previous algorithm split on the first separator only, producing
overly broad prefixes (e.g. ph_ for ph_phc_abc123). Replace with a
greedy longest-shared-prefix approach that finds the longest
separator-boundary prefix shared by 3+ exact patterns, avoiding
false merges across unrelated cookie families.
Signed-off-by: Émile Ré <emile@getprobo.com>
The console now manages CookiePattern entities instead of raw
Cookie rows. The frontend queries cookiePatterns on each category
and uses createCookiePattern, updateCookiePattern,
deleteCookiePattern, and moveCookiePatternToCategory mutations.
The entire Cookie GraphQL surface (type, connection, mutations,
inputs, payloads, resolvers, Go types) is removed since the
backing struct already lost description and cookieCategoryID.
Signed-off-by: Émile Ré <emile@getprobo.com>
Set pattern_analysis_requested_at on all existing banners so the worker
runs once per banner on release and merges any existing prefix groups.
Signed-off-by: Émile Ré <emile@getprobo.com>
Background worker polls cookie_banners with pattern_analysis_requested_at
set, groups EXACT patterns sharing a common prefix, and merges groups of
3+ into a PREFIX pattern. Detection sets the flag when new EXACT patterns
are created. The worker relinks cookies, removes orphaned patterns, and
updates the draft version via ensureDraftVersionForBanner.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add CookiePattern type, connection, and mutations to the GraphQL schema
with full resolver implementations. Add service methods for pattern
CRUD, category movement, listing, and counting. This enables the console
to manage cookie patterns instead of individual cookies.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a cookie_patterns table that groups cookies sharing a common
prefix (e.g. phc_*) into a single manageable row. Every cookie now
belongs to a pattern (EXACT or PREFIX match type). Category, description,
and display metadata move from cookies to patterns, making patterns the
unit of management and display in the console and published snapshots.
Signed-off-by: Émile Ré <emile@getprobo.com>
Cookies set by browser extensions are not the website operator's
compliance responsibility. This adds stack-trace inspection to
filter out extension-originated document.cookie writes, and
annotates pre-existing cookies with a source field so operators
can triage them separately.
Introduces a CookieSource enum (SCRIPT / PRE_EXISTING) across
the full stack: PostgreSQL, coredata, service, HTTP handler, and
GraphQL schema. On conflict, source is upgraded from PRE_EXISTING
to SCRIPT when a page script is later observed setting the cookie.
Signed-off-by: Émile Ré <emile@getprobo.com>
Helpers feeding ProseMirror JSON templates under pkg/probo/templates
could return empty strings, producing { "type": "text", "text": "" }
nodes that Tiptap rejects with "Empty text nodes are not allowed" when
the document is loaded into the editor. The backend HTML/PDF export
silently tolerates them, so the issue only surfaces in the UI.
Route every enum format helper's default branch through
stringOrNotSpecified (formatClassification, formatAssetType,
formatFinding{Kind,Status,Priority}, formatObligation{Status,Type},
formatVendorCategory, formatDataSensitivity, formatBusinessImpact,
formatProcessingActivityRole, formatLawfulBasis,
formatSpecialOrCriminalData, formatTransferSafeguard, formatDPIANeeded,
formatTIANeeded, formatResidualRisk), wrap asset DataTypesStored, and
treat an empty FullName as a missing profile in every owner/DPO lookup
(lookupProfileName plus the inline profileMap reads in data, asset,
finding, obligation and processing-activity builders).
Also tweak the vendor list template: insert a horizontalRule after each
record so they paginate cleanly, rename the trailing section to
"Definitions", and drop the now-redundant "3.1 Lexicon" subheading.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Replace the old snapshot-based system for vendors with the publish
document system, mirroring the prior processing activity / DPIA / TIA
migration. Includes the GraphQL mutation, MCP tool, CLI command, n8n
operation, frontend publish dialog, e2e tests, and a prosemirror
register template covering vendor profile fields plus per-vendor
sections for services, contacts, risk assessments, compliance reports,
BAA and DPA agreements.
The vendor register lives as a generated DocumentTypeRegister document
on the organization, reused across publishes (the major version bumps
on every republish). Approvers can be passed in to create a draft
pending approval; otherwise the version is published immediately. The
frontend Vendors page exposes a Publish button and a Document link
button when the document exists, and pre-fills the previous default
approvers.
Remove snapshot mode entirely from vendors and their sub-entities: drop
snapshotId/sourceId from GraphQL Vendor type and VendorFilter; remove
SnapshotsTypeVendors from the snapshot registry and delete
Vendors.Snapshot, VendorSnapshotter interface and all
*.InsertVendorSnapshots methods on contacts, services, risk
assessments, compliance reports, BAA and DPA. Drop the snapshot routes
and banner from the frontend. The snapshot_id columns remain in the
database but are now filtered out with snapshot_id IS NULL.
Add Get/Upsert/Clear GeneratedDocumentID methods on Vendor backed by a
new vendors_document_id column on generated_documents, matching the
ProcessingActivity/Finding/Obligation pattern.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
RFC 7662 lets clients introspect any OAuth2 token, but the endpoint
only resolved access tokens. Look up refresh tokens too, honor the
optional token_type_hint to drive lookup order with a fallback to the
other table, and report revoked or expired refresh tokens as inactive.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Session validity was decoupled from credential rotation: changing
a password (logged-in flow) or completing a forgot-password reset
left every existing iam_sessions row valid until its idle TTL.
A user who saw their account compromised on another device had
no way to actually evict that device by rotating the password.
Inside the same DB transaction as the password update, expire the
identity's other active sessions:
- ChangePassword keeps the caller's current session and revokes
every other session for the identity, so the user is not
logged out of the browser they just used.
- ResetPassword has no caller session (the user is anonymous,
authenticated only by a stateless token) and revokes all of
the identity's active sessions.
The session middleware already rejects rows with expire_reason
set, so revoked sessions are kicked out on the next request
without any middleware change.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
The SCIM event handler read the client address from
r.RemoteAddr only, so when probod runs behind an HTTP
load balancer every event row recorded the load
balancer's internal IP. Switch to clientip.Extract,
which honors the Forwarded / X-Forwarded-For headers
already validated by trustedproxy middleware.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
A client can prepend a spoofed entry to X-Forwarded-For before
the request reaches our load balancer. Taking the first value
would return the attacker's address. Since we sit behind a
single trusted LB that appends the real client IP as the last
entry, switch to rightmost extraction for both X-Forwarded-For
and RFC 7239 Forwarded headers.
Signed-off-by: Émile Ré <emile@getprobo.com>
probod-bootstrap only needs the config struct definitions for
YAML marshaling but transitively pulled in ~40 heavy runtime
dependencies via pkg/probod. Move all config types and their
methods to a new pkg/probodconfig package and re-export them
from pkg/probod via type aliases for backward compatibility.
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the old PDF/snapshot-based exports for processing activities,
Data Protection Impact Assessments and Transfer Impact Assessments with
the publish document system. Includes GraphQL mutations, MCP tools, CLI
commands, n8n operations, frontend publish dialogs, e2e tests, and
prosemirror register templates that mirror the previous PDF layouts.
Each register lives as a generated DocumentTypeRegister document on the
organization, reused across publishes (the major version bumps on every
republish). Approvers can be passed in to create a draft pending
approval; otherwise the version is published immediately. The frontend
ProcessingActivities page exposes a Publish dropdown per register and a
Document link button per active tab, pre-fills the previous default
approvers, and navigates to the published document on success.
Remove snapshot mode entirely from these three entities: drop snapshotId
and sourceId from GraphQL schemas, types, filters, resolvers, MCP spec,
frontend routes and pages; remove SnapshotsTypeProcessingActivities from
the snapshot registry and delete the ProcessingActivities.Snapshot,
ProcessingActivitySnapshotter interface and *.InsertProcessingActivitySnapshots
methods. The snapshot_id columns remain in the database but are now
filtered out with snapshot_id IS NULL.
Add Get/Upsert/Clear GeneratedDocumentID methods on each entity type
(ProcessingActivity, DataProtectionImpactAssessment,
TransferImpactAssessment) backed by new columns in the generated_documents
table, matching the Finding/Obligation pattern.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Batch-load CookieBanner and CookieCategory entities via
dataloadgen instead of making individual service calls in
GraphQL resolvers, matching the existing dataloader pattern
used for organizations, frameworks, etc.
Signed-off-by: Émile Ré <emile@getprobo.com>
Resolvers for CookieCategory, Organization, CookieBanner,
Translations, and CookieBannerVersion.Categories were either
missing authorization checks or returning ID-only stubs
without querying the database. This fixes both issues by
adding proper authorize calls and fetching full entities.
Signed-off-by: Émile Ré <emile@getprobo.com>
Display record attributes and parsed consent data with
per-category consent state and cookies from the banner
version snapshot. The page lives outside the config layout
with its own breadcrumb navigation.
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the opaque cookieBannerVersionId filter with an
integer version filter. The SQL filter now resolves the
version number via a subquery against cookie_banner_versions.
Also fix the CookieBannerVersion resolver on consent records
to load the full version from the database instead of
returning a stub with only the ID set (which caused the
version to always display as 0).
Signed-off-by: Émile Ré <emile@getprobo.com>
Exposes the cookie consent record audit trail through a new
"Consent Records" tab on the cookie banner configuration page.
The full stack includes: extended coredata filter (visitor ID,
banner version), GraphQL schema/types/resolvers, and a React
page with SortableTable (size 50) and three compliance filters
(action, visitor ID, banner version).
Signed-off-by: Émile Ré <emile@getprobo.com>