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>
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>
OAuth grant tokens issued before API scope enforcement only stored
OIDC scopes. Set every existing iam_oauth2_access_tokens row to the
full supported scope set so bearer tokens keep working under the
OAuth2 scope gate.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
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>
The Upsert behavior is documented in the Go comment above the
method; inline SQL comments duplicated that and cluttered the
query.
Signed-off-by: Émile Ré <emile@probo.com>
Three paths could leave a catalog row's attribution out of step with its
common_third_party_id. A FIRST_PARTY reclassification in the mapping
worker kept a stale org ThirdParty link instead of clearing it. The
upsert requeued terminal FIRST_PARTY rows for enrichment on a vendor
they never adopt, since the vendor-preservation clause nulls it. And the
proboctl upsert command did not normalize the verdict when an operator
linked or unlinked a vendor without passing --attribution.
Clear the org link on a first-party verdict, exclude FIRST_PARTY rows
from the enrichment requeue, and have the CLI downgrade THIRD_PARTY to
UNDETERMINED on unlink and promote UNDETERMINED to THIRD_PARTY on link.
Signed-off-by: Émile Ré <emile@probo.com>
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>
Mirror the common third party upsert for the tracker pattern catalog so
an operator can curate rows by hand. The command is keyed on the natural
key (tracker type, pattern, max age) rather than a slug, applies each
optional field only when its flag is passed, and reuses the existing
Upsert invariants that protect a terminal FIRST_PARTY verdict and never
clobber a researched description with an empty one. Pass --enrich to
re-arm the row for the async enrichment worker.
Signed-off-by: Émile Ré <emile@probo.com>
The tracker-pattern catalog was binary (linked to a vendor or not), so
generic and first-party artifacts (loglevel keys, wallet-extension keys,
an org's own trackers) were retried forever and, once one row was wrongly
attributed, re-propagated to every organization with no re-check.
Give catalog rows a terminal attribution verdict (UNDETERMINED,
THIRD_PARTY, FIRST_PARTY): FIRST_PARTY short-circuits the whole mapping
pipeline so the artifact is never attributed again. Gate deterministic
vendor adoption behind a trust bar so only curated/operator rows
auto-propagate; lower-confidence agent/heuristic rows are reused as hints
and re-resolved, and an independent agent re-confirmation corroborates and
promotes them. Make the mapping agent emit an evidence source and reject
any attribution that lacks concrete evidence, and let it declare a
first-party verdict. Skip the speculative agent for PRE_EXISTING-source
patterns, whose low signal invites invented vendors.
Add proboctl "ctp mark-first-party" and an --attribution list filter to
audit and remediate existing wrong links, and a cursor rule documenting
migration naming so the timestamp is taken from date -u, not invented.
Signed-off-by: Émile Ré <emile@probo.com>
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>
Map membership, profile, and inactive-user failures from
OpenOIDCChildSessionForOrganization to a generic 404 instead
of 500 so org-scoped OIDC callbacks do not reveal tenant
access details.
Signed-off-by: Bryan Frimin <bryan@probo.com>
OIDC login dropped organization_id before the provider redirect, so
callbacks with an existing matching root session never created an org
child session. Persist organization_id in OIDC state, open the child
session on callback, and forward the parameter from the sign-in UI.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Radix documents the 11/12 APCA guarantee against a step 2 background
only, not step 1. Correct the claim and the example (which used
bg-sand-1), add the precise Lc 60 / Lc 90 targets and a source link,
and note that step 1 is safe in practice but not part of the stated
guarantee.
Signed-off-by: Émile Ré <emile@probo.com>
Make shadow-1 a subtle drop shadow instead of Radix's inset one, so
shadow-1 ... shadow-6 form a single composable drop-elevation ramp
in Tailwind's --tw-shadow slot. Steps 2-6 remain the Radix shadows
(gray mapped to sand).
Add a numeric inset-shadow-1 ... inset-shadow-3 ramp in the separate
inset-shadow-* slot, mirroring the depth of Tailwind's 2xs/xs/sm
defaults but sand-tinted so it adapts to dark mode. Reset both
--shadow-* and --inset-shadow-* so only the v2 scales are exposed;
inset-shadow-none stays available as a static utility.
Signed-off-by: Émile Ré <emile@probo.com>
Apply font smoothing (antialiased / grayscale) and overflow-wrap on
the document base, mirroring the root defaults Radix Themes sets.
Placed in @layer base so any utility can override it. The body font
is left to Tailwind's preflight, which already applies --font-sans.
Signed-off-by: Émile Ré <emile@probo.com>
Move each token layer of the v2 theme into its own file under
packages/ui/src/v2/theme (colors, typography, radius, shadows) and
reduce theme.css to an entry that imports them. The public import
path (@probo/ui/src/v2/theme.css) is unchanged, so adopters and the
docs keep working; this only improves navigability as the theme
grows. No token values change.
Signed-off-by: Émile Ré <emile@probo.com>
Expose the translucent neutral scale as bg-/text-/border-sand-a1
... sand-a12, mapped to the imported --sand-a* variables so they
switch light/dark like the solid steps. Enables overlays, scrims,
and layered tints without hardcoded rgba. Limited to sand; the
other hues keep only their solid steps for now.
Signed-off-by: Émile Ré <emile@probo.com>
Self-host the Inter variable font through @fontsource-variable/inter
rather than a CDN, so the v2 type scale renders in Inter with no
third-party request. Import it from the theme and point --font-sans
at the "Inter Variable" family the package registers.
Add --font-mono as a Radix-style system stack: the design specifies
Menlo, an Apple system font that cannot be self-hosted, so it falls
back per OS (Consolas, Liberation Mono, Monaco, monospace).
Signed-off-by: Émile Ré <emile@probo.com>
Source both token scales from Radix: rounded-1 ... rounded-6 (the
Radix "Medium" radius set, 3-16px) and shadow-1 ... shadow-6
(ported from @radix-ui/themes tokens/shadow.css with the gray
neutral mapped to sand).
Import the sand/black alpha color scales that the shadow layers
reference, and register the shadows via @theme inline so those
alpha tokens carry light/dark switching automatically, the same
way the color scale works. Each scale resets its Tailwind
namespace (--radius-* / --shadow-*) to initial so a v2 build
exposes only the numeric Radix scale.
Signed-off-by: Émile Ré <emile@probo.com>
Define the v2 typography tokens sourced from the Probo Radix UI
Figma file: font sizes 1-9 with paired line-height and
letter-spacing, the Inter sans family, and the light/regular/
medium/bold weights, exposed as text-1 ... text-9 utilities.
Reset Tailwind's default t-shirt type scale (--text-*: initial)
so a v2 build exposes only the numeric Radix scale, mirroring how
the color block wipes --color-*. Letter-spacing is kept in em so
tracking scales with font size, per the Radix specification.
Signed-off-by: Émile Ré <emile@probo.com>
Turn the v2 theme into a build-level standalone color system instead
of a runtime-scoped add-on. Reset Tailwind's default palette and keep
only transparent, black, white, and the Radix scales, so an app opts
into v2 by importing it in place of the v1 theme.
Drop the redundant [data-theme="v2"] block: isolation now comes from
the separate import, and the inline utilities never read those tokens.
Update the v2 color guide to describe build-level isolation.
Signed-off-by: Émile Ré <emile@probo.com>
Introduce a new v2 theme system in packages/ui/src/v2/ using
@radix-ui/colors for sand, gold, red, green, amber, and sky scales.
Colors are imported from the library (with P3 wide-gamut support),
mapped to Tailwind via @theme inline, and scoped under
[data-theme="v2"] for isolation from the existing v1 theme. Dark mode
activates via the .dark class on <html>.
Includes contributor docs (contrib/claude/v2-colors.md) and a Cursor
rule (.cursor/rules/v2-color-scale.mdc) for the color system.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the deprecated @react-email/components dependency (and its
deprecated sub-packages) with the unified react-email package per the
React Email 6 migration, updating all component and render imports across
the email templates from "@react-email/components" to "react-email".
This removes the npm deprecation warnings emitted by the old
@react-email/* packages while keeping the template build output
unchanged.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Route document deletion through explicit confirmation dialogs so single and bulk delete actions require users to acknowledge the destructive operation before the mutation runs.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Sacha Al Himdani <SachaProbo@users.noreply.github.com>
The common tracker pattern "show" only printed attempt counters, so the
enrichment payload — run status, model, agent attribution, and per-field
outcomes — was invisible, and a partial run was indistinguishable from a
complete one. Render that payload the same way the common third party
"show" already does.
Surface the last enrichment attempt timestamp in both catalog list views
so an operator can spot stale or never-attempted rows at a glance.
Signed-off-by: Émile Ré <emile@probo.com>
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>
Three crash- and migration-recovery gaps in the unified enrichment
model left rows stuck or misclassified:
- Upsert re-armed a blank, newly-linked tracker pattern without
clearing its prior enrichment payload. A crash between the worker's
claim and persist then left the row with a stale payload, so the
stale-recovery sweep (which only catches rows with a null payload)
skipped it forever. Clear enrichment on re-arm so the row reads as
not-yet-completed again, and pin the behavior with a test.
- The migration added last_enrichment_attempt_at to
common_third_parties without seeding it. Rows with prior attempts
kept a NULL clock and could never satisfy the stale-reset predicate.
Backfill from updated_at, the historical claim-time proxy.
- The migration switched the tracker-pattern enriched-state source to
the enrichment payload without backfilling rows previously marked by
enriched_at, making already-enriched rows read as unenriched.
Seed a provenance sentinel for rows that carried the old done-flag.
Signed-off-by: Émile Ré <emile@probo.com>
Let an operator create or update a catalog third party and queue it for
the async enrichment worker in one step, so a minimal name/category row
gets its profile, compliance documents, owned domains, and logo filled
in without a separate reenrich call.
Enrichment is armed explicitly via RequestEnrichmentByIDs rather than
through the receiver: the coredata Upsert sets enrichment_requested_at
from the receiver only on insert and leaves it untouched on conflict,
so this is the only path that re-arms an existing row uniformly while
also resetting the attempt budget for a fresh run.
Signed-off-by: Émile Ré <emile@probo.com>
Make common_tracker_patterns and common_third_parties share one
enrichment-tracking model and fix the misleading proboctl status.
Both tables now carry the enrichment JSONB provenance payload, an
enrichment_attempts counter, and a last_enrichment_attempt_at clock.
On common_tracker_patterns the enriched_at done-flag is renamed to
last_enrichment_attempt_at and stamped at claim time, so it is truthful
to "attempt" rather than "success". A row is considered to have been
through the workflow when it carries an enrichment payload, not when a
timestamp is set, which lets stale recovery key off the payload being
absent with budget remaining, exactly like common_third_parties.
The claim path reads the attempt counter and timestamp back via
RETURNING so the in-memory receiver matches the database clock instead
of a separate app-side time.Now.
The enricher builds a per-field provenance payload (description and
third-party outcomes plus the mapping attribution) and persists it via
UpdateEnrichment, named to mirror the common-third-party sibling. The
common pattern enrichment worker gains a max-attempts ceiling so a
permanently failing row stops looping.
proboctl now shows "enriched" only when every field the last run
recorded an outcome for resolved a value, otherwise "partial (X/Y)",
replacing the misleading "enriched (no description)" label.
Signed-off-by: Émile Ré <emile@probo.com>
A token without admin.directory.customer.readonly makes
Customers.Get return 403 forever. Treat that as terminal so
the source-name worker keeps the generic name and stops
retrying.
Signed-off-by: Bryan Frimin <bryan@probo.com>