Closes the tenant-anchor divergence that blocked the Auth-5 sdk/backend flips.
The registry is the authority for tenant identity (model B2), but nothing ever told the product about a new tenant. The anchors drifted: the registry held acme/matrix-acme while the product database held only the legacy seed tenant 9282a473. With the SDK gate enabled, its TenantResolver does GetTenantBySlug and would 403 every authenticated request.
New internal/product port, mirroring keycloak.Adapter: handlers depend on the interface; main wires HTTPProvisioner when PRODUCT_API_URL is set and NoopProvisioner otherwise, so an unconfigured deployment still creates tenants.
Tenant creation now also provisions the product tenant with our UUID, using the same best-effort contract as Keycloak provisioning: failure does not roll the tenant back, it emits product.provision_failed so the divergence is traceable; success emits product.tenant_provisioned. A 409 counts as success — onboarding may retry and the product insert is idempotent (compliance#217).
Server.productProvisioner() guarantees the documented never-nil invariant (tests construct Server directly and would otherwise panic mid-tenant-creation).
Already proven live: all 6 registry tenants were provisioned into the product DB through this exact endpoint with UUIDs preserved (HTTP 201, UUID MATCHES), and a re-POST left exactly 1 row.
5 new tests; full suite green with -race, coverage 71.4% (gate 70).
**Closes the tenant-anchor divergence that blocked the Auth-5 sdk/backend flips.**
The registry is the authority for tenant identity (model B2), but nothing ever told the product about a new tenant. The anchors drifted: the registry held `acme`/`matrix-acme` while the product database held only the legacy seed tenant `9282a473`. With the SDK gate enabled, its TenantResolver does `GetTenantBySlug` and would **403 every authenticated request**.
**New `internal/product` port**, mirroring `keycloak.Adapter`: handlers depend on the interface; `main` wires `HTTPProvisioner` when `PRODUCT_API_URL` is set and `NoopProvisioner` otherwise, so an unconfigured deployment still creates tenants.
Tenant creation now also provisions the product tenant **with our UUID**, using the same best-effort contract as Keycloak provisioning: failure does not roll the tenant back, it emits `product.provision_failed` so the divergence is traceable; success emits `product.tenant_provisioned`. A 409 counts as success — onboarding may retry and the product insert is idempotent (compliance#217).
`Server.productProvisioner()` guarantees the documented never-nil invariant (tests construct Server directly and would otherwise panic mid-tenant-creation).
**Already proven live:** all 6 registry tenants were provisioned into the product DB through this exact endpoint with UUIDs preserved (HTTP 201, `UUID MATCHES`), and a re-POST left exactly 1 row.
5 new tests; full suite green with `-race`, coverage **71.4%** (gate 70).
Closes the tenant-anchor divergence that blocked the Auth-5 sdk/backend flips.
The registry is the authority for tenant identity (model B2), but nothing ever
told the product about a new tenant. The anchors drifted: the registry held
acme/matrix-acme while the product database held only the legacy seed tenant
9282a473. With the SDK gate enabled its TenantResolver does GetTenantBySlug and
would 403 EVERY authenticated request, because no real user's org slug existed
locally.
New internal/product port, mirroring keycloak.Adapter: handlers depend on the
interface, main wires HTTPProvisioner when PRODUCT_API_URL is set and
NoopProvisioner otherwise, so an unconfigured deployment still creates tenants.
Tenant creation now also provisions the product tenant with OUR uuid, using the
same best-effort contract as Keycloak provisioning: a failure does not roll the
tenant back, it emits a product.provision_failed audit event so the divergence
is traceable. Success emits product.tenant_provisioned.
A 409 from the product counts as success — onboarding may retry and the
product's insert is idempotent on the primary key (compliance#217).
Server.productProvisioner() guarantees the documented never-nil invariant;
tests construct Server directly and would otherwise panic mid-tenant-creation.
5 tests incl. the point of the whole port (the registry UUID is what gets sent).
Full suite green with -race, coverage 71.4% (gate 70).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNdLL9BdsWm7MCyui5ffPD
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes the tenant-anchor divergence that blocked the Auth-5 sdk/backend flips.
The registry is the authority for tenant identity (model B2), but nothing ever told the product about a new tenant. The anchors drifted: the registry held
acme/matrix-acmewhile the product database held only the legacy seed tenant9282a473. With the SDK gate enabled, its TenantResolver doesGetTenantBySlugand would 403 every authenticated request.New
internal/productport, mirroringkeycloak.Adapter: handlers depend on the interface;mainwiresHTTPProvisionerwhenPRODUCT_API_URLis set andNoopProvisionerotherwise, so an unconfigured deployment still creates tenants.Tenant creation now also provisions the product tenant with our UUID, using the same best-effort contract as Keycloak provisioning: failure does not roll the tenant back, it emits
product.provision_failedso the divergence is traceable; success emitsproduct.tenant_provisioned. A 409 counts as success — onboarding may retry and the product insert is idempotent (compliance#217).Server.productProvisioner()guarantees the documented never-nil invariant (tests construct Server directly and would otherwise panic mid-tenant-creation).Already proven live: all 6 registry tenants were provisioned into the product DB through this exact endpoint with UUIDs preserved (HTTP 201,
UUID MATCHES), and a re-POST left exactly 1 row.5 new tests; full suite green with
-race, coverage 71.4% (gate 70).