22// the org, not just whoever's browser session completed the consent.
33//
44// Issue #1453: the encrypted-secrets provider (the writable provider on
5- // self-host and the Cloudflare host) files token rows under the ACTING
5+ // self-host and the Cloudflare host) filed token rows under the ACTING
66// caller's partition — so completing an org connection's consent in a user
7- // session lands the tokens at owner='user', subject=<that user>, while the
8- // connection row itself says org. The consenting user's own probe still reads
9- // the tokens (user rows shadow org rows), so the connection looks healthy —
10- // but every other principal resolves no value and fails with
7+ // session landed the tokens at owner='user', subject=<that user>, while the
8+ // connection row itself said org. The consenting user's own calls still read
9+ // the tokens (user rows shadow org rows), so the connection looked healthy —
10+ // but every other principal resolved no value and failed with
1111// `oauth_connection_missing`. The same mis-filing was fixed for the WorkOS
12- // Vault provider in #950; encrypted-secrets never got that fix .
12+ // Vault provider in #950.
1313//
1414// The journey: the bootstrap admin registers an OAuth-protected integration,
1515// completes the org-owned authorization-code flow (a real test AS on
16- // 127.0.0.1), and proves the tool works for them. A second, invited member of
17- // the same org then invokes the same tool — the guarantee under test is that
18- // the org connection resolves for them too.
16+ // 127.0.0.1), and proves the tool call carries the minted token upstream. A
17+ // second, invited member of the same org then invokes the same tool — the
18+ // guarantee under test is that the same org connection resolves the same
19+ // token for them too.
1920import { randomBytes } from "node:crypto" ;
2021import { createServer } from "node:http" ;
2122
@@ -33,23 +34,22 @@ import { serveOAuthTestServer } from "@executor-js/sdk/testing";
3334
3435import { scenario } from "../src/scenario" ;
3536import { Api , Target } from "../src/services" ;
36- import type { Identity } from "../src/target" ;
37- import { signInSession } from "../targets/selfhost" ;
37+ import { createInvitedIdentity } from "../targets/selfhost" ;
3838
3939const api = composePluginApi ( [ openApiHttpPlugin ( ) ] as const ) ;
4040
4141const unique = ( prefix : string ) => `${ prefix } ${ randomBytes ( 4 ) . toString ( "hex" ) } ` ;
4242
43- /** Upstream on 127.0.0.1: `GET /me` is 200 for any bearer. Credential
44- * resolution is the subject under test, so the upstream only needs to prove
45- * a token reached it . */
43+ /** Upstream on 127.0.0.1: `GET /me` echoes the authorization header it
44+ * received, so the scenario can assert a real token reached it — not merely
45+ * that a request was made . */
4646const serveUpstream = ( ) =>
4747 Effect . acquireRelease (
4848 Effect . callback < { readonly url : string ; readonly close : ( ) => void } > ( ( resume ) => {
4949 const server = createServer ( ( request , response ) => {
5050 if ( request . method === "GET" && ( request . url ?? "" ) . startsWith ( "/me" ) ) {
5151 response . writeHead ( 200 , { "content-type" : "application/json" } ) ;
52- response . end ( JSON . stringify ( { me : "ok" } ) ) ;
52+ response . end ( JSON . stringify ( { authorization : request . headers . authorization ?? null } ) ) ;
5353 return ;
5454 }
5555 response . writeHead ( 404 , { "content-type" : "application/json" } ) ;
@@ -106,43 +106,6 @@ const specFor = (
106106 } ,
107107 } ) ;
108108
109- /** A second, distinct member of the (single) selfhost org: admin invite →
110- * invited email signup → Better Auth session. */
111- const createInvitedIdentity = async ( baseUrl : string , admin : Identity ) : Promise < Identity > => {
112- const cookie = admin . headers ?. cookie ;
113- expect ( typeof cookie , "bootstrap admin has a Better Auth session cookie" ) . toBe ( "string" ) ;
114-
115- const invite = await fetch ( new URL ( "/api/admin/invites" , baseUrl ) , {
116- method : "POST" ,
117- headers : {
118- "content-type" : "application/json" ,
119- cookie : cookie ! ,
120- origin : new URL ( baseUrl ) . origin ,
121- } ,
122- body : JSON . stringify ( { role : "member" } ) ,
123- } ) ;
124- expect ( invite . status , `admin invite create response: ${ await invite . clone ( ) . text ( ) } ` ) . toBe ( 200 ) ;
125- const inviteBody = ( await invite . json ( ) ) as { readonly code ?: string } ;
126- expect ( typeof inviteBody . code , "invite response includes a redeemable code" ) . toBe ( "string" ) ;
127-
128- const email = `org-oauth-member-${ randomBytes ( 5 ) . toString ( "hex" ) } @e2e.test` ;
129- const password = "org-oauth-member-password-123" ;
130- const signup = await fetch ( new URL ( "/api/auth/sign-up/email" , baseUrl ) , {
131- method : "POST" ,
132- headers : { "content-type" : "application/json" , origin : new URL ( baseUrl ) . origin } ,
133- body : JSON . stringify ( { email, password, name : email , inviteCode : inviteBody . code } ) ,
134- } ) ;
135- expect ( signup . status , `invited signup response: ${ await signup . clone ( ) . text ( ) } ` ) . toBe ( 200 ) ;
136-
137- const session = await signInSession ( baseUrl , { email, password } ) ;
138- return {
139- label : email ,
140- credentials : { email, password } ,
141- headers : { cookie : session . cookieHeader } ,
142- cookies : session . cookies ,
143- } ;
144- } ;
145-
146109/** Complete the test AS's consent headlessly and return the authorization
147110 * code from the callback redirect. */
148111const consentCode = ( authorizationUrl : string ) =>
@@ -164,6 +127,7 @@ const consentCode = (authorizationUrl: string) =>
164127
165128type ToolEnvelope = {
166129 readonly ok : boolean ;
130+ readonly data ?: { readonly authorization ?: string | null } ;
167131 readonly error ?: { readonly code ?: string ; readonly message ?: string } ;
168132} ;
169133
@@ -261,30 +225,38 @@ scenario(
261225 } ) ;
262226 expect ( execution . status , `${ who } : the execution completes` ) . toBe ( "completed" ) ;
263227 if ( execution . status !== "completed" ) return yield * Effect . die ( "not completed" ) ;
264- const envelope = ( execution . structured as { result ?: ToolEnvelope } ) . result ;
265- return envelope ;
228+ return ( execution . structured as { result ?: ToolEnvelope } ) . result ;
266229 } ) ;
267230
268- // Sanity: the consenting member sees a healthy connection — this is
269- // exactly why the bug is invisible to whoever set the connection up.
231+ // Sanity: the consenting member's call carries a real bearer to the
232+ // upstream — this passing is exactly why the bug is invisible to
233+ // whoever set the connection up.
270234 const adminResult = yield * invoke ( adminClient , "consenting member" ) ;
271235 expect (
272- adminResult ,
273- "the consenting member's tool call resolves the org connection" ,
274- ) . toMatchObject ( { ok : true } ) ;
236+ adminResult ?. ok ,
237+ `the consenting member's tool call resolves the org connection (got: ${ JSON . stringify ( adminResult ) } )` ,
238+ ) . toBe ( true ) ;
239+ const adminToken = adminResult ?. data ?. authorization ;
240+ expect ( adminToken , "the upstream received the minted bearer" ) . toMatch ( / ^ B e a r e r .+ / ) ;
275241
276242 // THE guarantee: a different member of the same org resolves the
277- // same org-owned connection. Before the fix the token rows sit in
278- // the consenting user's partition, so this member resolves nothing
279- // and the call fails with `oauth_connection_missing`.
280- const member = yield * Effect . promise ( ( ) => createInvitedIdentity ( target . baseUrl , admin ) ) ;
243+ // same org-owned connection to the same token. Before the fix the
244+ // token rows sat in the consenting user's partition, so this member
245+ // resolved nothing and failed with `oauth_connection_missing`.
246+ const member = yield * Effect . promise ( ( ) =>
247+ createInvitedIdentity ( target . baseUrl , admin , { emailPrefix : "org-oauth-member" } ) ,
248+ ) ;
281249 const memberClient = yield * makeClient ( api , member ) ;
282250 const memberResult = yield * invoke ( memberClient , "other member" ) ;
283251 expect (
284- memberResult ,
252+ memberResult ?. ok ,
285253 "another org member's tool call resolves the same org connection " +
286254 `(got: ${ JSON . stringify ( memberResult ) } )` ,
287- ) . toMatchObject ( { ok : true } ) ;
255+ ) . toBe ( true ) ;
256+ expect (
257+ memberResult ?. data ?. authorization ,
258+ "both members resolve the same org-shared token" ,
259+ ) . toBe ( adminToken ) ;
288260 } ) ,
289261 Effect . gen ( function * ( ) {
290262 yield * adminClient . connections
0 commit comments