Skip to content

Compliance Portal Refactor#1476

Open
gearnode wants to merge 37 commits into
mainfrom
new-compliance-portal
Open

Compliance Portal Refactor#1476
gearnode wants to merge 37 commits into
mainfrom
new-compliance-portal

Conversation

@gearnode

@gearnode gearnode commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary by cubic

Moves the public trust center to a dedicated compliance portal with OAuth-based auth and a host-root /graphql, replacing magic links and slug routing. Adds a trust center base domain and per-portal domains, extracts certificate management into a worker-driven certificates flow, and switches local ACME to step-ca with a Caddy HTTP-01 proxy; also verifies OAuth2 ID tokens.

App: console +2623 -1885

  • Unified sign-in for portal authorize; added OAuth client branding and magic-link fallback pages; preserved authorize continue across methods.
  • Split brand into profile, domains (with managed default), visual identity, and custom links; removed the legacy External URLs and domain page; redirected the route to brand.
  • Renamed trust-center graphs and permissions to compliance-portal; added #/lib/relay/useMutation; migrated dialogs/lists, connection keys, and visibility helpers.
  • Moved frameworks and NDA UI into overview components; updated files and third‑parties to new permission strings.

App: trust +67 -414

  • Served from host root and pointed Relay to /graphql; removed path-prefix helpers and the magic-link verify route; simplified connect flow.
  • Read profile/title from the trust center; updated continue-URL handling and navigation.

Other +132 -108

  • Renamed Trust API to pkg/server/api/complianceportal/v1; updated workflows/configs and pointed frontends to /graphql.
  • Introduced a trust center base-domain config and Postgres defaults; moved custom domains onto trust centers with a managed default; extracted SSL state to a certificates table and added a certmanager worker service.
  • Replaced Pebble with step-ca; added a Caddy port-80 proxy for HTTP-01; set default HTTPS to :443 and updated .env/compose.
  • Verified OAuth2 ID tokens (RS256 signature, issuer, audience, expiry) and threaded JWKS/issuer/client ID through handlers.

Written for commit 601182d. Summary will update on new commits.

Review in cubic

