self::REQUEST_VERSION,'request'=>$request,'new_operation_id'=>$newOperation)); } /** Shape check for transport use; the store re-derives every field. */ public static function checkPayload(string $payload): array { if(strlen($payload)>OG_Native_Remote_Binding::MAX_BYTES+256){throw new InvalidArgumentException('restart_capacity');} $body=json_decode($payload,true,32,JSON_THROW_ON_ERROR); if(!is_array($body) || array_keys($body)!==array('version','request','new_operation_id') || !is_array($body['request']) || !is_string($body['new_operation_id']) || self::requestPayload($body['request'],$body['new_operation_id'])!==$payload){throw new InvalidArgumentException('restart_invalid');} return $body; } /** Unsigned queue status. Only 'approved'/'used' carry a signed approval. */ public static function status(string $body,string $newOperation): array { if(strlen($body)>16384){throw new InvalidArgumentException('restart_status');} $value=json_decode($body,true,8,JSON_THROW_ON_ERROR); if(!is_array($value) || array_keys($value)!==array('version','restart_id','new_operation_id','status','approval','erasure_complete') || OG_Native_Remote_Binding::canonical($value)!==$body || $value['version']!==self::STATUS_VERSION || !is_string($value['restart_id']) || !preg_match('/^[a-f0-9]{64}$/D',$value['restart_id']) || $value['new_operation_id']!==$newOperation || !in_array($value['status'],array('requested','approved','rejected','used'),true) || !is_string($value['approval']) || $value['erasure_complete']!==false || (in_array($value['status'],array('approved','used'),true)!==($value['approval']!==''))){throw new InvalidArgumentException('restart_status');} return $value; } public static function verify(string $body,array $active,array $revoked,string $environment): array { if(strlen($body)>8192 || !in_array($environment,array('production','review'),true) || !function_exists('sodium_crypto_sign_verify_detached')){throw new InvalidArgumentException('approval_invalid');} $envelope=json_decode($body,true,4,JSON_THROW_ON_ERROR); if(!is_array($envelope) || array_keys($envelope)!==array('format','algorithm','environment','key_id','approval','signature') || OG_Native_Remote_Binding::canonical($envelope)!==$body || $envelope['format']!==self::FORMAT || $envelope['algorithm']!=='Ed25519' || $envelope['environment']!==$environment || !is_string($envelope['key_id']) || !preg_match('/^'.($environment==='production'?'prod':'test').'\-[a-z0-9-]{1,48}$/D',$envelope['key_id']) || isset($revoked[$envelope['key_id']]) || !is_string($envelope['approval']) || strlen($envelope['approval'])>4096 || !is_string($envelope['signature']) || !preg_match('/^[a-f0-9]{128}$/D',$envelope['signature'])){throw new InvalidArgumentException('approval_invalid');} $public=$active[$envelope['key_id']]??null; if(!is_string($public)){throw new InvalidArgumentException('approval_key_unknown');} OG_Native_Remote_Binding::validateRegistry(array($envelope['key_id']=>$public)); $signed=$envelope;unset($signed['signature']); if(!sodium_crypto_sign_verify_detached(hex2bin($envelope['signature']),self::PREFIX.OG_Native_Remote_Binding::canonical($signed),hex2bin($public))){throw new InvalidArgumentException('approval_signature');} $approval=json_decode($envelope['approval'],true,4,JSON_THROW_ON_ERROR); if(!is_array($approval) || array_keys($approval)!==self::FIELDS || OG_Native_Remote_Binding::canonical($approval)!==$envelope['approval'] || $approval['version']!==self::VERSION || $approval['decision']!=='approved' || $approval['authority']!=='owner' || !is_int($approval['approved_at']) || $approval['approved_at']<1){throw new InvalidArgumentException('approval_invalid');} foreach(array('restart_id'=>64,'install'=>32,'namespace'=>32,'superseded_operation_id'=>64,'presented_operation_id'=>64,'new_operation_id'=>64,'template_digest'=>64) as $field=>$length){ if(!is_string($approval[$field]) || !preg_match('/^[a-f0-9]{'.$length.'}$/D',$approval[$field])){throw new InvalidArgumentException('approval_invalid');} } foreach(array('request_id','cutoff_seconds') as $field){ if(!is_string($approval[$field]) || !preg_match('/^[1-9][0-9]{0,18}$/D',$approval[$field])){throw new InvalidArgumentException('approval_invalid');} } return $approval; } } /** Internal trusted-adapter seam for the fixed restart endpoint. */ interface OG_Native_Restart_Client { public function restart(string $payload): ?string; public function result(): array; } /** Restart journal, per-operation audit, archive and alerts. Caller authorizes; * the signed owner approval is the only input that can replace an operation. * No network inside transactions, no erasure, no completion. */ trait OG_Native_Restart { private static function restartSpecs(): array { return array( 'native_restart_requests_v1'=>array(array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','new_operation_id'=>'char(64)','generation'=>'char(32)','superseded_operation_id'=>'char(64)','restart_id'=>'varchar(64)','state'=>'varchar(12)','approval'=>'mediumtext','actor_id'=>'bigint unsigned','created_at'=>'bigint unsigned','updated_at'=>'bigint unsigned','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','new_operation_id')))), 'native_operation_archive_v1'=>array(array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','operation_id'=>'char(64)','kind'=>'varchar(24)','seq'=>'smallint unsigned','payload'=>'mediumtext','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','operation_id','kind','seq')))), 'native_operation_audit_v1'=>array(array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','seq'=>'smallint unsigned','operation_id'=>'char(64)','event'=>'varchar(32)','actor_id'=>'bigint unsigned','recorded_at'=>'bigint unsigned','detail'=>'varchar(64)','prev_hmac'=>'char(64)','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','seq')))), )+self::operationAttentionSpecs(); } private static function restartEvents(): array { return array('restart_requested','restart_submitted','restart_rejected','restart_refused','restart_approval_invalid','restart_approval_verified','operation_superseded','operation_issued'); } public function installNativeRestartSchema(): array { try { $this->connect(1);$this->validateSchema(); $names=array_keys(self::restartSpecs()); if($this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME IN (?,?,?,?)',array_merge(array($this->profile['database']),array_map(fn($n)=>$this->prefix.$n,$names)))){throw new OG_Privacy_Case_Refusal('native_restart_schema_exists');} $this->sql('CREATE TABLE '.$this->table('native_restart_requests_v1').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,new_operation_id CHAR(64) NOT NULL,generation CHAR(32) NOT NULL,superseded_operation_id CHAR(64) NOT NULL,restart_id VARCHAR(64) NOT NULL,state VARCHAR(12) NOT NULL,approval MEDIUMTEXT NOT NULL,actor_id BIGINT UNSIGNED NOT NULL,created_at BIGINT UNSIGNED NOT NULL,updated_at BIGINT UNSIGNED NOT NULL,body_hmac CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id,new_operation_id)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->sql('CREATE TABLE '.$this->table('native_operation_archive_v1').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,operation_id CHAR(64) NOT NULL,kind VARCHAR(24) NOT NULL,seq SMALLINT UNSIGNED NOT NULL,payload MEDIUMTEXT NOT NULL,body_hmac CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id,operation_id,kind,seq)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->sql('CREATE TABLE '.$this->table('native_operation_audit_v1').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,seq SMALLINT UNSIGNED NOT NULL,operation_id CHAR(64) NOT NULL,event VARCHAR(32) NOT NULL,actor_id BIGINT UNSIGNED NOT NULL,recorded_at BIGINT UNSIGNED NOT NULL,detail VARCHAR(64) NOT NULL,prev_hmac CHAR(64) NOT NULL,body_hmac CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id,seq)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->sql('CREATE TABLE '.$this->table('native_operation_attention_v1').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,operation_id CHAR(64) NOT NULL,reason VARCHAR(32) NOT NULL,recorded_at BIGINT UNSIGNED NOT NULL,PRIMARY KEY(install_id,namespace,request_id,operation_id,reason)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateTableSpecs(self::restartSpecs()); return array('status'=>'native_restart_schema_installed','erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_restart_install');}finally{$this->finish();} } private static function restartMac(array $row,string $secret): string { unset($row['body_hmac']);return hash_hmac('sha256',"og-native-restart-journal-v1\n".json_encode($row,JSON_THROW_ON_ERROR),hex2bin($secret)); } private static function archiveMac(string $operation,string $kind,int $seq,string $payload,string $secret): string { return hash_hmac('sha256',"og-native-operation-archive-v1\n".json_encode(array($operation,$kind,$seq,$payload),JSON_THROW_ON_ERROR),hex2bin($secret)); } private static function auditMac(array $row,string $secret): string { unset($row['body_hmac']);return hash_hmac('sha256',"og-native-operation-audit-v1\n".json_encode($row,JSON_THROW_ON_ERROR),hex2bin($secret)); } private function restartRows(array $args,string $generation,string $secret): array { $rows=$this->rows('SELECT new_operation_id,generation,superseded_operation_id,restart_id,state,approval,CAST(actor_id AS CHAR) AS actor_id,CAST(created_at AS CHAR) AS created_at,CAST(updated_at AS CHAR) AS updated_at,body_hmac FROM '.$this->table('native_restart_requests_v1').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY CAST(created_at AS UNSIGNED),new_operation_id LIMIT 22',$args); if(count($rows)>21){throw new OG_Privacy_Case_Refusal('native_restart_damaged');} $open=0; foreach($rows as $r){ if($r['generation']!==$generation || !preg_match('/^[a-f0-9]{64}$/D',$r['new_operation_id']) || !preg_match('/^[a-f0-9]{64}$/D',$r['superseded_operation_id']) || !preg_match('/^(|[a-f0-9]{64})$/D',$r['restart_id']) || !in_array($r['state'],array('draft','pending','applied','rejected','refused'),true) || strlen($r['approval'])>8192 || ($r['state']==='applied')!==($r['approval']!=='') || self::nativeAttemptInteger($r['actor_id'])<1 || self::nativeAttemptInteger($r['created_at'])<1 || self::nativeAttemptInteger($r['updated_at'])<1 || !hash_equals(self::restartMac($r,$secret),$r['body_hmac'])){ throw new OG_Privacy_Case_Refusal('native_restart_damaged'); } if(in_array($r['state'],array('draft','pending'),true)){$open++;} } if($open>1){throw new OG_Privacy_Case_Refusal('native_restart_damaged');} return $rows; } private static function restartOpen(array $rows): ?array { foreach($rows as $r){if(in_array($r['state'],array('draft','pending'),true)){return $r;}} return null; } private function auditRows(array $args,string $secret): array { $rows=$this->rows('SELECT CAST(seq AS CHAR) AS seq,operation_id,event,CAST(actor_id AS CHAR) AS actor_id,CAST(recorded_at AS CHAR) AS recorded_at,detail,prev_hmac,body_hmac FROM '.$this->table('native_operation_audit_v1').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY CAST(seq AS UNSIGNED) LIMIT 1001',$args); if(count($rows)>1000){throw new OG_Privacy_Case_Refusal('native_audit_capacity');} $previous=str_repeat('0',64); foreach($rows as $i=>$r){ // actor_id 0 means "system", never a WordPress user. if((string)($i+1)!==$r['seq'] || !preg_match('/^[a-f0-9]{64}$/D',$r['operation_id']) || !in_array($r['event'],self::restartEvents(),true) || !preg_match('/^(0|[1-9][0-9]{0,18})$/D',$r['actor_id']) || self::nativeAttemptInteger($r['recorded_at'])<1 || !preg_match('/^[a-z0-9_]{0,64}$/D',$r['detail']) || $r['prev_hmac']!==$previous || !hash_equals(self::auditMac($r,$secret),$r['body_hmac'])){ throw new OG_Privacy_Case_Refusal('native_audit_damaged'); } $previous=$r['body_hmac']; } return $rows; } private function auditAppend(array $args,string $secret,string $operation,string $event,int $actor,int $now,string $detail): void { if(!in_array($event,self::restartEvents(),true) || !preg_match('/^[a-f0-9]{64}$/D',$operation) || $actor<0 || !preg_match('/^[a-z0-9_]{0,64}$/D',$detail)){throw new OG_Privacy_Case_Refusal('native_audit_input');} $rows=$this->auditRows($args,$secret); if(count($rows)>=1000){throw new OG_Privacy_Case_Refusal('native_audit_capacity');} $row=array('seq'=>(string)(count($rows)+1),'operation_id'=>$operation,'event'=>$event,'actor_id'=>(string)$actor,'recorded_at'=>(string)$now,'detail'=>$detail,'prev_hmac'=>$rows?end($rows)['body_hmac']:str_repeat('0',64)); $row['body_hmac']=self::auditMac($row,$secret); if($this->sql('INSERT INTO '.$this->table('native_operation_audit_v1').' (install_id,namespace,request_id,seq,operation_id,event,actor_id,recorded_at,detail,prev_hmac,body_hmac) VALUES (?,?,?,?,?,?,?,?,?,?,?)',array_merge($args,array_values($row)))!==1){throw new OG_Privacy_Case_Refusal('native_audit_conflict');} $saved=$this->auditRows($args,$secret); if(end($saved)!==$row){throw new OG_Privacy_Case_Refusal('native_audit_damaged');} } private function restartWrite(array $args,?array $old,array $row,string $secret): array { $row['body_hmac']=self::restartMac($row,$secret); if($old===null){ if($this->sql('INSERT INTO '.$this->table('native_restart_requests_v1').' (install_id,namespace,request_id,new_operation_id,generation,superseded_operation_id,restart_id,state,approval,actor_id,created_at,updated_at,body_hmac) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)',array_merge($args,array_values($row)))!==1){throw new OG_Privacy_Case_Refusal('native_restart_conflict');} }elseif($this->sql('UPDATE '.$this->table('native_restart_requests_v1').' SET restart_id=?,state=?,approval=?,updated_at=?,body_hmac=? WHERE install_id=? AND namespace=? AND request_id=? AND new_operation_id=? AND body_hmac=?',array_merge(array($row['restart_id'],$row['state'],$row['approval'],$row['updated_at'],$row['body_hmac']),$args,array($row['new_operation_id'],$old['body_hmac'])))!==1){ throw new OG_Privacy_Case_Refusal('native_restart_conflict'); } return $row; } private function restartNow(): int { return self::nativeAttemptInteger($this->rows('SELECT CAST(UNIX_TIMESTAMP() AS CHAR) AS now_seconds')[0]['now_seconds']??null); } /** Request-level AND operation-level warning inside the caller's lifecycle * transaction. Reasons are retained; a restart never clears them. */ private function restartAttention(array $context,int $requestId,string $operation,string $reason,int $now): void { if(!in_array($reason,array('native_restart_refused','native_restart_rejected','native_restart_approval_invalid'),true)){throw new OG_Privacy_Case_Refusal('native_attention_input_invalid');} $authority=$this->rows('SELECT install_id,namespace,accepting FROM '.$this->table('native_attention_authority').' WHERE singleton=1'); if(count($authority)!==1||$authority[0]['install_id']!==$context['install']||$authority[0]['namespace']!==$context['suppression_namespace']||(string)$authority[0]['accepting']!=='1'){throw new OG_Privacy_Case_Refusal('attention_binding_changed');} if(!in_array($reason,array_column($this->nativeAttentionRows($context,$requestId),'reason'),true)){ if($this->sql('INSERT INTO '.$this->table('native_erasure_attention').' (install_id,namespace,request_id,reason,recorded_at) VALUES (?,?,?,?,?)',array($context['install'],$context['suppression_namespace'],$requestId,$reason,$now))!==1){throw new OG_Privacy_Case_Refusal('native_attention_conflict');} } $this->operationAttentionInsert($context,$requestId,$operation,$reason,$now); } /** Verified current operation and its signed stop, if any. */ private function restartBasis(array $binding,string $generation,string $secret,array $keys,string $environment,array $history,array $revoked): array { $operation=$this->nativeRemoteRow($binding,$generation,$secret); if($operation===null){return array(null,'unobserved','');} $outcome=$this->nativeOutcomeRow($binding,$generation,$operation,$secret,$keys,$environment,$history,$revoked); $result=self::nativeOutcomeResult($outcome,$operation,$keys,$environment,$history,$revoked); $reason=($result['state']==='review_required' && ($result['verified']['status']??null)==='authenticated_remote_review')?(string)$result['verified']['reason']:''; return array($operation,$result['state'],$reason); } private static function restartReasonEligible(string $state,string $reason): bool { // scope_too_large stays on the scope-proposal path; unknown/404 retries the same operation. return $state==='review_required' && in_array($reason,array('receipt_expired','restart_approval_required'),true); } public function readNativeRestart(array $context,$requestId,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try { $keys=OG_Native_Receipt_Keys::active($keys,$revoked); if(!in_array($environment,array('production','review'),true)){throw new OG_Privacy_Case_Refusal('native_outcome_environment');} $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::nativeRemoteSpecs(),self::nativeOutcomeSpecs(),self::restartSpecs())); $secret=$this->nativeRemoteSecret();$args=array($binding['install'],$binding['namespace'],$requestId); list($operation,$state,$reason)=$this->restartBasis($binding,$context['generation'],$secret,$keys,$environment,$history,$revoked); $rows=$this->restartRows($args,$context['generation'],$secret);$open=self::restartOpen($rows); $audit=$this->auditRows($args,$secret); $archived=$this->rows('SELECT DISTINCT operation_id FROM '.$this->table('native_operation_archive_v1').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY operation_id LIMIT 22',$args); $this->commit(); $eligible=$operation!==null && $open===null && count($rows)<20 && (bool)$keys && self::restartReasonEligible($state,$reason); return array('status'=>'native_restart_state','operation_id'=>$operation['operation_id']??null,'state'=>$state,'review_reason'=>$reason, 'restart_eligible'=>$eligible,'open'=>$open===null?null:array('new_operation_id'=>$open['new_operation_id'],'state'=>$open['state'],'restart_id'=>$open['restart_id']), 'restarts'=>array_map(static fn($r)=>array('new_operation_id'=>$r['new_operation_id'],'superseded_operation_id'=>$r['superseded_operation_id'],'state'=>$r['state'],'updated_at'=>(int)$r['updated_at']),$rows), 'audit'=>array_map(static fn($r)=>array('operation_id'=>$r['operation_id'],'event'=>$r['event'],'actor_id'=>(int)$r['actor_id'],'recorded_at'=>(int)$r['recorded_at'],'detail'=>$r['detail']),$audit), 'superseded_operations'=>array_column($archived,'operation_id'),'erasure_complete'=>false,'restart_authorized'=>false); }catch(Throwable $e){return array_merge($this->failure($e,'native_restart_read'),array('erasure_complete'=>false,'restart_authorized'=>false));}finally{$this->finish();} } /** Admin intent: durable draft with a NEW random operation ID, persisted * before any network so an interrupted send repeats the same request. */ public function requestNativeRestart(array $context,$requestId,string $operationId,$actor,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try { if(!is_int($actor)||$actor<1||!preg_match('/^[a-f0-9]{64}$/D',$operationId)||!in_array($environment,array('production','review'),true)){throw new OG_Privacy_Case_Refusal('native_restart_input');} $keys=OG_Native_Receipt_Keys::active($keys,$revoked); $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::nativeRemoteSpecs(),self::nativeOutcomeSpecs(),self::restartSpecs())); $secret=$this->nativeRemoteSecret();$args=array($binding['install'],$binding['namespace'],$requestId); list($operation,$state,$reason)=$this->restartBasis($binding,$context['generation'],$secret,$keys,$environment,$history,$revoked); if($operation===null || $operation['operation_id']!==$operationId){throw new OG_Privacy_Case_Refusal('native_restart_operation');} $rows=$this->restartRows($args,$context['generation'],$secret);$open=self::restartOpen($rows); if($open!==null){ if($open['superseded_operation_id']!==$operationId){throw new OG_Privacy_Case_Refusal('native_restart_damaged');} $this->commit(); return array('status'=>'native_restart_draft','replayed'=>true,'new_operation_id'=>$open['new_operation_id'],'payload'=>OG_Native_Restart_Approval::requestPayload(json_decode($operation['payload'],true,32,JSON_THROW_ON_ERROR),$open['new_operation_id']),'erasure_complete'=>false,'restart_authorized'=>false); } if(!self::restartReasonEligible($state,$reason)){throw new OG_Privacy_Case_Refusal('native_restart_not_eligible');} if(!$keys){throw new OG_Privacy_Case_Refusal('registry_no_active_keys');} if(count($rows)>=20){throw new OG_Privacy_Case_Refusal('native_restart_capacity');} $now=$this->restartNow();$new=bin2hex(random_bytes(32)); $row=array('new_operation_id'=>$new,'generation'=>$context['generation'],'superseded_operation_id'=>$operationId,'restart_id'=>'','state'=>'draft','approval'=>'','actor_id'=>(string)$actor,'created_at'=>(string)$now,'updated_at'=>(string)$now); $this->restartWrite($args,null,$row,$secret); $this->auditAppend($args,$secret,$operationId,'restart_requested',$actor,$now,$reason); $saved=self::restartOpen($this->restartRows($args,$context['generation'],$secret)); if($saved===null || $saved['new_operation_id']!==$new){throw new OG_Privacy_Case_Refusal('native_restart_damaged');} $this->commit(); return array('status'=>'native_restart_draft','replayed'=>false,'new_operation_id'=>$new,'payload'=>OG_Native_Restart_Approval::requestPayload(json_decode($operation['payload'],true,32,JSON_THROW_ON_ERROR),$new),'erasure_complete'=>false,'restart_authorized'=>false); }catch(Throwable $e){return array_merge($this->failure($e,'native_restart_request'),array('erasure_complete'=>false,'restart_authorized'=>false));}finally{$this->finish();} } /** Payload of the open draft/pending restart for the CURRENT operation. */ public function nativeRestartPayload(array $context,$requestId,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try { $keys=OG_Native_Receipt_Keys::active($keys,$revoked); $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::nativeRemoteSpecs(),self::restartSpecs())); $secret=$this->nativeRemoteSecret();$args=array($binding['install'],$binding['namespace'],$requestId); $operation=$this->nativeRemoteRow($binding,$context['generation'],$secret); $open=self::restartOpen($this->restartRows($args,$context['generation'],$secret)); $this->commit(); if($open===null || $operation===null){return array('status'=>'native_restart_none','erasure_complete'=>false);} if($open['superseded_operation_id']!==$operation['operation_id']){return array('status'=>'unavailable','reason'=>'native_restart_damaged','erasure_complete'=>false);} return array('status'=>'native_restart_open','state'=>$open['state'],'new_operation_id'=>$open['new_operation_id'],'payload'=>OG_Native_Restart_Approval::requestPayload(json_decode($operation['payload'],true,32,JSON_THROW_ON_ERROR),$open['new_operation_id']),'erasure_complete'=>false,'restart_authorized'=>false); }catch(Throwable $e){return array_merge($this->failure($e,'native_restart_payload'),array('erasure_complete'=>false));}finally{$this->finish();} } /** Bounded discovery of open restarts for the scheduled poller. */ public function nativeRestartCandidates(array $context,int $after=0): array { try { if($after<0){throw new OG_Privacy_Case_Refusal('native_restart_cursor');} $this->historicalEnter($context,true,self::restartSpecs()); $rows=$this->rows('SELECT DISTINCT CAST(request_id AS CHAR) AS request_id FROM '.$this->table('native_restart_requests_v1').' WHERE install_id=? AND namespace=? AND request_id>? AND state IN (?,?) ORDER BY CAST(request_id AS UNSIGNED) LIMIT 5',array($context['install'],$context['suppression_namespace'],$after,'draft','pending')); $ids=array();foreach($rows as $row){$id=self::nativeAttemptInteger($row['request_id']);if($id<=$after){throw new OG_Privacy_Case_Refusal('native_restart_cursor');}$ids[]=$id;$after=$id;} $this->commit();return array('status'=>'native_restart_candidates','request_ids'=>$ids,'next_cursor'=>$ids?$after:0,'erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_restart_inventory');}finally{$this->finish();} } /** Apply the server's reply for the open restart. Unsigned states can only * move the local queue; ONLY a verified owner approval bound to this exact * operation, template and cutoff supersedes it, atomically, with archive. */ public function settleNativeRestart(array $context,$requestId,string $newOperationId,$body,$httpStatus,array $keys,string $environment='production',array $history=array(),array $revoked=array()): array { try { if(!preg_match('/^[a-f0-9]{64}$/D',$newOperationId) || ($body!==null && !is_string($body)) || ($httpStatus!==null && (!is_int($httpStatus)||$httpStatus<0||$httpStatus>599)) || !in_array($environment,array('production','review'),true)){throw new OG_Privacy_Case_Refusal('native_restart_input');} $keys=OG_Native_Receipt_Keys::active($keys,$revoked); $binding=$this->nativeScopeBinding($context,$requestId,array_merge(self::nativeRemoteSpecs(),self::nativeOutcomeSpecs(),self::scheduledSpecs(),self::resumeSpecs(),self::clockSpecs(),self::nativeAttemptSpecs(),self::nativeAckSpecs(),self::localSpecs(),self::localFlagSpecs(),self::flagProjectionSpecs(),self::takeoverProgressSpecs(),self::nativeAttentionSpecs(),self::attentionAuthoritySpecs(),self::restartSpecs())); $secret=$this->nativeRemoteSecret();$generation=$context['generation'];$args=array($binding['install'],$binding['namespace'],$requestId); list($operation,$state,$reason)=$this->restartBasis($binding,$generation,$secret,$keys,$environment,$history,$revoked); $open=self::restartOpen($this->restartRows($args,$generation,$secret)); // A reply for a request that is no longer open (or for an operation // that is no longer current) is stale and changes nothing. if($operation===null || $open===null || $open['new_operation_id']!==$newOperationId || $open['superseded_operation_id']!==$operation['operation_id']){throw new OG_Privacy_Case_Refusal('native_restart_stale');} $now=$this->restartNow();$current=$operation['operation_id'];$row=$open;$row['updated_at']=(string)$now; if($body===null){ if(!in_array($httpStatus,array(404,409),true)){$this->commit();return array('status'=>'native_restart_unconfirmed','http_status'=>$httpStatus,'erasure_complete'=>false,'restart_authorized'=>false);} // The server refused this exact request. Terminal for this new // operation ID; a later admin request may ask again. $row['state']='refused';$this->restartWrite($args,$open,$row,$secret); $this->auditAppend($args,$secret,$current,'restart_refused',0,$now,'http_'.$httpStatus); $this->restartAttention($context,$requestId,$current,'native_restart_refused',$now); $this->commit();return array('status'=>'native_restart_refused','http_status'=>$httpStatus,'attention_recorded'=>true,'erasure_complete'=>false,'restart_authorized'=>false); } $status=OG_Native_Restart_Approval::status($body,$newOperationId); if($open['restart_id']!=='' && !hash_equals($open['restart_id'],$status['restart_id'])){throw new OG_Privacy_Case_Refusal('native_restart_conflict');} $row['restart_id']=$status['restart_id']; if($status['status']==='requested'){ if($open['state']==='draft'){ $row['state']='pending';$this->restartWrite($args,$open,$row,$secret); $this->auditAppend($args,$secret,$current,'restart_submitted',0,$now,$status['restart_id']); } $this->commit();return array('status'=>'native_restart_pending','restart_id'=>$status['restart_id'],'erasure_complete'=>false,'restart_authorized'=>false); } if($status['status']==='rejected'){ $row['state']='rejected';$this->restartWrite($args,$open,$row,$secret); $this->auditAppend($args,$secret,$current,'restart_rejected',0,$now,$status['restart_id']); $this->restartAttention($context,$requestId,$current,'native_restart_rejected',$now); $this->commit();return array('status'=>'native_restart_rejected','attention_recorded'=>true,'erasure_complete'=>false,'restart_authorized'=>false); } $request=json_decode($operation['payload'],true,32,JSON_THROW_ON_ERROR); try { $approval=OG_Native_Restart_Approval::verify($status['approval'],$keys,$revoked,$environment); if($approval['restart_id']!==$status['restart_id'] || $approval['install']!==$binding['install'] || $approval['namespace']!==$binding['namespace'] || $approval['request_id']!==(string)$requestId || $approval['presented_operation_id']!==$current || $approval['new_operation_id']!==$newOperationId || $approval['template_digest']!==OG_Native_Restart_Approval::templateDigest($request) || $approval['cutoff_seconds']!==(string)$binding['cutoff_seconds'] || $request['cutoff_seconds']!==(string)$binding['cutoff_seconds']){throw new InvalidArgumentException('approval_binding');} }catch(Throwable $e){ // Never supersede on an unverifiable approval; keep the request open. // The hourly poll repeats the same reply (e.g. an approval signed by a // key that is now history-only, until the owner renews it): record each // distinct reply once so polling cannot exhaust the bounded audit trail. $detail=substr(hash('sha256',$status['approval']),0,32); $last=$this->auditRows($args,$secret);$last=$last?end($last):null; if($last===null || $last['event']!=='restart_approval_invalid' || $last['operation_id']!==$current || $last['detail']!==$detail){ $this->auditAppend($args,$secret,$current,'restart_approval_invalid',0,$now,$detail); } $this->restartAttention($context,$requestId,$current,'native_restart_approval_invalid',$now); $this->commit();return array('status'=>'native_restart_approval_invalid','attention_recorded'=>true,'erasure_complete'=>false,'restart_authorized'=>false); } // The signed stop must still be in place: a late committed receipt // for the old operation wins and makes the approval unnecessary. if(!self::restartReasonEligible($state,$reason)){throw new OG_Privacy_Case_Refusal('native_restart_not_eligible');} $this->restartSupersede($args,$binding,$generation,$secret,$operation,$request,$newOperationId,$keys,$environment,$history,$revoked,$now); $row['state']='applied';$row['approval']=$status['approval'];$this->restartWrite($args,$open,$row,$secret); $this->auditAppend($args,$secret,$current,'restart_approval_verified',0,$now,$status['restart_id']); $this->auditAppend($args,$secret,$current,'operation_superseded',0,$now,$newOperationId); $this->auditAppend($args,$secret,$newOperationId,'operation_issued',0,$now,$current); $saved=$this->nativeRemoteRow($binding,$generation,$secret); if($saved===null || $saved['operation_id']!==$newOperationId){throw new OG_Privacy_Case_Refusal('native_restart_damaged');} $this->commit(); return array('status'=>'native_restart_applied','superseded_operation_id'=>$current,'operation_id'=>$newOperationId,'cutoff_seconds'=>(int)$binding['cutoff_seconds'],'erasure_complete'=>false,'restart_authorized'=>true); }catch(Throwable $e){return array_merge($this->failure($e,'native_restart_settle'),array('erasure_complete'=>false,'restart_authorized'=>false));}finally{$this->finish();} } /** Under the caller's lifecycle lock: archive every row of the old operation, * remove it from the active per-request tables, and issue the new operation * with the identical payload except its ID. Core-proof journal rows are * operation-keyed and stay where they are. */ private function restartSupersede(array $args,array $binding,string $generation,string $secret,array $operation,array $request,string $new,array $keys,string $environment,array $history,array $revoked,int $now): void { $old=$operation['operation_id']; if($this->rows('SELECT request_id FROM '.$this->table('native_send_attempts_v4').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 1',$args)){throw new OG_Privacy_Case_Refusal('native_old_attempt_reconciliation');} if($this->rows('SELECT request_id FROM '.$this->table('native_receipt_acks_v1').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 1',$args)){throw new OG_Privacy_Case_Refusal('native_restart_ack_present');} if($this->rows('SELECT request_id FROM '.$this->table('native_local_cleanup_v1').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 1',$args)){throw new OG_Privacy_Case_Refusal('native_restart_cleanup_present');} $attempts=$this->scheduledRows($args,$generation,$old,$secret); // An unsettled dispatch with a live lease may still be on the wire: wait. // A settled attempt (or an expired lease) cannot block; any late reply // for the old operation fails verification against the new one. if($attempts && in_array(end($attempts)['stage'],array('claimed','dispatch_unknown'),true) && (int)end($attempts)['lease_until']>$now){throw new OG_Privacy_Case_Refusal('native_attempt_busy');} $grants=$this->resumeRows($args,$generation,$old,$secret,$attempts); $clocks=$this->rows('SELECT CAST(attempt AS CHAR) AS attempt,generation,operation_id,token,CAST(lease_until AS CHAR) AS lease_until,CAST(observed_at AS CHAR) AS observed_at,body_hmac FROM '.$this->table('native_clock_observations_v1').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY CAST(attempt AS UNSIGNED) LIMIT 607',$args); if(count($clocks)>606){throw new OG_Privacy_Case_Refusal('native_clock_damaged');} foreach($clocks as $clock){ $mac=$clock;unset($mac['attempt']); if($clock['generation']!==$generation || $clock['operation_id']!==$old || !hash_equals(self::clockMac($mac,$secret),$clock['body_hmac'])){throw new OG_Privacy_Case_Refusal('native_clock_damaged');} } $raw=$this->rows('SELECT generation,operation_id,payload,digest,body_hmac FROM '.$this->table('native_remote_operations_v4').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',$args); $outcome=$this->nativeOutcomeRow($binding,$generation,$operation,$secret,$keys,$environment,$history,$revoked); if(count($raw)!==1 || $raw[0]['operation_id']!==$old || $outcome===null){throw new OG_Privacy_Case_Refusal('native_restart_damaged');} $archive=array(array('remote_operation',array($raw[0])),array('outcome',array($outcome)),array('scheduled_attempt',$attempts),array('retry_resume',$grants),array('clock_observation',$clocks)); foreach($archive as list($kind,$items)){ foreach(array_values($items) as $i=>$item){ $payload=json_encode($item,JSON_THROW_ON_ERROR);$seq=$i+1; if($this->sql('INSERT INTO '.$this->table('native_operation_archive_v1').' (install_id,namespace,request_id,operation_id,kind,seq,payload,body_hmac) VALUES (?,?,?,?,?,?,?,?)',array_merge($args,array($old,$kind,$seq,$payload,self::archiveMac($old,$kind,$seq,$payload,$secret))))!==1){throw new OG_Privacy_Case_Refusal('native_archive_conflict');} } } foreach(array('native_remote_outcomes_v4'=>1,'native_scheduled_attempts_v1'=>count($attempts),'native_retry_resumes_v1'=>count($grants),'native_clock_observations_v1'=>count($clocks)) as $table=>$expected){ if($this->sql('DELETE FROM '.$this->table($table).' WHERE install_id=? AND namespace=? AND request_id=?',$args)!==$expected){throw new OG_Privacy_Case_Refusal('native_restart_conflict');} } // The pre-dispatch flag manifest is bound to the operation. Rebind the // SAME captured content (never a fresh capture) so the original scope // carries over unchanged; keep the old bytes in the archive. $header=array('version'=>5,'install'=>$binding['install'],'namespace'=>$binding['namespace'],'request_id'=>(string)$binding['request_id'],'generation'=>$generation,'operation_id'=>$old,'evidence_digest'=>$request['evidence_digest']); $manifest=$this->localFlagManifest($binding,$header,$secret); if($manifest!==null){ $rawManifest=$this->rows('SELECT payload,body_hmac FROM '.$this->table('native_local_flag_manifests_v1').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',$args); if(count($rawManifest)!==1){throw new OG_Privacy_Case_Refusal('native_flag_manifest_damaged');} $item=json_encode($rawManifest[0],JSON_THROW_ON_ERROR); if($this->sql('INSERT INTO '.$this->table('native_operation_archive_v1').' (install_id,namespace,request_id,operation_id,kind,seq,payload,body_hmac) VALUES (?,?,?,?,?,?,?,?)',array_merge($args,array($old,'flag_manifest',1,$item,self::archiveMac($old,'flag_manifest',1,$item,$secret))))!==1){throw new OG_Privacy_Case_Refusal('native_archive_conflict');} $manifest['operation_id']=$new;$rebound=json_encode($manifest,JSON_THROW_ON_ERROR); if($this->sql('UPDATE '.$this->table('native_local_flag_manifests_v1').' SET payload=?,body_hmac=? WHERE install_id=? AND namespace=? AND request_id=? AND body_hmac=?',array_merge(array($rebound,self::localFlagMac('manifest',$rebound,$secret)),$args,array($rawManifest[0]['body_hmac'])))!==1){throw new OG_Privacy_Case_Refusal('native_restart_conflict');} if($this->localFlagManifest($binding,array_merge($header,array('operation_id'=>$new)),$secret)!==$manifest){throw new OG_Privacy_Case_Refusal('native_flag_manifest_damaged');} } // The block-projection snapshot is captured with the manifest, before // dispatch; an APPLIED projection or takeover progress means local // cleanup already ran, which only a committed receipt may authorise. if($this->rows('SELECT request_id FROM '.$this->table('native_takeover_progress_v1').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 1',$args)){throw new OG_Privacy_Case_Refusal('native_restart_cleanup_present');} $projection=$this->flagProjectionRow($binding,$header,$secret); if($projection!==null){ if($projection['state']!=='prepared'){throw new OG_Privacy_Case_Refusal('native_restart_cleanup_present');} $rawProjection=$this->rows('SELECT payload,body_hmac FROM '.$this->table('native_flag_projection_v1').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',$args); if(count($rawProjection)!==1){throw new OG_Privacy_Case_Refusal('native_projection_damaged');} $item=json_encode($rawProjection[0],JSON_THROW_ON_ERROR); if($this->sql('INSERT INTO '.$this->table('native_operation_archive_v1').' (install_id,namespace,request_id,operation_id,kind,seq,payload,body_hmac) VALUES (?,?,?,?,?,?,?,?)',array_merge($args,array($old,'flag_projection',1,$item,self::archiveMac($old,'flag_projection',1,$item,$secret))))!==1){throw new OG_Privacy_Case_Refusal('native_archive_conflict');} $projection['header']['operation_id']=$new;$rebound=json_encode($projection,JSON_THROW_ON_ERROR); if($this->sql('UPDATE '.$this->table('native_flag_projection_v1').' SET payload=?,body_hmac=? WHERE install_id=? AND namespace=? AND request_id=? AND body_hmac=?',array_merge(array($rebound,self::localFlagMac('projection',$rebound,$secret)),$args,array($rawProjection[0]['body_hmac'])))!==1){throw new OG_Privacy_Case_Refusal('native_restart_conflict');} if($this->flagProjectionRow($binding,array_merge($header,array('operation_id'=>$new)),$secret)!==$projection){throw new OG_Privacy_Case_Refusal('native_projection_damaged');} } $request['operation_id']=$new; $payload=OG_Native_Remote_Binding::encode($request);$digest=OG_Native_Remote_Binding::digest($request); if($this->sql('UPDATE '.$this->table('native_remote_operations_v4').' SET operation_id=?,payload=?,digest=?,body_hmac=? WHERE install_id=? AND namespace=? AND request_id=? AND operation_id=? AND body_hmac=?', array_merge(array($new,$payload,$digest,self::nativeRemoteMac($generation,$payload,$secret)),$args,array($old,$raw[0]['body_hmac'])))!==1){throw new OG_Privacy_Case_Refusal('native_restart_conflict');} } }