@@ -110,6 +110,9 @@ function statefulAPI(
110110 failActual ?: unknown ;
111111 malformedDryRun ?: boolean ;
112112 replaceProjection ?: boolean ;
113+ dryRunProjectionOverride ?: Record < string , unknown > ;
114+ actualProjectionOverride ?: Record < string , unknown > ;
115+ persistedActualState ?: AppleConnection ;
113116 persistActual ?: boolean ;
114117 } = { } ,
115118) : {
@@ -173,12 +176,20 @@ function statefulAPI(
173176 ? { ...( update as Record < string , unknown > ) }
174177 : { ...current , ...( update as Record < string , unknown > ) }
175178 ) as AppleConnection ;
179+ const projectionOverride = patchOptions . dryRun
180+ ? options . dryRunProjectionOverride
181+ : options . actualProjectionOverride ;
182+ if ( projectionOverride ) Object . assign ( after , structuredClone ( projectionOverride ) ) ;
176183 if ( patchOptions . dryRun && options . malformedDryRun ) {
177184 return { config_version : version , dry_run : true , before : { } , after : { } } ;
178185 }
179186 if ( ! patchOptions . dryRun ) {
180187 writes += 1 ;
181- if ( options . persistActual !== false ) current = after ;
188+ if ( options . persistActual !== false ) {
189+ current = options . persistedActualState
190+ ? structuredClone ( options . persistedActualState )
191+ : after ;
192+ }
182193 version = NEXT_CONFIG_VERSION ;
183194 }
184195 return {
@@ -590,6 +601,100 @@ describe("native Sign in with Apple remote setup", () => {
590601 expect ( captured . err ) . not . toContain ( PRIVATE_KEY ) ;
591602 } ) ;
592603
604+ test ( "rejects a dry-run projection that changes a nested preserved field" , async ( ) => {
605+ const harness = statefulAPI ( {
606+ initial : connection ( false , false , {
607+ unrelated_provider_setting : {
608+ nested : { mode : "keep" , secret : PRIVATE_KEY } ,
609+ } ,
610+ } ) ,
611+ dryRunProjectionOverride : {
612+ unrelated_provider_setting : {
613+ nested : { mode : "changed" , secret : PRIVATE_KEY } ,
614+ } ,
615+ } ,
616+ } ) ;
617+ const prepared = await prepareIOSNativeAppleConnection ( baseOptions ( ) , {
618+ api : harness . api ,
619+ prompts : unexpectedPrompts ( ) ,
620+ } ) ;
621+ if ( prepared . status !== "ready" ) throw new Error ( "expected ready plan" ) ;
622+
623+ await expect ( applyIOSNativeAppleConnection ( prepared , harness . api ) ) . rejects . toThrow (
624+ "could not safely validate native Sign in with Apple" ,
625+ ) ;
626+ expect ( harness . patchCalls . map ( ( call ) => call . options . dryRun ) ) . toEqual ( [ true ] ) ;
627+ expect ( harness . actualWrites ( ) ) . toBe ( 0 ) ;
628+ expect ( captured . err ) . not . toContain ( PRIVATE_KEY ) ;
629+ } ) ;
630+
631+ test ( "rejects an actual-write projection that changes a preserved credential value" , async ( ) => {
632+ const changedSecret = `${ PRIVATE_KEY } _CHANGED` ;
633+ const harness = statefulAPI ( {
634+ initial : connection ( false , false , { client_secret : PRIVATE_KEY } ) ,
635+ actualProjectionOverride : { client_secret : changedSecret } ,
636+ } ) ;
637+ const prepared = await prepareIOSNativeAppleConnection ( baseOptions ( ) , {
638+ api : harness . api ,
639+ prompts : unexpectedPrompts ( ) ,
640+ } ) ;
641+ if ( prepared . status !== "ready" ) throw new Error ( "expected ready plan" ) ;
642+
643+ let thrown : unknown ;
644+ try {
645+ await applyIOSNativeAppleConnection ( prepared , harness . api ) ;
646+ } catch ( error ) {
647+ thrown = error ;
648+ }
649+ expect ( thrown ) . toMatchObject ( {
650+ code : ERROR_CODE . PLAPI_UNEXPECTED_RESPONSE ,
651+ message : expect . stringContaining ( "removed or changed existing fields" ) ,
652+ } ) ;
653+ expect ( harness . patchCalls . map ( ( call ) => call . options . dryRun ) ) . toEqual ( [ true , false ] ) ;
654+ expect ( harness . actualWrites ( ) ) . toBe ( 1 ) ;
655+ expect ( String ( thrown ) ) . not . toContain ( PRIVATE_KEY ) ;
656+ expect ( String ( thrown ) ) . not . toContain ( changedSecret ) ;
657+ expect ( captured . err ) . not . toContain ( PRIVATE_KEY ) ;
658+ expect ( captured . err ) . not . toContain ( changedSecret ) ;
659+ } ) ;
660+
661+ test ( "rejects a final state that drops a secret despite preserving projections" , async ( ) => {
662+ const initial = connection ( false , true , {
663+ client_id : SERVICES_ID ,
664+ client_secret : PRIVATE_KEY ,
665+ unrelated_provider_setting : { nested : { mode : "keep" } } ,
666+ } ) ;
667+ const harness = statefulAPI ( {
668+ initial,
669+ persistedActualState : connection ( true , true , {
670+ bundle_id : BUNDLE_IDENTIFIER ,
671+ client_id : SERVICES_ID ,
672+ unrelated_provider_setting : { nested : { mode : "keep" } } ,
673+ } ) ,
674+ } ) ;
675+ const prepared = await prepareIOSNativeAppleConnection ( baseOptions ( ) , {
676+ api : harness . api ,
677+ prompts : unexpectedPrompts ( ) ,
678+ } ) ;
679+ if ( prepared . status !== "ready" ) throw new Error ( "expected ready plan" ) ;
680+
681+ let thrown : unknown ;
682+ try {
683+ await applyIOSNativeAppleConnection ( prepared , harness . api ) ;
684+ } catch ( error ) {
685+ thrown = error ;
686+ }
687+ expect ( thrown ) . toMatchObject ( {
688+ code : ERROR_CODE . IOS_REMOTE_VERIFY_FAILED ,
689+ message : expect . stringContaining ( "did not pass final verification" ) ,
690+ } ) ;
691+ expect ( harness . patchCalls . map ( ( call ) => call . options . dryRun ) ) . toEqual ( [ true , false ] ) ;
692+ expect ( harness . actualWrites ( ) ) . toBe ( 1 ) ;
693+ expect ( String ( thrown ) ) . not . toContain ( PRIVATE_KEY ) ;
694+ expect ( JSON . stringify ( prepared ) ) . not . toContain ( PRIVATE_KEY ) ;
695+ expect ( captured . err ) . not . toContain ( PRIVATE_KEY ) ;
696+ } ) ;
697+
593698 test ( "rereads final state and rejects a write that did not persist" , async ( ) => {
594699 const harness = statefulAPI ( { persistActual : false } ) ;
595700 const prepared = await prepareIOSNativeAppleConnection ( baseOptions ( ) , {
0 commit comments