11import { Context , Effect , Layer } from "effect" ;
22
3- import { AccountProvider } from "@executor-js/api/server" ;
3+ import { AccountProvider , type AccountHeaders } from "@executor-js/api/server" ;
44import {
55 AccountError ,
66 AccountForbidden ,
@@ -12,7 +12,7 @@ import { ApiKeyService } from "../auth/api-keys";
1212import { UserStoreService } from "../auth/context" ;
1313import type { Session } from "../auth/middleware" ;
1414import { WorkOSClient } from "../auth/workos" ;
15- import { authorizeOrganization } from "../auth/organization" ;
15+ import { ORG_SELECTOR_HEADER , authorizeOrganizationSelector } from "../auth/organization" ;
1616import { AutumnService } from "../extensions/billing/service" ;
1717import { getMemberLimitForPlan , selectActiveMemberLimitPlan } from "../extensions/billing/plans" ;
1818
@@ -87,16 +87,20 @@ export const workosAccountProvider: Layer.Layer<
8787 ? Effect . succeed ( caller . session )
8888 : Effect . fail < AccountUnauthorized > ( new AccountUnauthorized ( ) ) ;
8989
90- // Like cloud's `requireSessionOrganization`: an authenticated session that
91- // currently holds an active membership in its session org. Yields the
92- // session + resolved org, or AccountNoOrganization.
93- const requireOrganization = ( ) =>
90+ // The org scope for an org-scoped request: the console URL's org (sent in
91+ // the selector header) when present, else the session's own org. Membership
92+ // is re-checked live, so the header is a selector, not a trust boundary —
93+ // and two browser tabs on different orgs each send their own header, so
94+ // they stay independent (see organization.ts). Yields the session +
95+ // resolved org, or AccountNoOrganization.
96+ const requireOrganization = ( headers : AccountHeaders ) =>
9497 Effect . gen ( function * ( ) {
9598 const session = yield * requireSession ( ) ;
96- if ( ! session . organizationId ) {
99+ const selector = headers [ ORG_SELECTOR_HEADER ] ?? session . organizationId ;
100+ if ( ! selector ) {
97101 return yield * new AccountNoOrganization ( ) ;
98102 }
99- const org = yield * authorizeOrganization ( session . accountId , session . organizationId ) . pipe (
103+ const org = yield * authorizeOrganizationSelector ( session . accountId , selector ) . pipe (
100104 Effect . provideContext ( ctx ) ,
101105 Effect . mapError ( ( ) => new AccountNoOrganization ( ) ) ,
102106 ) ;
@@ -158,11 +162,15 @@ export const workosAccountProvider: Layer.Layer<
158162 } ) ;
159163
160164 return AccountProvider . of ( {
161- me : ( ) =>
165+ me : ( headers ) =>
162166 Effect . gen ( function * ( ) {
163167 const session = yield * requireSession ( ) ;
164- const org = session . organizationId
165- ? yield * authorizeOrganization ( session . accountId , session . organizationId ) . pipe (
168+ // Same selector precedence as requireOrganization: the URL's org
169+ // (header) drives /account/me so the shell reflects the org the tab
170+ // is viewing, not a session-global active org.
171+ const selector = headers [ ORG_SELECTOR_HEADER ] ?? session . organizationId ;
172+ const org = selector
173+ ? yield * authorizeOrganizationSelector ( session . accountId , selector ) . pipe (
166174 Effect . provideContext ( ctx ) ,
167175 Effect . orElseSucceed ( ( ) => null ) ,
168176 )
@@ -178,18 +186,18 @@ export const workosAccountProvider: Layer.Layer<
178186 } ;
179187 } ) ,
180188
181- listApiKeys : ( ) =>
189+ listApiKeys : ( headers ) =>
182190 Effect . gen ( function * ( ) {
183- const { session, org } = yield * requireOrganization ( ) ;
191+ const { session, org } = yield * requireOrganization ( headers ) ;
184192 const keys = yield * apiKeys
185193 . listUserKeys ( { accountId : session . accountId , organizationId : org . id } )
186194 . pipe ( Effect . catchTag ( "ApiKeyManagementError" , toAccountError ) ) ;
187195 return { apiKeys : keys } ;
188196 } ) ,
189197
190- createApiKey : ( _headers , name ) =>
198+ createApiKey : ( headers , name ) =>
191199 Effect . gen ( function * ( ) {
192- const { session, org } = yield * requireOrganization ( ) ;
200+ const { session, org } = yield * requireOrganization ( headers ) ;
193201 const trimmed = name . trim ( ) . slice ( 0 , MAX_API_KEY_NAME_LENGTH ) ;
194202 if ( ! trimmed ) {
195203 return yield * new AccountError ( { message : "API key name is required" } ) ;
@@ -199,9 +207,9 @@ export const workosAccountProvider: Layer.Layer<
199207 . pipe ( Effect . catchTag ( "ApiKeyManagementError" , toAccountError ) ) ;
200208 } ) ,
201209
202- revokeApiKey : ( _headers , apiKeyId ) =>
210+ revokeApiKey : ( headers , apiKeyId ) =>
203211 Effect . gen ( function * ( ) {
204- const { session, org } = yield * requireOrganization ( ) ;
212+ const { session, org } = yield * requireOrganization ( headers ) ;
205213 const ownedKeys = yield * apiKeys
206214 . listUserKeys ( { accountId : session . accountId , organizationId : org . id } )
207215 . pipe ( Effect . catchTag ( "ApiKeyManagementError" , toAccountError ) ) ;
@@ -214,9 +222,9 @@ export const workosAccountProvider: Layer.Layer<
214222 return { success : true } ;
215223 } ) ,
216224
217- listMembers : ( ) =>
225+ listMembers : ( headers ) =>
218226 Effect . gen ( function * ( ) {
219- const { session, org } = yield * requireOrganization ( ) ;
227+ const { session, org } = yield * requireOrganization ( headers ) ;
220228
221229 // Seats fall back to safe display defaults on lookup error — never
222230 // blank the page over a transient Autumn/WorkOS hiccup. The real cap
@@ -252,9 +260,9 @@ export const workosAccountProvider: Layer.Layer<
252260 return { members, seats } ;
253261 } ) ,
254262
255- listRoles : ( ) =>
263+ listRoles : ( headers ) =>
256264 Effect . gen ( function * ( ) {
257- const { org } = yield * requireOrganization ( ) ;
265+ const { org } = yield * requireOrganization ( headers ) ;
258266 const result = yield * workos
259267 . listOrgRoles ( org . id )
260268 . pipe ( Effect . catchTag ( "WorkOSError" , toAccountError ) ) ;
@@ -263,9 +271,9 @@ export const workosAccountProvider: Layer.Layer<
263271 } ;
264272 } ) ,
265273
266- inviteMember : ( _headers , body ) =>
274+ inviteMember : ( headers , body ) =>
267275 Effect . gen ( function * ( ) {
268- const { session, org } = yield * requireOrganization ( ) ;
276+ const { session, org } = yield * requireOrganization ( headers ) ;
269277 yield * requireAdmin ( session . accountId , org . id ) ;
270278 yield * reserveMemberSlot ( org . id ) ;
271279 const invitation = yield * workos
@@ -278,9 +286,9 @@ export const workosAccountProvider: Layer.Layer<
278286 return { id : invitation . id , email : invitation . email } ;
279287 } ) ,
280288
281- removeMember : ( _headers , membershipId ) =>
289+ removeMember : ( headers , membershipId ) =>
282290 Effect . gen ( function * ( ) {
283- const { session, org } = yield * requireOrganization ( ) ;
291+ const { session, org } = yield * requireOrganization ( headers ) ;
284292 yield * requireAdmin ( session . accountId , org . id ) ;
285293 yield * assertMembershipInOrg ( org . id , membershipId ) ;
286294 yield * workos
@@ -289,9 +297,9 @@ export const workosAccountProvider: Layer.Layer<
289297 return { success : true } ;
290298 } ) ,
291299
292- updateMemberRole : ( _headers , membershipId , roleSlug ) =>
300+ updateMemberRole : ( headers , membershipId , roleSlug ) =>
293301 Effect . gen ( function * ( ) {
294- const { session, org } = yield * requireOrganization ( ) ;
302+ const { session, org } = yield * requireOrganization ( headers ) ;
295303 yield * requireAdmin ( session . accountId , org . id ) ;
296304 yield * assertMembershipInOrg ( org . id , membershipId ) ;
297305 yield * workos
@@ -300,9 +308,9 @@ export const workosAccountProvider: Layer.Layer<
300308 return { success : true } ;
301309 } ) ,
302310
303- updateOrgName : ( _headers , name ) =>
311+ updateOrgName : ( headers , name ) =>
304312 Effect . gen ( function * ( ) {
305- const { session, org } = yield * requireOrganization ( ) ;
313+ const { session, org } = yield * requireOrganization ( headers ) ;
306314 yield * requireAdmin ( session . accountId , org . id ) ;
307315 const updated = yield * workos
308316 . updateOrganization ( org . id , name )
0 commit comments