@gearnode gearnode force-pushed the new-compliance-portal branch from 29638d3 to eeae51b Compare July 10, 2026 13:18
@gearnode gearnode changed the title WIP Compliance Portal Refactor Jul 10, 2026
@gearnode gearnode force-pushed the new-compliance-portal branch from eeae51b to 8ddbdd7 Compare July 13, 2026 12:18
@gearnode gearnode marked this pull request as ready for review July 15, 2026 12:50
Comment thread pkg/server/api/api.go
@@ -63,6 +63,7 @@ type (
IAM *iam.Service
Trust *trust.Service
ESign *esign.Service
CustomDomain *management.Service

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bad naming.


func NewSecurityHeadersMiddleware(extraHeaderFields map[string]string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style

Comment thread pkg/server/server.go
"go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/agentrun"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/complianceportal/management"
trust "go.probo.inc/probo/pkg/complianceportal/visitor"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad alias name

@@ -24,10 +24,10 @@ import (

"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
trust "go.probo.inc/probo/pkg/complianceportal/visitor"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad alias name

@@ -17,8 +17,8 @@ package slack_v1
import (
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
trust "go.probo.inc/probo/pkg/complianceportal/visitor"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad alias name

gearnode added 21 commits July 15, 2026 14:55
Introduce the trust center base domain setting (default probopage.com)
so managed default domains can be minted for every compliance page.
This configuration is a prerequisite for the domain-ownership migration
and the default-domain provisioning that happens at organization
creation.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Custom domains previously hung off the organization. Attach them to the
trust center instead, adding default and custom domain references plus a
managed flag, and backfill existing rows. Domains now belong to a
compliance page rather than the whole organization.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Pull SSL/ACME state out of custom_domains into a dedicated hostname-keyed
certificates table, and repoint cached certificates from the domain to
the certificate. Custom domains now reference a certificate id, keeping
certificate provisioning decoupled from domain business logic.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Replace the Provisioner and Renewer with poll-based provision and renew
workers orchestrated by a certmanager Service. Certificate operations are
now hostname-centric and driven by the certificates table, decoupled from
custom-domain business logic.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Rename the compliance external URL concept to compliance custom links,
and move the public-facing profile fields (description, website, email,
headquarter address) off the organization onto the trust center. Backfill
managed default domains for pages that lack them.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Split the compliance portal into an admin-facing management side and a
public-facing visitor side under pkg/complianceportal. Trust-center CRUD,
domains, custom links, frameworks, files and accesses move out of
pkg/probo, and the visitor read logic moves out of pkg/trust. IAM actions
migrate onto compliance-portal scopes.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Wire the certificate manager and trust center base domain into IAM so
organization creation provisions a managed default domain and certificate
atomically. Email presenters in IAM and mailman resolve public URLs
through the compliance portal resolver and read profile fields from the
trust center. probod initializes the certmanager service and injects the
new management and visitor services.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Rename the compliancepage HTTP package to complianceportal and drop the
slug-based id middleware. Compliance pages are now served exclusively
over SNI-based custom-domain HTTPS, with the SNI middleware resolving the
page by TLS server name and redirecting secondary domains to the
canonical host.

Signed-off-by: Bryan Frimin <bryan@probo.com>
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>
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>
Introduce the app-bound useMutation primitive and rename the trust center
graph and reference hooks to compliance page, moving the shared reference
dialogs alongside them. Sweep the compliance page subpages onto the new
mutation hook and the compliance-portal permission strings, and strip the
profile fields from the organization settings form.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Decompose the brand page into profile, domains, visual identity, and
custom link sections, and move frameworks and the NDA card onto the
overview page as dedicated fragment components. Remove the standalone
domain page and redirect its route to brand, where domains now live.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Stop relying on the /trust/{id} path prefix in the public trust app and
serve it from the host root, reading contact and profile data from the
trust center instead of the organization. Mirror the same fragment
ownership change in the compliance portal hero.

Signed-off-by: Bryan Frimin <bryan@probo.com>
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>
Register the new provider enum value so compliance portal OAuth
clients can be stored and resolved like other IAM OIDC providers.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Teach CIMD registration and discovery about per-portal client
metadata, and carry portal context through token and ID token
issuance for downstream session creation.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Build and validate client metadata documents for trust center
hostnames so OAuth initiation can publish the correct redirect URIs.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Persist portal hostname and OAuth state on session records so the
callback can bind an authorization code to the correct trust center.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Handle OAuth initiation, callback exchange, and session creation
for anonymous trust center visitors through the portal package.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Drop the one-time session handoff flow now that trust center
visitors authenticate through OAuth against the compliance portal.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Relocate the public trust center GraphQL surface, OAuth handlers,
and SPA serving into the compliance portal API package and remove
the legacy trust v1 server tree.

Signed-off-by: Bryan Frimin <bryan@probo.com>
gearnode added 3 commits July 15, 2026 15:02
Expose per-portal OAuth client metadata from connect and route OIDC
authorization through compliance portal session state.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Create IAM sessions from OAuth callbacks with the portal hostname
and consent data needed by the compliance portal API.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Mount the compliance portal mux on trust center hosts, drop the
legacy trust web server, and share response header helpers.

Signed-off-by: Bryan Frimin <bryan@probo.com>
useCreateTrustCenterReferenceMutation,
useUpdateTrustCenterReferenceMutation,
} from "#/hooks/graph/TrustCenterReferenceGraph";
useCreateCompliancePageReferenceMutation,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trust center vs compliance page vs compliance portal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no new hooks graph file

gearnode added 9 commits July 15, 2026 15:07
Redirect visitors through the compliance portal OAuth initiate
endpoint and remove the verify-magic-link page and routes.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Surface dedicated routes when a portal magic link is expired or
already used, and add the portal login entry point in console.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Update Relay schema paths and HTTP endpoints so the portal app
talks to the compliance portal GraphQL and OAuth routes.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Extend test helpers for portal OAuth flows and cover connect,
callback, and NDA signing against the compliance portal API.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Add the complianceportal Go embed target, wire it into CI and
release workflows, and document the new build entry point.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Pebble skipped real HTTP-01 validation, which hid integration gaps
for compliance-page custom domains. step-ca with a Caddy port-80
proxy exercises the same path production uses while keeping issued
certs persistent across compose restarts.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Update generated test config, loopback HTTPS client, and workflow
root CA wiring for the step-ca ACME directory and port-443 trust
center listener introduced by the new compose stack.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Compliance portal OAuth must call the configured Probo base URL
during connect flows; in local dev that endpoint is loopback and
was blocked by default SSRF protection on the initiate HTTP client.

Signed-off-by: Bryan Frimin <bryan@probo.com>
The compliance portal OAuth callback accepted ID tokens after only
parsing claims, without checking the signature, issuer, audience, or
expiry. Add RS256 verification helpers to the JOSE package, enforce
those checks in ParseIDTokenIdentity, and thread JWKS, issuer, and
client ID through the token response and callback handler.

Signed-off-by: Bryan Frimin <bryan@probo.com>
return payload.data?.oidcProviders ?? [];
}

export default function PortalLoginPage() {

@codenem codenem Jul 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we get portal sign in from console?

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23 issues found across 299 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pkg/server/api/mcp/v1/types/organization.go">

<violation number="1" location="pkg/server/api/mcp/v1/types/organization.go:23">
P2: NewOrganization no longer maps Description from the coredata Organization. If the API Organization struct retains a Description field, callers will receive an empty string instead of the actual value. Either restore the mapping or also remove the field from the struct if it's intentionally being dropped.</violation>
</file>

<file name="apps/console/src/pages/iam/auth/MagicLinkAlreadyUsedPage.tsx">

<violation number="1" location="apps/console/src/pages/iam/auth/MagicLinkAlreadyUsedPage.tsx:38">
P2: Users arriving directly from a stale email link may have no SPA history to go back to. `navigate(-1)` would then navigate outside the app entirely. A safer approach: fall back to a known route like `/login` when `window.history.length` is too shallow, or always navigate to the login/home page.</violation>
</file>

<file name="apps/console/src/pages/organizations/compliance-page/brand/_components/CompliancePageCustomLinkDialog.tsx">

<violation number="1" location="apps/console/src/pages/organizations/compliance-page/brand/_components/CompliancePageCustomLinkDialog.tsx:184">
P2: Editing a URL after manually choosing a Name replaces that Name with the detected social-network name. Clear `nameAutoDetected` when the Name field receives a user edit, so auto-fill only owns untouched names.</violation>
</file>

<file name="pkg/server/api/complianceportal/v1/mux.go">

<violation number="1" location="pkg/server/api/complianceportal/v1/mux.go:122">
P3: Unknown portal paths are always dispatched to `webServer`; `handleCustomDomain404` is unreachable because `/*` matches them first. Route unmatched paths through the custom 404 handler (or remove the unused handler) so the intended error response can be produced.</violation>
</file>

<file name="pkg/certmanager/renew_worker.go">

<violation number="1" location="pkg/certmanager/renew_worker.go:94">
P0: Certificates selected for renewal are never renewed: `Process` succeeds without performing work after `Claim` marks them `Renewing`. Implement renewal and persist the resulting certificate/status, or avoid claiming this worker until processing exists.</violation>
</file>

<file name="pkg/coredata/migrations/20260706T132923Z.sql">

<violation number="1" location="pkg/coredata/migrations/20260706T132923Z.sql:26">
P1: Organizations without a trust center lose their configured custom domain during this migration. Preserve or explicitly migrate these rows instead of deleting all domains that have no `trust_centers` reference.</violation>
</file>

<file name="pkg/server/response_headers.go">

<violation number="1" location="pkg/server/response_headers.go:19">
P2: HSTS preload directive is present but `includeSubDomains` is missing. For the `preload` directive to be effective for browser preload-list submission, `includeSubDomains` is required per hstspreload.org requirements. Either add `includeSubDomains` if all subdomains should enforce HTTPS, or remove the `preload` directive if subdomain coverage is not intended.</violation>
</file>

<file name="apps/compliance-portal/src/components/TopBar/TopBar.tsx">

<violation number="1" location="apps/compliance-portal/src/components/TopBar/TopBar.tsx:114">
P2: Access login returns visitors to the route but drops its query/hash state. Preserve the full in-app location in `continue` so links with parameters resume exactly after OAuth.</violation>
</file>

<file name="contrib/lima/provision.sh">

<violation number="1" location="contrib/lima/provision.sh:242">
P2: The `.env` file for the compliance portal dev server is written to `apps/trust/.env` (line 192), but the npm workspace was renamed from `@probo/trust` to `@probo/compliance-portal`. If the source directory was also renamed, this `.env` won't be picked up by the compliance-portal dev server and `VITE_API_URL` will be unset. Update the path to match the new workspace directory.</violation>
</file>

<file name="pkg/crypto/jose/jose.go">

<violation number="1" location="pkg/crypto/jose/jose.go:120">
P3: Oversized JWK exponents are silently narrowed, so this function can return a key that does not represent the source JWK. Validate that `e` is positive and fits `int` before converting; return an error otherwise.</violation>

<violation number="2" location="pkg/crypto/jose/jose.go:126">
P2: A missing JWKS crashes token verification instead of rejecting the token. Return an error before dereferencing `jwks` so a failed or uninitialized OAuth key-set configuration cannot take down the callback request.</violation>
</file>

<file name="apps/console/src/pages/organizations/compliance-page/brand/_components/CompliancePageCustomLinkList.tsx">

<violation number="1" location="apps/console/src/pages/organizations/compliance-page/brand/_components/CompliancePageCustomLinkList.tsx:33">
P2: Organizations with more than 100 custom links cannot view, edit, or reorder links after the first page, because this list silently requests only the default page. Suggest loading all allowed links or add pagination before making the list management surface depend on this connection.</violation>
</file>

<file name="apps/console/src/pages/iam/auth/MagicLinkExpiredPage.tsx">

<violation number="1" location="apps/console/src/pages/iam/auth/MagicLinkExpiredPage.tsx:38">
P2: `navigate(-1)` on this page can leave users stranded. Users arriving from an expired email link in a new tab have no history to go back to — `navigate(-1)` will either do nothing or exit the app entirely. The page says "Please request a new one" but offers no direct way to do so. Consider navigating to the login/sign-in route as a fallback (e.g., `navigate('/login')`) so users can retry authentication, or provide a "Request new link" action instead.</violation>
</file>

<file name="pkg/server/api/complianceportal/sni_middleware.go">

<violation number="1" location="pkg/server/api/complianceportal/sni_middleware.go:88">
P1: GraphQL mutations submitted through a secondary portal domain can be retried as body-less GET requests after this 301, rather than reaching the canonical `/graphql` endpoint. Use a method-preserving permanent redirect (308) for this host canonicalization.</violation>
</file>

<file name="pkg/coredata/migrations/20260710T085314Z.sql">

<violation number="1" location="pkg/coredata/migrations/20260710T085314Z.sql:31">
P1: Organizations without an existing trust center lose their profile data during this migration: they are excluded from the copy, then their source columns are dropped. Preserve those rows by creating/migrating a trust center first, or retain the organization fields until that backfill can occur.</violation>
</file>

<file name="apps/console/src/pages/organizations/compliance-page/domain/_components/NewCompliancePageDomainDialog.tsx">

<violation number="1" location="apps/console/src/pages/organizations/compliance-page/domain/_components/NewCompliancePageDomainDialog.tsx:153">
P3: Domain help renders the same example twice, reducing the usefulness of the examples list. Restore a distinct second example such as `trust.example.com`.</violation>
</file>

<file name="packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts">

<violation number="1" location="packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts:176">
P2: Clearing a previously-set string field (title, description, websiteUrl, email, headquarterAddress) to empty is silently ignored — the old value persists. The truthy guard (`if (fieldName)`) used here matches `searchEngineIndexing`'s pattern, but that field uses empty string as a distinct '(Unchanged)' sentinel. These new free-form content strings have no such semantic: emptying the input is a valid clear intent. Since n8n provides no way to distinguish untouched-from-cleared for a `default: ''` string parameter, the safest fix is to always include these fields in the input so the API receives the current user intent.</violation>
</file>

<file name="pkg/complianceportal/resolver/resolver.go">

<violation number="1" location="pkg/complianceportal/resolver/resolver.go:81">
P1: A trust center with an inactive custom-domain certificate but no loaded default-domain row publishes that unready custom host as its canonical URL. Fall back to the slug-derived default host instead, so links do not direct users to a domain that cannot yet serve HTTPS.</violation>
</file>

<file name="pkg/coredata/migrations/20260709T090905Z.sql">

<violation number="1" location="pkg/coredata/migrations/20260709T090905Z.sql:98">
P2: Cross-tenant certificate associations are currently valid at the database layer, so a mistaken or insufficiently scoped update can attach another tenant's key/certificate to this domain. Preserve the tenant invariant with a composite tenant-aware reference (or equivalent constraint) when adding this relationship.</violation>
</file>

<file name="GNUmakefile">

<violation number="1" location="GNUmakefile:389">
P1: Production builds omit the compliance portal bundle: `EMBEDDED` and `WITH_APPS` still target `apps/trust`, so the new portal can be deleted by `clean` but never built or embedded. Update those build inputs to the compliance-portal workspace alongside this rename.</violation>
</file>

<file name="pkg/coredata/compliance_framework.go">

<violation number="1" location="pkg/coredata/compliance_framework.go:386">
P1: `LoadWithHiddenByTrustCenterID` now assigns `ROW_NUMBER()` as rank for every row, ignoring the persisted `cf.rank` values that `UpdateRank` and `Insert` store. Any custom ordering set via `UpdateRank` will be silently lost when loading frameworks with this method. If any consumer paginates by `COMPLIANCE_FRAMEWORK_ORDER_RANK` or uses the returned rank for display, the behavior regresses. Consider preserving `cf.rank` for existing linked frameworks (`cf.id IS NOT NULL`) and only generating sequential ranks for unlinked ones, similar to the original logic.</violation>
</file>

<file name="apps/console/src/pages/organizations/compliance-page/domain/_components/DeleteCompliancePageDomainDialog.tsx">

<violation number="1" location="apps/console/src/pages/organizations/compliance-page/domain/_components/DeleteCompliancePageDomainDialog.tsx:43">
P3: `compliancePageId` is a required but unused prop, so callers must supply data this dialog never consumes. Remove it from the prop contract unless it is needed for cache updates.</violation>
</file>

<file name="pkg/iam/oauth2/discovery.go">

<violation number="1" location="pkg/iam/oauth2/discovery.go:86">
P2: Authorization endpoints with provider-required query parameters lose them here, causing their OAuth redirects to fail or select the wrong tenant. Merge generated parameters into `u.Query()` instead of replacing the endpoint query.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 200 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.

Re-trigger cubic

return certificate, nil
}

func (h *renewHandler) Process(_ context.Context, _ coredata.Certificate) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Certificates selected for renewal are never renewed: Process succeeds without performing work after Claim marks them Renewing. Implement renewal and persist the resulting certificate/status, or avoid claiming this worker until processing exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/certmanager/renew_worker.go, line 94:

<comment>Certificates selected for renewal are never renewed: `Process` succeeds without performing work after `Claim` marks them `Renewing`. Implement renewal and persist the resulting certificate/status, or avoid claiming this worker until processing exists.</comment>

<file context>
@@ -0,0 +1,127 @@
+	return certificate, nil
+}
+
+func (h *renewHandler) Process(_ context.Context, _ coredata.Certificate) error {
+	return nil
+}
</file context>

FROM organizations o
WHERE o.id = tc.organization_id AND o.custom_domain_id IS NOT NULL;

DELETE FROM custom_domains cd

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Organizations without a trust center lose their configured custom domain during this migration. Preserve or explicitly migrate these rows instead of deleting all domains that have no trust_centers reference.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/coredata/migrations/20260706T132923Z.sql, line 26:

<comment>Organizations without a trust center lose their configured custom domain during this migration. Preserve or explicitly migrate these rows instead of deleting all domains that have no `trust_centers` reference.</comment>

<file context>
@@ -0,0 +1,33 @@
+FROM organizations o
+WHERE o.id = tc.organization_id AND o.custom_domain_id IS NOT NULL;
+
+DELETE FROM custom_domains cd
+WHERE NOT EXISTS (
+    SELECT 1 FROM trust_centers tc
</file context>

RawQuery: r.URL.RawQuery,
}

http.Redirect(w, r, target.String(), http.StatusMovedPermanently)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: GraphQL mutations submitted through a secondary portal domain can be retried as body-less GET requests after this 301, rather than reaching the canonical /graphql endpoint. Use a method-preserving permanent redirect (308) for this host canonicalization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/server/api/complianceportal/sni_middleware.go, line 88:

<comment>GraphQL mutations submitted through a secondary portal domain can be retried as body-less GET requests after this 301, rather than reaching the canonical `/graphql` endpoint. Use a method-preserving permanent redirect (308) for this host canonicalization.</comment>

<file context>
@@ -57,16 +58,50 @@ func NewSNIMiddleware(trustSvc *trust.Service) func(next http.Handler) http.Hand
+						RawQuery: r.URL.RawQuery,
+					}
+
+					http.Redirect(w, r, target.String(), http.StatusMovedPermanently)
+
+					return
</file context>
Suggested change
http.Redirect(w, r, target.String(), http.StatusMovedPermanently)
http.Redirect(w, r, target.String(), http.StatusPermanentRedirect)

