@@ -13,6 +13,7 @@ import {
1313 isInvoiceableTransaction ,
1414 isRefundTransaction ,
1515} from "@/utils/invoice.js" ;
16+ import { isConfigurableDomain , normalizeDomain } from "@/utils/sso-domain.js" ;
1617
1718import { logAuditEvent } from "@llmgateway/audit" ;
1819import {
@@ -63,6 +64,7 @@ const organizationSchema = z.object({
6364 apiKeyLimit : z . number ( ) . nullable ( ) ,
6465 retentionLevel : z . enum ( [ "retain" , "none" ] ) ,
6566 providerCompliancePolicy : providerCompliancePolicySchema . nullable ( ) ,
67+ ssoAutoJoinDomain : z . string ( ) . nullable ( ) ,
6668 status : z . enum ( [ "active" , "inactive" , "deleted" ] ) . nullable ( ) ,
6769 autoTopUpEnabled : z . boolean ( ) ,
6870 autoTopUpThreshold : z . string ( ) . nullable ( ) ,
@@ -138,6 +140,7 @@ const updateOrganizationSchema = z.object({
138140 providerCompliancePolicy : providerCompliancePolicySchema
139141 . nullable ( )
140142 . optional ( ) ,
143+ ssoAutoJoinDomain : z . string ( ) . max ( 253 ) . nullable ( ) . optional ( ) ,
141144 autoTopUpEnabled : z . boolean ( ) . optional ( ) ,
142145 autoTopUpThreshold : z . number ( ) . min ( 5 ) . optional ( ) ,
143146 autoTopUpAmount : z
@@ -494,6 +497,7 @@ organization.openapi(updateOrganization, async (c) => {
494497 billingNotes,
495498 retentionLevel,
496499 providerCompliancePolicy,
500+ ssoAutoJoinDomain,
497501 autoTopUpEnabled,
498502 autoTopUpThreshold,
499503 autoTopUpAmount,
@@ -559,6 +563,37 @@ organization.openapi(updateOrganization, async (c) => {
559563 }
560564 }
561565
566+ // Google SSO domain auto-join is an enterprise feature managed by owners and
567+ // admins. The value is normalized and validated before storage.
568+ let normalizedSsoDomain : string | null | undefined ;
569+ if ( ssoAutoJoinDomain !== undefined ) {
570+ if ( userOrganization . organization ?. plan !== "enterprise" ) {
571+ throw new HTTPException ( 403 , {
572+ message : "SSO auto-join requires an enterprise plan" ,
573+ } ) ;
574+ }
575+ if (
576+ userOrganization . role !== "owner" &&
577+ userOrganization . role !== "admin"
578+ ) {
579+ throw new HTTPException ( 403 , {
580+ message : "Only owners and admins can configure SSO auto-join" ,
581+ } ) ;
582+ }
583+ if ( ssoAutoJoinDomain === null || ssoAutoJoinDomain . trim ( ) === "" ) {
584+ normalizedSsoDomain = null ;
585+ } else {
586+ const normalized = normalizeDomain ( ssoAutoJoinDomain ) ;
587+ if ( ! isConfigurableDomain ( normalized ) ) {
588+ throw new HTTPException ( 400 , {
589+ message :
590+ "Invalid or disallowed domain. Use a corporate domain like acme.com (consumer email domains are not allowed)." ,
591+ } ) ;
592+ }
593+ normalizedSsoDomain = normalized ;
594+ }
595+ }
596+
562597 const updateData : any = { } ;
563598 if ( name !== undefined ) {
564599 updateData . name = name ;
@@ -584,6 +619,9 @@ organization.openapi(updateOrganization, async (c) => {
584619 if ( providerCompliancePolicy !== undefined ) {
585620 updateData . providerCompliancePolicy = providerCompliancePolicy ;
586621 }
622+ if ( normalizedSsoDomain !== undefined ) {
623+ updateData . ssoAutoJoinDomain = normalizedSsoDomain ;
624+ }
587625 if ( autoTopUpEnabled !== undefined ) {
588626 updateData . autoTopUpEnabled = autoTopUpEnabled ;
589627 if ( autoTopUpEnabled && ! userOrganization . organization ?. autoTopUpEnabled ) {
@@ -599,11 +637,24 @@ organization.openapi(updateOrganization, async (c) => {
599637 updateData . autoTopUpAmount = autoTopUpAmount . toString ( ) ;
600638 }
601639
602- const [ updatedOrganization ] = await db
603- . update ( tables . organization )
604- . set ( updateData )
605- . where ( eq ( tables . organization . id , id ) )
606- . returning ( ) ;
640+ let updatedOrganization ;
641+ try {
642+ [ updatedOrganization ] = await db
643+ . update ( tables . organization )
644+ . set ( updateData )
645+ . where ( eq ( tables . organization . id , id ) )
646+ . returning ( ) ;
647+ } catch ( err ) {
648+ const code =
649+ ( err as { code ?: string ; cause ?: { code ?: string } } ) ?. code ??
650+ ( err as { cause ?: { code ?: string } } ) ?. cause ?. code ;
651+ if ( code === "23505" && normalizedSsoDomain ) {
652+ throw new HTTPException ( 409 , {
653+ message : "This domain is already configured by another organization." ,
654+ } ) ;
655+ }
656+ throw err ;
657+ }
607658
608659 // Build changes metadata for audit log
609660 const changes : Record < string , { old : unknown ; new : unknown } > = { } ;
@@ -717,6 +768,27 @@ organization.openapi(updateOrganization, async (c) => {
717768 } ) ;
718769 }
719770
771+ if (
772+ normalizedSsoDomain !== undefined &&
773+ normalizedSsoDomain !== oldOrg . ssoAutoJoinDomain
774+ ) {
775+ await logAuditEvent ( {
776+ organizationId : id ,
777+ userId : user . id ,
778+ action : "organization.sso_auto_join.update" ,
779+ resourceType : "organization" ,
780+ resourceId : id ,
781+ metadata : {
782+ changes : {
783+ ssoAutoJoinDomain : {
784+ old : oldOrg . ssoAutoJoinDomain ,
785+ new : normalizedSsoDomain ,
786+ } ,
787+ } ,
788+ } ,
789+ } ) ;
790+ }
791+
720792 return c . json ( {
721793 message : "Organization updated successfully" ,
722794 organization : updatedOrganization ,
0 commit comments