@@ -2998,73 +2998,132 @@ describe('runtime policy stores', () => {
29982998 } ) ;
29992999 } ) ;
30003000
3001- test ( 'proxy policy and credential replacement recover from every persistence cut ' , {
3001+ test ( 'proxy replacement never persists its secret outside the credential vault ' , {
30023002 skip :
30033003 process . platform === 'win32'
30043004 ? 'POSIX file handles are required to inject persistence failures'
30053005 : false ,
30063006 } , async ( ) => {
3007- for ( let failedSync = 1 ; failedSync <= 7 ; failedSync += 1 ) {
3008- await withInteractiveOwner ( async ( { root, stores } ) => {
3009- const initial = await stores . runtimePolicy . getSnapshot ( ) ;
3010- const target = {
3007+ await withInteractiveOwner ( async ( { root, stores } ) => {
3008+ const initial = await stores . runtimePolicy . getSnapshot ( ) ;
3009+ const secret = 'vault-only-proxy-secret' ;
3010+ const probe = await open ( root , 'r' ) ;
3011+ const fileHandlePrototype = Object . getPrototypeOf ( probe ) as {
3012+ sync : typeof probe . sync ;
3013+ } ;
3014+ const originalSync = fileHandlePrototype . sync ;
3015+ await probe . close ( ) ;
3016+ let syncCalls = 0 ;
3017+ const syncMock = mock . method (
3018+ fileHandlePrototype ,
3019+ 'sync' ,
3020+ async function ( this : typeof probe ) {
3021+ syncCalls += 1 ;
3022+ if ( syncCalls === 3 ) throw new Error ( 'injected proxy policy persistence failure' ) ;
3023+ return originalSync . call ( this ) ;
3024+ } ,
3025+ ) ;
3026+
3027+ try {
3028+ await assert . rejects (
3029+ stores . operations . updateNetworkProxy ( {
3030+ expectedPolicyRevision : initial . revision ,
3031+ expectedCredential : null ,
3032+ networkProxy : {
3033+ ...initial . policy . networkProxy ,
3034+ enabled : true ,
3035+ host : 'vault-only.proxy.internal' ,
3036+ port : 7897 ,
3037+ authEnabled : true ,
3038+ username : 'vault-only-user' ,
3039+ } ,
3040+ credential : { kind : 'replace' , secret } ,
3041+ } ) ,
3042+ isStoreError ( 'commit_outcome_unknown' ) ,
3043+ ) ;
3044+ } finally {
3045+ syncMock . mock . restore ( ) ;
3046+ }
3047+
3048+ const filesContainingSecret : string [ ] = [ ] ;
3049+ for ( const entry of await readdir ( root ) ) {
3050+ if ( ! entry . endsWith ( '.json' ) ) continue ;
3051+ if ( ( await readFile ( join ( root , entry ) , 'utf8' ) ) . includes ( secret ) ) {
3052+ filesContainingSecret . push ( entry ) ;
3053+ }
3054+ }
3055+ assert . deepEqual ( filesContainingSecret , [ 'credential-vault.json' ] ) ;
3056+ assert . equal ( existsSync ( join ( root , 'runtime-policy-network-proxy.json' ) ) , false ) ;
3057+ assert . equal ( existsSync ( join ( root , 'runtime-policy.json' ) ) , false ) ;
3058+ } ) ;
3059+ } ) ;
3060+
3061+ test ( 'disabling proxy authentication commits policy before deleting its credential' , {
3062+ skip :
3063+ process . platform === 'win32'
3064+ ? 'POSIX file handles are required to inject persistence failures'
3065+ : false ,
3066+ } , async ( ) => {
3067+ await withInteractiveOwner ( async ( { root, stores } ) => {
3068+ const initial = await stores . runtimePolicy . getSnapshot ( ) ;
3069+ const secret = 'retained-disabled-proxy-secret' ;
3070+ const configured = await stores . operations . updateNetworkProxy ( {
3071+ expectedPolicyRevision : initial . revision ,
3072+ expectedCredential : null ,
3073+ networkProxy : {
30113074 ...initial . policy . networkProxy ,
30123075 enabled : true ,
3013- host : 'recoverable .proxy.internal' ,
3076+ host : 'disable-order .proxy.internal' ,
30143077 port : 7897 ,
30153078 authEnabled : true ,
3016- username : 'recoverable-user' ,
3017- } ;
3018- const probe = await open ( root , 'r' ) ;
3019- const fileHandlePrototype = Object . getPrototypeOf ( probe ) as {
3020- sync : typeof probe . sync ;
3021- } ;
3022- const originalSync = fileHandlePrototype . sync ;
3023- await probe . close ( ) ;
3024- let syncCalls = 0 ;
3025- const syncMock = mock . method (
3026- fileHandlePrototype ,
3027- 'sync' ,
3028- async function ( this : typeof probe ) {
3029- syncCalls += 1 ;
3030- if ( syncCalls === failedSync ) {
3031- throw new Error ( `injected proxy update persistence cut ${ failedSync } ` ) ;
3032- }
3033- return originalSync . call ( this ) ;
3034- } ,
3035- ) ;
3079+ username : 'disable-order-user' ,
3080+ } ,
3081+ credential : { kind : 'replace' , secret } ,
3082+ } ) ;
3083+ assert . equal ( configured . kind , 'committed' ) ;
3084+ if ( configured . kind !== 'committed' ) return ;
30363085
3037- try {
3038- await assert . rejects (
3039- stores . operations . updateNetworkProxy ( {
3040- expectedPolicyRevision : initial . revision ,
3041- expectedCredential : null ,
3042- networkProxy : target ,
3043- credential : { kind : 'replace' , secret : 'recoverable-secret' } ,
3044- } ) ,
3045- isStoreError ( failedSync === 1 ? 'io_failed' : 'commit_outcome_unknown' ) ,
3046- ) ;
3047- } finally {
3048- syncMock . mock . restore ( ) ;
3049- }
3050- assert . equal ( syncCalls , failedSync ) ;
3051-
3052- const policy = await stores . runtimePolicy . getSnapshot ( ) ;
3053- const credential = await getCredentialStatus ( stores . credentialVault , proxyCredential ( ) ) ;
3054- if ( failedSync === 1 ) {
3055- assert . deepEqual ( policy , initial ) ;
3056- assert . equal ( credential . configured , false ) ;
3057- assert . equal ( existsSync ( join ( root , 'runtime-policy-network-proxy.json' ) ) , false ) ;
3058- return ;
3059- }
3086+ const probe = await open ( root , 'r' ) ;
3087+ const fileHandlePrototype = Object . getPrototypeOf ( probe ) as {
3088+ sync : typeof probe . sync ;
3089+ } ;
3090+ const originalSync = fileHandlePrototype . sync ;
3091+ await probe . close ( ) ;
3092+ let syncCalls = 0 ;
3093+ const syncMock = mock . method (
3094+ fileHandlePrototype ,
3095+ 'sync' ,
3096+ async function ( this : typeof probe ) {
3097+ syncCalls += 1 ;
3098+ if ( syncCalls === 3 ) throw new Error ( 'injected proxy credential deletion failure' ) ;
3099+ return originalSync . call ( this ) ;
3100+ } ,
3101+ ) ;
30603102
3061- assert . deepEqual ( policy . policy . networkProxy , target ) ;
3062- assert . equal ( credential . configured , true ) ;
3063- assert . equal ( existsSync ( join ( root , 'runtime-policy-network-proxy.json' ) ) , false ) ;
3064- const material = await stores . operations . exportCredentialMaterial ( proxyCredential ( ) ) ;
3065- assert . equal ( material ?. secret , 'recoverable-secret' ) ;
3066- } ) ;
3067- }
3103+ try {
3104+ await assert . rejects (
3105+ stores . operations . updateNetworkProxy ( {
3106+ expectedPolicyRevision : configured . snapshot . revision ,
3107+ expectedCredential : credentialBasis ( configured . credentialStatus ) ,
3108+ networkProxy : {
3109+ ...configured . snapshot . policy . networkProxy ,
3110+ authEnabled : false ,
3111+ username : '' ,
3112+ } ,
3113+ credential : { kind : 'delete' } ,
3114+ } ) ,
3115+ isStoreError ( 'commit_outcome_unknown' ) ,
3116+ ) ;
3117+ } finally {
3118+ syncMock . mock . restore ( ) ;
3119+ }
3120+
3121+ const persistedPolicy = JSON . parse (
3122+ await readFile ( join ( root , 'runtime-policy.json' ) , 'utf8' ) ,
3123+ ) as { readonly policy : { readonly networkProxy : RuntimePolicy [ 'networkProxy' ] } } ;
3124+ assert . equal ( persistedPolicy . policy . networkProxy . authEnabled , false ) ;
3125+ assert . ok ( ( await readFile ( join ( root , 'credential-vault.json' ) , 'utf8' ) ) . includes ( secret ) ) ;
3126+ } ) ;
30683127 } ) ;
30693128
30703129 test ( 'blocks WebFetch while privacy mode is active' , async ( ) => {
0 commit comments