array( array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','page_no'=>'smallint unsigned','generation'=>'char(32)','operation_id'=>'char(64)','page_digest'=>'char(64)','receipt_digest'=>'char(64)','default_erase'=>'char(1)','policy_payload'=>'text','state'=>'varchar(8)','recorded_at'=>'bigint unsigned','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','page_no'))))); } public function installNativeLocalSchema(): array { try { $this->connect(1);$this->validateSchema(); if($this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?',array($this->profile['database'],$this->prefix.'native_local_cleanup_v1'))){throw new OG_Privacy_Case_Refusal('native_local_schema_exists');} $this->sql('CREATE TABLE '.$this->table('native_local_cleanup_v1').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,page_no SMALLINT UNSIGNED NOT NULL,generation CHAR(32) NOT NULL,operation_id CHAR(64) NOT NULL,page_digest CHAR(64) NOT NULL,receipt_digest CHAR(64) NOT NULL,default_erase CHAR(1) NOT NULL,policy_payload TEXT NOT NULL,state VARCHAR(8) NOT NULL,recorded_at BIGINT UNSIGNED NOT NULL,body_hmac CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id,page_no)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateTableSpecs(self::localSpecs());return array('status'=>'native_local_schema_installed','erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'local_install');}finally{$this->finish();} } private static function localMac(array $row,string $secret): string { unset($row['body_hmac']);return hash_hmac('sha256',"og-native-local-page-v1\n".json_encode($row,JSON_THROW_ON_ERROR),hex2bin($secret)); } private static function localPolicy(array $body,array $policy): array { if(!array_is_list($policy)||count($policy)!==count($body['orders'])){throw new OG_Privacy_Case_Refusal('native_local_policy_invalid');} foreach($policy as $i=>$decision){ if(!is_array($decision)||array_keys($decision)!==array('ref','action','reason')||$decision['ref']!==$body['orders'][$i]['ref']|| !in_array($decision['action'],array('erase','retain'),true)||$decision['reason']!==($decision['action']==='retain'?'store_policy':'erasure_request')){ throw new OG_Privacy_Case_Refusal('native_local_policy_invalid'); } } return $policy; } private static function localRow(array $row,array $expected,array $body,string $secret): array { if(array_intersect_key($row,$expected)!==$expected||!in_array($row['default_erase'],array('0','1'),true)||!in_array($row['state'],array('prepared','verified'),true)|| self::nativeAttemptInteger($row['recorded_at'])<1||!hash_equals(self::localMac($row,$secret),$row['body_hmac'])){ throw new OG_Privacy_Case_Refusal('native_local_damaged'); } try{$policy=json_decode($row['policy_payload'],true,12,JSON_THROW_ON_ERROR);} catch(Throwable $e){throw new OG_Privacy_Case_Refusal('native_local_damaged');} if(!is_array($policy)||self::localPolicy($body,$policy)!==$policy||json_encode($policy,JSON_THROW_ON_ERROR)!==$row['policy_payload']){throw new OG_Privacy_Case_Refusal('native_local_damaged');} return $policy; } /** Freeze each WooCommerce retain/erase decision before any order write. * Replays return the signed decision and never call the policy filter again. */ public function freezeNativeLocalPolicy(array $context,$requestId,$page,callable $defaultResolver,callable $decider,$callerAssertsLegacyWritersDrained,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try{ if($callerAssertsLegacyWritersDrained!==true||!is_int($page)||$page<1||$page>100||!in_array($environment,array('production','review'),true)){throw new OG_Privacy_Case_Refusal('native_local_not_authorised');} $keys=OG_Native_Receipt_Keys::active($keys,$revoked); $b=$this->nativeScopeBinding($context,$requestId,array_merge(self::localSpecs(),self::nativePageSpecs(),self::nativeOrderFenceSpecs(),self::nativeRemoteSpecs(),self::nativeOutcomeSpecs())); $this->legalCleanupAdmission($context,$requestId,'orders');$secret=$this->nativeRemoteSecret();$generation=$context['generation']; $op=$this->nativeRemoteRow($b,$generation,$secret);if($op===null){throw new OG_Privacy_Case_Refusal('native_local_receipt_required');} $outcome=$this->nativeOutcomeRow($b,$generation,$op,$secret,$keys,$environment,$history,$revoked);if($outcome===null||$outcome['response']===''){throw new OG_Privacy_Case_Refusal('native_local_receipt_required');} $state=$this->nativePages($b);$fences=$this->nativeOrderFenceRows($b);$request=json_decode($op['payload'],true,12,JSON_THROW_ON_ERROR); if(!$state['observed_end']||$request['evidence_digest']!==$state['last_digest']||!isset($state['pages'][$page-1])||($fences[$page]??null)!==$state['pages'][$page-1]){throw new OG_Privacy_Case_Refusal('native_local_scope_changed');} $body=$state['pages'][$page-1];$args=array($b['install'],$b['namespace'],$requestId,$page); $select='SELECT generation,operation_id,page_digest,receipt_digest,default_erase,policy_payload,state,CAST(recorded_at AS CHAR) AS recorded_at,body_hmac FROM '.$this->table('native_local_cleanup_v1').' WHERE install_id=? AND namespace=? AND request_id=? AND page_no=?'; $expected=array('generation'=>$generation,'operation_id'=>$op['operation_id'],'page_digest'=>hash('sha256',json_encode($body,JSON_THROW_ON_ERROR)),'receipt_digest'=>hash('sha256',$outcome['response'])); $rows=$this->rows($select,$args); if($page===1&&$rows){if(count($rows)!==1){throw new OG_Privacy_Case_Refusal('native_local_damaged');}$policy=self::localRow($rows[0],$expected,$body,$secret);$this->commit();return array('status'=>'native_local_policy_frozen','policy'=>$policy,'default_erase'=>$rows[0]['default_erase']==='1','replayed'=>true,'erasure_complete'=>false);} if($page===1){try{$default=(bool)$defaultResolver();}catch(Throwable $e){throw new OG_Privacy_Case_Refusal('native_local_policy_unavailable');}} else{ $firstArgs=array($b['install'],$b['namespace'],$requestId,1);$first=$this->rows($select,$firstArgs); if(count($first)!==1){throw new OG_Privacy_Case_Refusal('native_local_default_required');} $firstBody=$state['pages'][0];$firstExpected=array('generation'=>$generation,'operation_id'=>$op['operation_id'],'page_digest'=>hash('sha256',json_encode($firstBody,JSON_THROW_ON_ERROR)),'receipt_digest'=>hash('sha256',$outcome['response'])); self::localRow($first[0],$firstExpected,$firstBody,$secret);if($first[0]['state']!=='verified'){throw new OG_Privacy_Case_Refusal('native_local_default_required');}$default=$first[0]['default_erase']==='1'; } if($rows){if(count($rows)!==1){throw new OG_Privacy_Case_Refusal('native_local_damaged');}$policy=self::localRow($rows[0],$expected,$body,$secret);if(($rows[0]['default_erase']==='1')!==$default){throw new OG_Privacy_Case_Refusal('native_local_damaged');}$this->commit();return array('status'=>'native_local_policy_frozen','policy'=>$policy,'default_erase'=>$default,'replayed'=>true,'erasure_complete'=>false);} try{$policy=$decider($body['orders'],$default);}catch(Throwable $e){$reason=in_array($e->getMessage(),array('local_order_changed','local_order_missing','local_order_unreadable'),true)?$e->getMessage():'native_local_policy_unavailable';throw new OG_Privacy_Case_Refusal($reason);} if(!is_array($policy)){$policy=array();}$policy=self::localPolicy($body,$policy);$payload=json_encode($policy,JSON_THROW_ON_ERROR); $row=$expected+array('default_erase'=>$default?'1':'0','policy_payload'=>$payload,'state'=>'prepared','recorded_at'=>(string)self::nativeAttemptInteger($this->rows('SELECT CAST(UNIX_TIMESTAMP() AS CHAR) AS now_seconds')[0]['now_seconds']??null));$row['body_hmac']=self::localMac($row,$secret); if($this->sql('INSERT INTO '.$this->table('native_local_cleanup_v1').' (install_id,namespace,request_id,page_no,generation,operation_id,page_digest,receipt_digest,default_erase,policy_payload,state,recorded_at,body_hmac) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)',array_merge($args,array_values($row)))!==1||$this->rows($select,$args)!==array($row)){throw new OG_Privacy_Case_Refusal('native_local_damaged');} $this->commit();return array('status'=>'native_local_policy_frozen','policy'=>$policy,'default_erase'=>$default,'replayed'=>false,'erasure_complete'=>false); }catch(Throwable $e){return array_merge($this->failure($e,'local_policy'),array('erasure_complete'=>false));}finally{$this->finish();} } /** Authenticated checkpoint inventory for browser sequencing, NOT proof that * order data stayed erased. Replayed pages still run the physical verifier. */ public function readNativeLocalProgress(array $context,$requestId,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try{ $keys=OG_Native_Receipt_Keys::active($keys,$revoked); $b=$this->nativeScopeBinding($context,$requestId,array_merge(self::localSpecs(),self::nativePageSpecs(),self::nativeOrderFenceSpecs(),self::nativeRemoteSpecs(),self::nativeOutcomeSpecs())); $secret=$this->nativeRemoteSecret();$op=$this->nativeRemoteRow($b,$context['generation'],$secret); if($op===null){throw new OG_Privacy_Case_Refusal('native_local_receipt_required');} $outcome=$this->nativeOutcomeRow($b,$context['generation'],$op,$secret,$keys,$environment,$history,$revoked); if($outcome===null||$outcome['response']===''){throw new OG_Privacy_Case_Refusal('native_local_receipt_required');} $state=$this->nativePages($b);$request=json_decode($op['payload'],true,12,JSON_THROW_ON_ERROR); if(!$state['observed_end']||$request['evidence_digest']!==$state['last_digest']||array_values($this->nativeOrderFenceRows($b))!==$state['pages']){throw new OG_Privacy_Case_Refusal('native_local_scope_changed');} $rows=$this->rows('SELECT CAST(page_no AS CHAR) AS page_no,generation,operation_id,page_digest,receipt_digest,default_erase,policy_payload,state,CAST(recorded_at AS CHAR) AS recorded_at,body_hmac FROM '.$this->table('native_local_cleanup_v1').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY page_no LIMIT 101',array($b['install'],$b['namespace'],$requestId)); if(count($rows)>100){throw new OG_Privacy_Case_Refusal('native_local_damaged');}$verified=array();$retained=array(); foreach($rows as $row){ $p=self::nativeAttemptInteger($row['page_no']);unset($row['page_no']); if($p<1||!isset($state['pages'][$p-1])){throw new OG_Privacy_Case_Refusal('native_local_damaged');} $expected=array('generation'=>$context['generation'],'operation_id'=>$op['operation_id'],'page_digest'=>hash('sha256',json_encode($state['pages'][$p-1],JSON_THROW_ON_ERROR)),'receipt_digest'=>hash('sha256',$outcome['response'])); $policy=self::localRow($row,$expected,$state['pages'][$p-1],$secret); if($row['state']==='verified'){$verified[]=$p;foreach($policy as $decision){if($decision['action']==='retain'){$retained[]=$decision['ref'];}}} } $this->commit();return array('status'=>'native_local_progress','verified_pages'=>$verified,'retained_orders'=>$retained,'retained_reason'=>'store_policy','erasure_complete'=>false); }catch(Throwable $e){return array_merge($this->failure($e,'local_progress'),array('erasure_complete'=>false));}finally{$this->finish();} } /** Internal callback and quiescence assertion are trusted, not caller authentication. * Always reverify actual order state, including when a checkpoint exists. */ public function cleanupNativePage(array $context,$requestId,$page,callable $cleaner,$callerAssertsLegacyWritersDrained,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try { if($callerAssertsLegacyWritersDrained!==true || !is_int($page)||$page<1||$page>100||!in_array($environment,array('production','review'),true)){throw new OG_Privacy_Case_Refusal('native_local_not_authorised');} $keys=OG_Native_Receipt_Keys::active($keys,$revoked); $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::localSpecs(),self::nativePageSpecs(),self::nativeOrderFenceSpecs(),self::nativeRemoteSpecs(),self::nativeOutcomeSpecs())); $this->legalCleanupAdmission($context,$requestId,'orders'); $secret=$this->nativeRemoteSecret();$generation=$context['generation'];$operation=$this->nativeRemoteRow($binding,$generation,$secret); if($operation===null){throw new OG_Privacy_Case_Refusal('native_local_receipt_required');} $outcome=$this->nativeOutcomeRow($binding,$generation,$operation,$secret,$keys,$environment,$history,$revoked); if($outcome===null || $outcome['response']===''){throw new OG_Privacy_Case_Refusal('native_local_receipt_required');} $state=$this->nativePages($binding);$fences=$this->nativeOrderFenceRows($binding); $request=json_decode($operation['payload'],true,12,JSON_THROW_ON_ERROR); if(!$state['observed_end'] || $request['evidence_digest']!==$state['last_digest'] || !isset($state['pages'][$page-1]) || ($fences[$page]??null)!==$state['pages'][$page-1]){throw new OG_Privacy_Case_Refusal('native_local_scope_changed');} $body=$state['pages'][$page-1];$args=array($binding['install'],$binding['namespace'],$requestId,$page); $select='SELECT generation,operation_id,page_digest,receipt_digest,default_erase,policy_payload,state,CAST(recorded_at AS CHAR) AS recorded_at,body_hmac FROM '.$this->table('native_local_cleanup_v1').' WHERE install_id=? AND namespace=? AND request_id=? AND page_no=?'; $row=array('generation'=>$generation,'operation_id'=>$operation['operation_id'],'page_digest'=>hash('sha256',json_encode($body,JSON_THROW_ON_ERROR)),'receipt_digest'=>hash('sha256',$outcome['response'])); $old=$this->rows($select,$args); if(count($old)!==1){throw new OG_Privacy_Case_Refusal('native_local_policy_required');}$policy=self::localRow($old[0],$row,$body,$secret); $guard=isset($context['legal_cleanup_token'])?$this->legalOrderGuard($context,$requestId):null; try{$cleaned=$cleaner($body['orders'],$policy,$guard);} catch(Throwable $e){ $reason=in_array($e->getMessage(),array('local_order_changed','local_order_unverified','local_notes_retained','local_order_missing','local_order_unreadable','legal_cleanup_guard_lost','native_order_meta_unreadable','native_order_meta_unverified','native_order_meta_marker_changed'),true)?$e->getMessage():'native_local_cleaner_failed'; throw new OG_Privacy_Case_Refusal($reason); } if($cleaned!==true){throw new OG_Privacy_Case_Refusal('native_local_verification_failed');} if($old[0]['state']==='prepared'){ $next=$old[0];$next['state']='verified';$next['body_hmac']=self::localMac($next,$secret); if($this->sql('UPDATE '.$this->table('native_local_cleanup_v1').' SET state=?,body_hmac=? WHERE install_id=? AND namespace=? AND request_id=? AND page_no=? AND state=? AND body_hmac=?',array('verified',$next['body_hmac'],$binding['install'],$binding['namespace'],$requestId,$page,'prepared',$old[0]['body_hmac']))!==1||$this->rows($select,$args)!==array($next)){throw new OG_Privacy_Case_Refusal('native_local_damaged');} } $retained=array();foreach($policy as $decision){if($decision['action']==='retain'){$retained[]=$decision['ref'];}} $this->commit();return array('status'=>'native_local_page_verified','page'=>$page,'replayed'=>$old[0]['state']==='verified','orders'=>count($body['orders']),'anonymised_orders'=>count($body['orders'])-count($retained),'retained_orders'=>$retained,'retained_reason'=>'store_policy','capture_end'=>$body['end'],'erasure_complete'=>false); }catch(Throwable $e){return array_merge($this->failure($e,'local_cleanup'),array('erasure_complete'=>false));}finally{$this->finish();} } }