@@ -4,7 +4,7 @@ import { organization } from '@sim/db/schema'
44import { createLogger } from '@sim/logger'
55import { eq , sql } from 'drizzle-orm'
66import { MIN_IDLE_TIMEOUT_HOURS } from '@/lib/api/contracts/organization'
7- import { getMemberOrganizationId } from '@/lib/auth/security-policy'
7+ import { getMemberOrganizationId , invalidateMembershipCache } from '@/lib/auth/security-policy'
88
99const logger = createLogger ( 'SessionPolicy' )
1010
@@ -150,6 +150,51 @@ export async function eagerClampOrgSessions(
150150 organizationId : string ,
151151 policy : ResolvedSessionPolicy
152152) : Promise < void > {
153+ const bounds = clampBoundsSql ( policy )
154+ if ( ! bounds ) return
155+
156+ await db . execute ( sql `
157+ UPDATE "session" SET expires_at = LEAST(${ bounds } )
158+ WHERE impersonated_by IS NULL
159+ AND user_id IN (
160+ SELECT user_id FROM member WHERE organization_id = ${ organizationId }
161+ )
162+ ` )
163+ }
164+
165+ /**
166+ * Applies the org's session policy to a user who just JOINED the org:
167+ * invalidates their cached membership (so the cookie-version and hook-clamp
168+ * fallbacks see the new org immediately) and clamps their pre-join sessions,
169+ * which otherwise keep their old expiry until the next sliding refresh.
170+ * Best-effort by design — a failure here must never fail the join; the
171+ * update-hook clamp self-heals within one refresh cycle.
172+ */
173+ export async function applySessionPolicyToNewMember (
174+ userId : string ,
175+ organizationId : string
176+ ) : Promise < void > {
177+ try {
178+ invalidateMembershipCache ( userId )
179+ const policy = await getSessionPolicy ( organizationId )
180+ const bounds = clampBoundsSql ( policy )
181+ if ( ! bounds ) return
182+
183+ await db . execute ( sql `
184+ UPDATE "session" SET expires_at = LEAST(${ bounds } )
185+ WHERE user_id = ${ userId } AND impersonated_by IS NULL
186+ ` )
187+ } catch ( error ) {
188+ logger . error ( 'Failed to apply session policy to new member; next refresh re-clamps' , {
189+ userId,
190+ organizationId,
191+ error,
192+ } )
193+ }
194+ }
195+
196+ /** SQL argument list for the LEAST() clamp, or null when the policy is empty. */
197+ function clampBoundsSql ( policy : ResolvedSessionPolicy ) {
153198 const bounds = [ sql `expires_at` ]
154199 if ( policy . maxSessionHours ) {
155200 const maxSecs = policy . maxSessionHours * 3600
@@ -159,13 +204,6 @@ export async function eagerClampOrgSessions(
159204 const idleSecs = Math . max ( policy . idleTimeoutHours , MIN_IDLE_TIMEOUT_HOURS ) * 3600
160205 bounds . push ( sql `now() + make_interval(secs => ${ idleSecs } )` )
161206 }
162- if ( bounds . length === 1 ) return
163-
164- await db . execute ( sql `
165- UPDATE "session" SET expires_at = LEAST(${ sql . join ( bounds , sql `, ` ) } )
166- WHERE impersonated_by IS NULL
167- AND user_id IN (
168- SELECT user_id FROM member WHERE organization_id = ${ organizationId }
169- )
170- ` )
207+ if ( bounds . length === 1 ) return null
208+ return sql . join ( bounds , sql `, ` )
171209}
0 commit comments