@@ -41,12 +41,9 @@ function selectTokenEndpointAuthMethod(
4141}
4242
4343/**
44- * Proposes a free, tenant-scoped provider ID by suffixing the domain's first
45- * label (`azure-ad` + `acme.com` -> `azure-ad-acme`), so a caller who hit the
46- * global-uniqueness collision is handed something concrete to type rather than
47- * being asked to invent a name. Callers pass a domain that already went through
48- * `normalizeSSODomain`, whose `^[a-z0-9-]+(\.[a-z0-9-]+)+$` shape guarantees a
49- * non-empty first label needing no further sanitizing.
44+ * Proposes a free provider ID by suffixing the domain's first label
45+ * (`azure-ad` + `acme.com` -> `azure-ad-acme`). Callers pass a domain already
46+ * through `normalizeSSODomain`, whose shape guarantees a non-empty first label.
5047 */
5148function suggestProviderId ( providerId : string , domain : string ) : string {
5249 return `${ providerId } -${ domain . split ( '.' ) [ 0 ] } `
@@ -135,12 +132,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
135132 )
136133 }
137134
138- // Security gate: configuring org SSO for a domain requires the org to have
139- // proven ownership of it (DNS TXT verification). Without this, the old
140- // first-come claim let any org wire another company's domain to their own
141- // IdP — an account-takeover primitive. Existing domains were grandfathered
142- // as verified by migration 0266, so live tenants are unaffected. Personal
143- // (org-less) SSO is not gated.
135+ /**
136+ * Configuring org SSO for a domain requires DNS-proven ownership; without it
137+ * a first-come claim lets any org wire another company's domain to their own
138+ * IdP. Migration 0266 grandfathered existing domains. Org-less SSO is not gated.
139+ */
144140 const isOrgDomainVerified = async ( ) : Promise < boolean > => {
145141 if ( ! orgId ) return true
146142 const [ verified ] = await db
@@ -166,9 +162,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
166162 { status : 403 }
167163 )
168164
169- // Fail fast before the expensive OIDC discovery. Re-checked immediately
170- // before the provider write below to close the TOCTOU window (the verified
171- // row could be removed while discovery is in flight).
165+ // Fail fast before OIDC discovery; re-checked before the write to close the
166+ // window where the proof is removed while discovery is in flight.
172167 if ( ! ( await isOrgDomainVerified ( ) ) ) return domainNotVerifiedResponse ( )
173168
174169 const isOwnedByCaller = ( provider : {
@@ -200,12 +195,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
200195 )
201196
202197 /**
203- * Better Auth treats `providerId` as globally unique, not per-tenant:
204- * `registerSSOProvider` rejects any id already present regardless of owner,
205- * and `checkProviderAccess` resolves providers by that column alone. Catch
206- * the cross-tenant collision here so the caller gets an actionable 409 that
207- * names a free id, instead of Better Auth's opaque 422 ("SSO provider with
208- * this providerId already exists") that gives no hint anything can be done.
198+ * Better Auth treats `providerId` as globally unique, not per-tenant, and
199+ * resolves providers by that column alone. Catching the cross-tenant
200+ * collision here turns its opaque 422 into a 409 naming a free id.
209201 */
210202 const findProviderIdConflict = async ( ) =>
211203 (
@@ -529,16 +521,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
529521 if ( digestAlgorithm ) samlConfig . digestAlgorithm = digestAlgorithm
530522
531523 /**
532- * These two are always written, empty when unset, rather than omitted.
533- * Better Auth merges SAML config with `??`, so an omitted key silently keeps
534- * whatever was stored — clearing either field would never take effect. Both
535- * are falsy-guarded downstream: `createIdP` falls back to
536- * issuer/entryPoint/cert without metadata, and `createSP` omits nameIDFormat.
524+ * Always written, empty when unset: Better Auth merges SAML config with
525+ * `??`, so an omitted key keeps whatever was stored and clearing either
526+ * field would never take effect. Both are falsy-guarded downstream.
537527 *
538- * Metadata in particular must not be generated here. Storing a document built
539- * from cert + entryPoint made re-saving destructive, because the form loads it
540- * back, resends it, and it then outranks the certificate — so rotating a SAML
541- * cert appeared to succeed and changed nothing.
528+ * Metadata must not be generated here — a document built from cert +
529+ * entryPoint outranks the certificate on re-save, silently defeating
530+ * SAML cert rotation.
542531 */
543532 samlConfig . idpMetadata = { metadata : idpMetadata ?? '' }
544533 samlConfig . identifierFormat = identifierFormat ?? ''
@@ -632,12 +621,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
632621 . limit ( 1 )
633622
634623 /**
635- * Unconditional write of Better Auth's `domainVerified` flag — the value Sim
636- * mirrors from its own DNS proof, and what lets an SSO sign-in auto-link to an
637- * existing same-email account. Used to withdraw trust, and to set the org-less
638- * (personal) verdict, which {@link grantProviderDomainTrust} decides from the
639- * deployment rather than from a domain. Granting on an org-scoped provider goes
640- * through that same helper, which re-tests ownership in the write itself.
624+ * Unconditional write of Better Auth's `domainVerified` flag, which Sim
625+ * mirrors from its own DNS proof. Used to withdraw trust; granting on an
626+ * org-scoped provider goes through {@link grantProviderDomainTrust}, which
627+ * re-tests ownership in the write itself.
641628 */
642629 const setProviderDomainVerified = async ( verified : boolean ) => {
643630 await db . update ( ssoProvider ) . set ( { domainVerified : verified } ) . where ( ownerClause )
@@ -646,19 +633,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
646633 /**
647634 * Grants domain trust only while the proof is held under a row lock.
648635 *
649- * Folding the ownership test into the UPDATE's WHERE clause is not sufficient:
650- * under READ COMMITTED the EXISTS subquery is evaluated against the statement's
651- * original snapshot, so a delete committing while the UPDATE waits on the
652- * provider row can still leave the subquery seeing the removed sso_domain row —
653- * granting trust after ownership is gone. Taking `FOR SHARE` on that row inside
654- * a transaction makes the two operations order properly: the delete's removal of
655- * sso_domain blocks until this commits, and if it committed first the SELECT
656- * finds nothing and no trust is written.
636+ * A WHERE-clause EXISTS test is not enough: under READ COMMITTED the subquery
637+ * sees the statement's original snapshot, so a delete committing while the
638+ * UPDATE waits can still grant trust after ownership is gone. `FOR SHARE`
639+ * orders the two — the delete blocks until this commits, and if it committed
640+ * first the SELECT finds nothing.
657641 *
658- * Org-less (personal) SSO is a self-host-only path — Sim's UI always registers
659- * org-scoped. It has no verified domain behind it, so it is trusted only when
660- * self-hosted, where the operator is the sole tenant. On the hosted deployment
661- * that trust would let anyone claim a domain they do not own.
642+ * Org-less SSO is self-host-only (Sim's UI always registers org-scoped) and
643+ * has no proof behind it, so it is trusted only when self-hosted.
662644 */
663645 const grantProviderDomainTrust = async ( ) : Promise < boolean > => {
664646 if ( ! orgId ) {
@@ -701,9 +683,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
701683 headers,
702684 } )
703685
704- // No newly-created row to roll back here, so clear the flag instead:
705- // `updateSSOProvider` only resets it when the domain changes, so a
706- // same-domain edit would otherwise leave stale trust standing.
686+ // Nothing to roll back on update, so clear the flag: `updateSSOProvider`
687+ // resets it only when the domain changes, leaving same-domain edits stale.
707688 if ( ! ( await grantProviderDomainTrust ( ) ) ) {
708689 await setProviderDomainVerified ( false )
709690 logger . warn ( 'Revoked SSO domain trust: verification was removed mid-update' , {
@@ -729,12 +710,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
729710 headers,
730711 } )
731712
732- // A refused grant means the verified sso_domain row was removed between the
733- // pre-write check and Better Auth persisting the provider, leaving a provider
734- // on a domain the org no longer proves — roll it back. registerSSOProvider is
735- // create-only, so a successful call always created a brand-new row; we delete
736- // by its primary-key `id`, not the logical providerId, which a concurrent
737- // delete+recreate could point at a different row.
713+ // A refused grant means the proof vanished mid-write, leaving a provider on a
714+ // domain the org no longer proves — roll it back. Deleted by primary key, not
715+ // providerId, which a concurrent delete+recreate could point at another row.
738716 if ( ! ( await grantProviderDomainTrust ( ) ) ) {
739717 // registerSSOProvider spreads the created row's `id` at runtime, but the
740718 // typed return omits it — read it defensively and only delete when it's a
0 commit comments