@@ -40,14 +40,35 @@ const makeContext = (
4040 options : {
4141 userId ?: string | null ;
4242 providers ?: Record < string , IdentityProviderConfig > ;
43+ runtime ?: boolean ;
4344 } = { }
4445) : { context : ConstructiveContext ; query : jest . Mock } => {
4546 const query = jest . fn ( async ( ) => ( {
4647 rows : databaseResult === undefined ? [ ] : [ { result : databaseResult } ]
4748 } as unknown as QueryResult ) ) ;
4849 const client = { query } as unknown as PoolClient ;
4950 const context = {
51+ api : {
52+ apiId : options . runtime
53+ ? '00000000-0000-0000-0000-000000000020'
54+ : undefined ,
55+ siteId : options . runtime
56+ ? '00000000-0000-0000-0000-000000000024'
57+ : undefined
58+ } ,
59+ token : options . runtime
60+ ? {
61+ id : '00000000-0000-0000-0000-000000000021' ,
62+ user_id : '00000000-0000-0000-0000-000000000022' ,
63+ principal_id : '00000000-0000-0000-0000-000000000023' ,
64+ kind : 'api_key' ,
65+ access_level : 'full_access'
66+ }
67+ : null ,
5068 requestOrigin : 'https://auth.example.com' ,
69+ siteId : options . runtime
70+ ? '00000000-0000-0000-0000-000000000024'
71+ : null ,
5172 userId : options . userId ?? null ,
5273 useModule : jest . fn ( async ( name : string ) => {
5374 if ( name === 'ssoSurface' ) return surface ;
@@ -135,7 +156,10 @@ describe('unified authentication GraphQL service', () => {
135156 access_token_expires_at : '2026-08-10T00:00:00.000Z' ,
136157 is_verified : false ,
137158 totp_enabled : false ,
138- mfa_required : false
159+ mfa_required : false ,
160+ callback_url : 'https://portal.example.com/auth/complete' ,
161+ site_state : opaque ,
162+ handoff_expires_at : '2026-08-10T00:01:00.000Z'
139163 } ) ;
140164 const service = createUnifiedAuthService ( false ) ;
141165
@@ -150,7 +174,9 @@ describe('unified authentication GraphQL service', () => {
150174 ) ;
151175
152176 expect ( result . accessToken ) . toBe ( 'cnc_live_bt_secret' ) ;
153- expect ( result . continuationUrl ) . toBeNull ( ) ;
177+ expect ( result . continuationUrl ) . toMatch (
178+ / ^ h t t p s : \/ \/ p o r t a l \. e x a m p l e \. c o m \/ a u t h \/ c o m p l e t e \? h a n d o f f = [ A - Z a - z 0 - 9 _ - ] { 43 } & s i t e _ s t a t e = /
179+ ) ;
154180 expect ( query ) . toHaveBeenCalledTimes ( 1 ) ;
155181 expect ( query . mock . calls [ 0 ] [ 0 ] ) . toContain (
156182 '"tenant_acme_sso_private"."sign_in_unified_login"'
@@ -163,10 +189,109 @@ describe('unified authentication GraphQL service', () => {
163189 'bearer' ,
164190 expect . stringMatching ( / ^ \\ x [ 0 - 9 a - f ] { 64 } $ / ) ,
165191 null ,
166- null
192+ null ,
193+ expect . stringMatching ( / ^ \\ x [ 0 - 9 a - f ] { 64 } $ / )
194+ ] ) ;
195+ } ) ;
196+
197+ it ( 'creates the same handoff continuation for reusable authentication' , async ( ) => {
198+ const { context, query } = makeContext ( {
199+ user_id : '00000000-0000-0000-0000-000000000011' ,
200+ callback_url : 'https://portal.example.com/auth/complete' ,
201+ site_state : opaque ,
202+ handoff_expires_at : '2026-08-10T00:01:00.000Z'
203+ } , { userId : '00000000-0000-0000-0000-000000000011' } ) ;
204+ const service = createUnifiedAuthService ( false ) ;
205+
206+ const result = await service . confirm (
207+ { constructive : context , browserBinding : opaque } ,
208+ { transactionId : opaque }
209+ ) ;
210+
211+ expect ( result . continuationUrl ) . toMatch (
212+ / ^ h t t p s : \/ \/ p o r t a l \. e x a m p l e \. c o m \/ a u t h \/ c o m p l e t e \? h a n d o f f = /
213+ ) ;
214+ expect ( query . mock . calls [ 0 ] [ 0 ] ) . toContain (
215+ '"tenant_acme_sso_private"."confirm_unified_login"'
216+ ) ;
217+ expect ( query . mock . calls [ 0 ] [ 1 ] ) . toEqual ( [
218+ expect . stringMatching ( / ^ \\ x [ 0 - 9 a - f ] { 64 } $ / ) ,
219+ expect . stringMatching ( / ^ \\ x [ 0 - 9 a - f ] { 64 } $ / ) ,
220+ expect . stringMatching ( / ^ \\ x [ 0 - 9 a - f ] { 64 } $ / )
167221 ] ) ;
168222 } ) ;
169223
224+ it ( 'creates the shared handoff through the registration wrapper' , async ( ) => {
225+ const { context, query } = makeContext ( {
226+ id : '00000000-0000-0000-0000-000000000010' ,
227+ user_id : '00000000-0000-0000-0000-000000000011' ,
228+ access_token : 'cnc_live_bt_registration' ,
229+ access_token_expires_at : '2026-08-10T00:00:00.000Z' ,
230+ is_verified : false ,
231+ totp_enabled : false ,
232+ mfa_required : false ,
233+ callback_url : 'https://portal.example.com/auth/complete' ,
234+ site_state : opaque ,
235+ handoff_expires_at : '2026-08-10T00:01:00.000Z'
236+ } ) ;
237+ const service = createUnifiedAuthService ( false ) ;
238+
239+ await expect ( service . signUp (
240+ { constructive : context , browserBinding : opaque } ,
241+ {
242+ transactionId : opaque ,
243+ email : 'new@example.com' ,
244+ password : 'correct horse battery staple'
245+ }
246+ ) ) . resolves . toMatchObject ( {
247+ accessToken : 'cnc_live_bt_registration' ,
248+ continuationUrl : expect . stringMatching ( / h a n d o f f = / )
249+ } ) ;
250+ expect ( query . mock . calls [ 0 ] [ 0 ] ) . toContain (
251+ '"tenant_acme_sso_private"."sign_up_unified_login"'
252+ ) ;
253+ } ) ;
254+
255+ it ( 'redeems through an authenticated routed Site runtime API key' , async ( ) => {
256+ const { context, query } = makeContext ( {
257+ id : '00000000-0000-0000-0000-000000000030' ,
258+ user_id : '00000000-0000-0000-0000-000000000031' ,
259+ access_token : 'cnc_live_bt_site' ,
260+ access_token_expires_at : '2026-08-10T01:00:00.000Z' ,
261+ is_verified : true ,
262+ totp_enabled : false ,
263+ mfa_required : false ,
264+ return_to : '/approvals/42'
265+ } , { runtime : true } ) ;
266+ const service = createUnifiedAuthService ( false ) ;
267+ const handoffCode = 'h' . repeat ( 43 ) ;
268+
269+ await expect ( service . redeem (
270+ { constructive : context } ,
271+ { handoffCode }
272+ ) ) . resolves . toMatchObject ( {
273+ accessToken : 'cnc_live_bt_site' ,
274+ returnTo : '/approvals/42'
275+ } ) ;
276+ expect ( query . mock . calls [ 0 ] [ 0 ] ) . toContain (
277+ '"tenant_acme_sso_private"."redeem_sso_handoff"'
278+ ) ;
279+ expect ( query . mock . calls [ 0 ] [ 1 ] ) . toEqual ( [
280+ expect . stringMatching ( / ^ \\ x [ 0 - 9 a - f ] { 64 } $ / )
281+ ] ) ;
282+ } ) ;
283+
284+ it ( 'does not let an auth-center browser credential redeem a Site handoff' , async ( ) => {
285+ const { context, query } = makeContext ( ) ;
286+ const service = createUnifiedAuthService ( false ) ;
287+
288+ await expect ( service . redeem (
289+ { constructive : context } ,
290+ { handoffCode : 'h' . repeat ( 43 ) }
291+ ) ) . rejects . toMatchObject ( { code : 'UNAUTHENTICATED' } ) ;
292+ expect ( query ) . not . toHaveBeenCalled ( ) ;
293+ } ) ;
294+
170295 it ( 'starts Provider authentication without exposing transaction or PKCE secrets' , async ( ) => {
171296 const { context, query } = makeContext ( {
172297 oauth_request_id : '00000000-0000-0000-0000-000000000099'
0 commit comments