WHERE tc.organization_id = o.id;

ALTER TABLE organizations
DROP COLUMN description,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Organizations without an existing trust center lose their profile data during this migration: they are excluded from the copy, then their source columns are dropped. Preserve those rows by creating/migrating a trust center first, or retain the organization fields until that backfill can occur.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/coredata/migrations/20260710T085314Z.sql, line 31:

<comment>Organizations without an existing trust center lose their profile data during this migration: they are excluded from the copy, then their source columns are dropped. Preserve those rows by creating/migrating a trust center first, or retain the organization fields until that backfill can occur.</comment>

<file context>
@@ -0,0 +1,34 @@
+WHERE tc.organization_id = o.id;
+
+ALTER TABLE organizations
+    DROP COLUMN description,
+    DROP COLUMN website_url,
+    DROP COLUMN email,
</file context>

host = byID[*trustCenter.CustomDomainID].Domain
case trustCenter.DefaultDomainID != nil && byID[*trustCenter.DefaultDomainID] != nil:
host = byID[*trustCenter.DefaultDomainID].Domain
case trustCenter.CustomDomainID != nil && byID[*trustCenter.CustomDomainID] != nil:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A trust center with an inactive custom-domain certificate but no loaded default-domain row publishes that unready custom host as its canonical URL. Fall back to the slug-derived default host instead, so links do not direct users to a domain that cannot yet serve HTTPS.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/complianceportal/resolver/resolver.go, line 81:

