@@ -2,10 +2,12 @@ import { createHash } from 'node:crypto'
22import { createServer , type Server } from 'node:http'
33import { db } from '@sim/db'
44import { clientCredentialToken } from '@sim/db/schema'
5+ import { createDeferred } from '@sim/testing/helpers/deferred'
56import { sleep } from '@sim/utils/helpers'
67import { generateId } from '@sim/utils/id'
7- import { eq , like , or } from 'drizzle-orm'
8+ import { eq , like , or , sql } from 'drizzle-orm'
89import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest'
10+ import { decryptSecret , encryptSecret } from '@/lib/core/security/encryption'
911import {
1012 invalidateVantaClientToken ,
1113 resolveVantaClientToken ,
@@ -34,6 +36,67 @@ function fields(suffix: string) {
3436 return { clientId, clientSecret : `secret-${ clientId } ` , scope : READ }
3537}
3638
39+ function sha256Json ( value : unknown ) : string {
40+ return createHash ( 'sha256' ) . update ( JSON . stringify ( value ) ) . digest ( 'hex' )
41+ }
42+
43+ /** Row ids and digests exactly as the build before the shared resolver derived them. */
44+ function previousBuildKeys ( input : { clientId : string ; clientSecret : string } , scope : string ) {
45+ const apiDomain = 'https://api.vanta.com'
46+ const applicationKey = `vanta:${ sha256Json ( [ apiDomain , input . clientId ] ) } `
47+ const secretFingerprint = sha256Json ( [ apiDomain , input . clientId , input . clientSecret ] )
48+ return {
49+ applicationKey,
50+ secretFingerprint,
51+ failureKey : `${ applicationKey } :${ sha256Json ( [ secretFingerprint , scope ] ) } ` ,
52+ accessTokenDigest : ( accessToken : string ) => sha256Json ( [ apiDomain , accessToken ] ) ,
53+ }
54+ }
55+
56+ /** Writes an application token row the way the previous build's mint did. */
57+ async function storePreviousBuildToken (
58+ keys : ReturnType < typeof previousBuildKeys > ,
59+ accessToken : string ,
60+ scope : string ,
61+ expiresInMs : number ,
62+ client : Pick < typeof db , 'insert' > = db
63+ ) {
64+ const { encrypted } = await encryptSecret (
65+ JSON . stringify ( { accessToken, secretFingerprint : keys . secretFingerprint , scope } )
66+ )
67+ await client . insert ( clientCredentialToken ) . values ( {
68+ id : keys . applicationKey ,
69+ encryptedValue : encrypted ,
70+ accessTokenDigest : keys . accessTokenDigest ( accessToken ) ,
71+ expiresAt : new Date ( Date . now ( ) + expiresInMs ) ,
72+ } )
73+ }
74+
75+ async function readStoredRow ( id : string ) {
76+ const [ row ] = await db
77+ . select ( )
78+ . from ( clientCredentialToken )
79+ . where ( eq ( clientCredentialToken . id , id ) )
80+ . limit ( 1 )
81+ if ( ! row ) throw new Error ( 'Expected a stored row' )
82+ return { ...row , decrypted : ( await decryptSecret ( row . encryptedValue ) ) . decrypted }
83+ }
84+
85+ /** Resolves once a session is blocked on the advisory lock for `key`; fails if `pending` settles first. */
86+ async function lockWaiterAppears ( key : string , pending : { settled : boolean } ) : Promise < void > {
87+ for ( ; ; ) {
88+ const waiting = await db . execute ( sql `
89+ WITH k AS (SELECT hashtextextended(${ key } , 0) AS h)
90+ SELECT 1 FROM pg_locks, k
91+ WHERE locktype = 'advisory' AND NOT granted AND objsubid = 1
92+ AND classid::bigint = ((k.h >> 32) & 4294967295)
93+ AND objid::bigint = (k.h & 4294967295)` )
94+ if ( waiting . length > 0 ) return
95+ if ( pending . settled ) throw new Error ( 'Resolution finished without waiting on the lock' )
96+ await sleep ( 10 )
97+ }
98+ }
99+
37100async function isActive ( token : string ) : Promise < boolean > {
38101 return (
39102 await originalFetch ( `${ providerUrl } /verify` , {
@@ -114,10 +177,12 @@ afterAll(async () => {
114177
115178describe ( 'Vanta application token lifecycle against shared storage' , ( ) => {
116179 it ( 'shares one active token across simultaneous resolutions for an application' , async ( ) => {
180+ const count = tokenRequests
117181 const results = await Promise . all (
118182 Array . from ( { length : 12 } , ( ) => resolveVantaClientToken ( fields ( 'concurrent' ) ) )
119183 )
120184 expect ( new Set ( results . map ( ( result ) => result . accessToken ) ) . size ) . toBe ( 1 )
185+ expect ( tokenRequests ) . toBe ( count + 1 )
121186 expect ( await isActive ( results [ 0 ] . accessToken ) ) . toBe ( true )
122187 expect ( ( await resolveVantaClientToken ( fields ( 'concurrent' ) ) ) . accessToken ) . toBe (
123188 results [ 0 ] . accessToken
@@ -265,3 +330,119 @@ describe('Vanta application token lifecycle against shared storage', () => {
265330 expect ( tokenRequests ) . toBe ( count )
266331 } )
267332} )
333+
334+ describe ( 'Vanta token rows shared with the previous build during a rolling deploy' , ( ) => {
335+ it ( 'serves a fresh token the previous build stored down to the 30-second margin without calling Vanta' , async ( ) => {
336+ const input = fields ( 'legacy-fresh' )
337+ await storePreviousBuildToken (
338+ previousBuildKeys ( input , READ ) ,
339+ 'legacy-fresh-token' ,
340+ READ ,
341+ 60_000
342+ )
343+ const count = tokenRequests
344+
345+ await expect ( resolveVantaClientToken ( input ) ) . resolves . toMatchObject ( {
346+ accessToken : 'legacy-fresh-token' ,
347+ apiDomain : 'https://api.vanta.com' ,
348+ } )
349+ expect ( tokenRequests ) . toBe ( count )
350+ } )
351+
352+ it ( 'honors a refusal the previous build remembered without calling Vanta' , async ( ) => {
353+ const input = fields ( 'legacy-failure' )
354+ const { encrypted } = await encryptSecret (
355+ JSON . stringify ( { code : 'invalid_credentials' , status : 401 } )
356+ )
357+ await db . insert ( clientCredentialToken ) . values ( {
358+ id : previousBuildKeys ( input , READ ) . failureKey ,
359+ encryptedValue : encrypted ,
360+ expiresAt : new Date ( Date . now ( ) + 20_000 ) ,
361+ } )
362+ const count = tokenRequests
363+
364+ await expect ( resolveVantaClientToken ( input ) ) . rejects . toMatchObject ( {
365+ code : 'invalid_credentials' ,
366+ status : 401 ,
367+ } )
368+ expect ( tokenRequests ) . toBe ( count )
369+ } )
370+
371+ it ( 'refuses other permissions for an expired application the previous build pinned' , async ( ) => {
372+ const input = fields ( 'legacy-expired-scope' )
373+ await storePreviousBuildToken ( previousBuildKeys ( input , READ ) , 'legacy-expired' , READ , - 1000 )
374+ const count = tokenRequests
375+
376+ await expect ( resolveVantaClientToken ( { ...input , scope : WRITE } ) ) . rejects . toMatchObject ( {
377+ code : 'permission_conflict' ,
378+ } )
379+ expect ( tokenRequests ) . toBe ( count )
380+ } )
381+
382+ it ( 'stores a minted token where and how the previous build reads it' , async ( ) => {
383+ const input = fields ( 'legacy-readable-mint' )
384+ const keys = previousBuildKeys ( input , READ )
385+
386+ const { accessToken } = await resolveVantaClientToken ( input )
387+
388+ const row = await readStoredRow ( keys . applicationKey )
389+ expect ( row . decrypted ) . toBe (
390+ JSON . stringify ( { accessToken, secretFingerprint : keys . secretFingerprint , scope : READ } )
391+ )
392+ expect ( row . accessTokenDigest ) . toBe ( keys . accessTokenDigest ( accessToken ) )
393+ } )
394+
395+ it ( 'stores a refusal where and how the previous build reads it' , async ( ) => {
396+ const input = fields ( 'legacy-readable-failure' )
397+ const keys = previousBuildKeys ( { ...input , clientSecret : 'revoked-secret' } , READ )
398+
399+ await expect (
400+ resolveVantaClientToken ( { ...input , clientSecret : 'revoked-secret' } )
401+ ) . rejects . toMatchObject ( { code : 'invalid_credentials' } )
402+
403+ const row = await readStoredRow ( keys . failureKey )
404+ expect ( row . decrypted ) . toBe ( JSON . stringify ( { code : 'invalid_credentials' , status : 401 } ) )
405+ expect ( row . accessTokenDigest ) . toBeNull ( )
406+ } )
407+
408+ it ( 'expires a token the previous build stored when Vanta rejects it' , async ( ) => {
409+ const input = fields ( 'legacy-invalidated' )
410+ const keys = previousBuildKeys ( input , READ )
411+ await storePreviousBuildToken ( keys , 'legacy-rejected-token' , READ , 3_600_000 )
412+
413+ await invalidateVantaClientToken ( 'legacy-rejected-token' , 'https://api.vanta.com' )
414+
415+ expect ( ( await readStoredRow ( keys . applicationKey ) ) . expiresAt . getTime ( ) ) . toBe ( 0 )
416+ } )
417+
418+ it ( 'waits on the application lock the previous build holds and serves the token it minted' , async ( ) => {
419+ const input = fields ( 'legacy-lock' )
420+ const keys = previousBuildKeys ( input , READ )
421+ const locked = createDeferred < void > ( )
422+ const commit = createDeferred < void > ( )
423+ const previousBuild = db . transaction ( async ( tx ) => {
424+ await tx . execute (
425+ sql `SELECT pg_advisory_xact_lock(hashtextextended(${ keys . applicationKey } , 0))`
426+ )
427+ locked . resolve ( )
428+ await commit . promise
429+ await storePreviousBuildToken ( keys , 'legacy-locked-token' , READ , 3_600_000 , tx )
430+ } )
431+ await locked . promise
432+ const count = tokenRequests
433+ const pending = { settled : false }
434+ const resolution = resolveVantaClientToken ( input ) . finally ( ( ) => {
435+ pending . settled = true
436+ } )
437+
438+ try {
439+ await lockWaiterAppears ( keys . applicationKey , pending )
440+ } finally {
441+ commit . resolve ( )
442+ await previousBuild
443+ }
444+
445+ await expect ( resolution ) . resolves . toMatchObject ( { accessToken : 'legacy-locked-token' } )
446+ expect ( tokenRequests ) . toBe ( count )
447+ } )
448+ } )
0 commit comments