<comment>A trust center with an inactive custom-domain certificate but no loaded default-domain row publishes that unready custom host as its canonical URL. Fall back to the slug-derived default host instead, so links do not direct users to a domain that cannot yet serve HTTPS.</comment>

<file context>
@@ -0,0 +1,146 @@
+		host = byID[*trustCenter.CustomDomainID].Domain
+	case trustCenter.DefaultDomainID != nil && byID[*trustCenter.DefaultDomainID] != nil:
+		host = byID[*trustCenter.DefaultDomainID].Domain
+	case trustCenter.CustomDomainID != nil && byID[*trustCenter.CustomDomainID] != nil:
+		host = byID[*trustCenter.CustomDomainID].Domain
+	}
</file context>

},
)

r.Handle("/*", webServer)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Unknown portal paths are always dispatched to webServer; handleCustomDomain404 is unreachable because /* matches them first. Route unmatched paths through the custom 404 handler (or remove the unused handler) so the intended error response can be produced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/server/api/complianceportal/v1/mux.go, line 122:

<comment>Unknown portal paths are always dispatched to `webServer`; `handleCustomDomain404` is unreachable because `/*` matches them first. Route unmatched paths through the custom 404 handler (or remove the unused handler) so the intended error response can be produced.</comment>

<file context>
@@ -0,0 +1,163 @@
+				},
+			)
+
+			r.Handle("/*", webServer)
+			r.NotFound(handleCustomDomain404)
+		},
</file context>

Comment thread apps/console/src/pages/iam/auth/PortalLoginPage.tsx Outdated
Comment thread pkg/crypto/jose/jose.go

return &rsa.PublicKey{
N: new(big.Int).SetBytes(nBytes),
E: int(new(big.Int).SetBytes(eBytes).Int64()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Oversized JWK exponents are silently narrowed, so this function can return a key that does not represent the source JWK. Validate that e is positive and fits int before converting; return an error otherwise.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/crypto/jose/jose.go, line 120:

<comment>Oversized JWK exponents are silently narrowed, so this function can return a key that does not represent the source JWK. Validate that `e` is positive and fits `int` before converting; return an error otherwise.</comment>

<file context>
@@ -97,3 +98,106 @@ func SignJWT(privateKey *rsa.PrivateKey, kid string, claims any) (string, error)
+
+	return &rsa.PublicKey{
+		N: new(big.Int).SetBytes(nBytes),
+		E: int(new(big.Int).SetBytes(eBytes).Int64()),
+	}, nil
+}
</file context>

type DeleteCompliancePageDomainDialogProps = PropsWithChildren<{
domain: string;
customDomainId: string;
compliancePageId: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: compliancePageId is a required but unused prop, so callers must supply data this dialog never consumes. Remove it from the prop contract unless it is needed for cache updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/pages/organizations/compliance-page/domain/_components/DeleteCompliancePageDomainDialog.tsx, line 43:

<comment>`compliancePageId` is a required but unused prop, so callers must supply data this dialog never consumes. Remove it from the prop contract unless it is needed for cache updates.</comment>

<file context>
@@ -40,39 +39,36 @@ const deleteCustomDomainMutation = graphql`
 type DeleteCompliancePageDomainDialogProps = PropsWithChildren<{
   domain: string;
+  customDomainId: string;
+  compliancePageId: string;
 }>;
 
</file context>

{" "}
compliance.example.com,
trust.example.com
compliance.example.com

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Domain help renders the same example twice, reducing the usefulness of the examples list. Restore a distinct second example such as trust.example.com.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/pages/organizations/compliance-page/domain/_components/NewCompliancePageDomainDialog.tsx, line 153:

<comment>Domain help renders the same example twice, reducing the usefulness of the examples list. Restore a distinct second example such as `trust.example.com`.</comment>

<file context>
@@ -154,7 +150,7 @@ export function NewCompliancePageDomainDialog(props: PropsWithChildren) {
               {" "}
               compliance.example.com,
-              trust.example.com
+              compliance.example.com
             </p>
           </div>
</file context>
Suggested change
compliance.example.com
trust.example.com

customDomain {
id
domain
managed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used?

import("#/pages/organizations/compliance-page/domain/CompliancePageDomainPageLoader"),
),
loader: () => {
throw redirect("brand");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we clean the route and update the links instead

updated_at
)
SELECT
translate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use pg function that generates gid?

SELECT
translate(
encode(
substring(decode(translate(pp.trust_center_id, '-_', '+/'), 'base64') FROM 1 FOR 8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use pg functions that we have already to manipulate gids

(
tc.slug || '.' || COALESCE(
NULLIF(current_setting('probo.trust_center_base_domain', true), ''),
'probopage.com'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure we want this for self managed instances

@@ -12,4 +12,13 @@
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.

ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'YOUSIGN';
ALTER TABLE trust_centers
ADD COLUMN page_title TEXT;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be minded that this used to be org name, which was used in headings like "{{name}}'s trust center", but now could be a full title

Comment thread pkg/probod/probod.go
@@ -656,6 +705,7 @@ func (impl *Implm) Run(
IAM: iamService,
Trust: trustService,
ESign: esignService,
CustomDomain: managementService,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming looks weird both Field and value

gearnode added 4 commits July 15, 2026 17:32
Move range, ETag, and cache handling out of the files API handler
so brand logos and other public assets can reuse the same code path.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Serve CIMD logos from dedicated endpoints, expose client metadata
branding through GraphQL, and resolve branding from OAuth clients.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Drop the portal-login redirect, accept continue URLs for magic links,
brand emails from the OAuth client when present, and update e2e coverage.

Signed-off-by: Bryan Frimin <bryan@probo.com>
Remove the separate portal login page, show OAuth client branding on
sign-in, and preserve authorize continue URLs across auth methods.

Signed-off-by: Bryan Frimin <bryan@probo.com>
@gearnode gearnode force-pushed the new-compliance-portal branch from 39a4d5d to 601182d Compare July 15, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants