array( array('singleton'=>'tinyint unsigned','install_id'=>'char(32)','generation'=>'char(32)', 'mode'=>'varchar(8)','state'=>'varchar(8)','body_hmac'=>'char(64)'), array('PRIMARY'=>array(0,array('singleton'))))); } private function nativeLifecycleAllSpecs(): array { return array_merge($this->historySpecs(),self::subjectSpecs(),self::historicalSpecs(),self::nativeFenceSpecs(), self::nativeAttentionSpecs(),self::attentionAuthoritySpecs(),self::nativeScopeSpecs(),self::nativePageSpecs(), self::nativeOrderFenceSpecs(),self::nativeRemoteSpecs(),self::nativeOutcomeSpecs(),self::nativeAttemptSpecs(), self::nativeAckSpecs(),self::nativeCoreSpecs(),self::scheduledSpecs(),self::resumeSpecs(),self::clockSpecs(),self::localSpecs(),self::localFlagSpecs(), self::flagProjectionSpecs(),self::blockTouchSpecs(),self::takeoverProgressSpecs(),self::legalSpecs(), self::legalReleaseSpecs(),self::restartSpecs(),self::nativeLifecycleSpecs()); } public static function nativeLifecycleTableSuffixes(): array { return array( 'control','operations','receipts','flags','block_revision','block_backup', 'privacy_cases','privacy_case_links','privacy_case_keys','privacy_namespaces', 'transport_operations','transport_subjects','transport_generations','transport_disposals', 'subject_authority_state','subject_authority','native_lifecycle_v1','historical_fences', 'native_erasure_bindings','native_erasure_attention','native_attention_authority', 'native_scope_candidates','native_scope_pages','native_order_fences', 'native_remote_operations_v4','native_remote_outcomes_v4','native_send_attempts_v4', 'native_receipt_acks_v1', 'native_core_journal_v1', 'native_scheduled_attempts_v1','native_retry_resumes_v1','native_clock_observations_v1', 'native_local_cleanup_v1','native_local_flag_manifests_v1','native_flag_projection_v1', 'native_block_touches_v1','native_takeover_progress_v1','native_legal_epoch_v1', 'native_legal_results_v1','native_legal_holds_v1','native_legal_releases_v1', 'native_restart_requests_v1','native_operation_archive_v1','native_operation_audit_v1','native_operation_attention_v1', ); } private static function nativeLifecycleMac(string $install,string $generation,string $mode,string $state,string $secret): string { return hash_hmac('sha256',"og-native-lifecycle-v1\n".$install."\n".$generation."\n".$mode."\n".$state,hex2bin($secret)); } private function nativeLifecycleRow(array $context,bool $exclusive=false): array { $control=$this->enter($context['install'],$context['generation'],$exclusive); // The control singleton is the lifecycle mutex; a second locking read // would create another SQL template without adding authority. $rows=$this->rows('SELECT install_id,generation,mode,state,body_hmac FROM '.$this->table('native_lifecycle_v1').' WHERE singleton=1'); if(count($rows)!==1 || $rows[0]['install_id']!==$context['install'] || $rows[0]['generation']!==$context['generation'] || !in_array($rows[0]['mode'],array('fresh','upgrade'),true) || !in_array($rows[0]['state'],array('prepared','active'),true) || !hash_equals(self::nativeLifecycleMac($rows[0]['install_id'],$rows[0]['generation'],$rows[0]['mode'],$rows[0]['state'],$control['secret']),$rows[0]['body_hmac'])) { throw new OG_Privacy_Case_Refusal('native_lifecycle_damaged'); } return array($rows[0],$control); } /** Publish PREPARED before any add-on table is created. Safe to retry. */ public function prepareNativeLifecycle(array $context,string $mode): array { try { foreach(array('install','generation','suppression_namespace') as $key){self::token($context[$key]??'');} if(!in_array($mode,array('fresh','upgrade'),true)){throw new OG_Privacy_Case_Refusal('native_lifecycle_mode');} $this->connect(1);$this->validateSchema();$this->validateBlocks();$this->validateCases(array_merge($this->historySpecs(),self::subjectSpecs())); $existing=$this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?',array($this->profile['database'],$this->prefix.'native_lifecycle_v1')); if(!$existing){ $this->sql('CREATE TABLE '.$this->table('native_lifecycle_v1').' (singleton TINYINT UNSIGNED NOT NULL PRIMARY KEY,install_id CHAR(32) NOT NULL,generation CHAR(32) NOT NULL,mode VARCHAR(8) NOT NULL,state VARCHAR(8) NOT NULL,body_hmac CHAR(64) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateTableSpecs(self::nativeLifecycleSpecs()); $control=$this->enter($context['install'],$context['generation'],true); $mac=self::nativeLifecycleMac($context['install'],$context['generation'],$mode,'prepared',$control['secret']); if($this->sql('INSERT INTO '.$this->table('native_lifecycle_v1').' (singleton,install_id,generation,mode,state,body_hmac) VALUES (1,?,?,?,?,?)',array($context['install'],$context['generation'],$mode,'prepared',$mac))!==1){throw new OG_Privacy_Case_Refusal('native_lifecycle_unconfirmed');} $this->commit();return array('status'=>'native_storage_prepared','mode'=>$mode,'state'=>'prepared','dispatch_enabled'=>false); } $this->validateTableSpecs(self::nativeLifecycleSpecs()); list($row,$control)=$this->nativeLifecycleRow($context,true); // ACTIVE is immutable. A later installer/upgrade probe reports the // published state instead of trying to relabel its origin mode. if($row['state']==='active'){ $this->commit();return array('status'=>'native_storage_prepared','mode'=>$row['mode'],'state'=>'active','dispatch_enabled'=>false); } // A fresh attempt which discovers legacy data at its final gate is // deliberately recoverable only through the explicit upgrade // route. That route may reclassify PREPARED; ACTIVE is immutable. if($row['mode']!==$mode){ if($row['mode']==='fresh' && $row['state']==='prepared' && $mode==='upgrade'){ $mac=self::nativeLifecycleMac($context['install'],$context['generation'],'upgrade','prepared',$control['secret']); if($this->sql('UPDATE '.$this->table('native_lifecycle_v1').' SET mode=?,body_hmac=? WHERE singleton=1 AND mode=? AND state=? AND body_hmac=?',array('upgrade',$mac,'fresh','prepared',$row['body_hmac']))!==1){throw new OG_Privacy_Case_Refusal('native_lifecycle_conflict');} $row['mode']='upgrade';$row['body_hmac']=$mac; }else{throw new OG_Privacy_Case_Refusal('native_lifecycle_mode_changed');} } $this->commit();return array('status'=>'native_storage_prepared','mode'=>$mode,'state'=>$row['state'],'dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'native_lifecycle_prepare');}finally{$this->finish();} } /** The future composition root is the only intended caller. */ public function activateNativeLifecycle(array $context,$legacyWritersDrained=false): array { try { if($legacyWritersDrained!==true){throw new OG_Privacy_Case_Refusal('legacy_writers_not_drained');} foreach(array('install','generation','suppression_namespace') as $key){self::token($context[$key]??'');} $this->connect(1);$this->validateSchema();$this->validateBlocks();$this->validateCases(); $this->validateTableSpecs($this->nativeLifecycleAllSpecs()); list($row,$control)=$this->nativeLifecycleRow($context,true); if($row['state']==='active'){$this->commit();return array('status'=>'native_storage_active','dispatch_enabled'=>false);} $mac=self::nativeLifecycleMac($context['install'],$context['generation'],$row['mode'],'active',$control['secret']); if($this->sql('UPDATE '.$this->table('native_lifecycle_v1').' SET state=?,body_hmac=? WHERE singleton=1 AND state=? AND body_hmac=?',array('active',$mac,'prepared',$row['body_hmac']))!==1){throw new OG_Privacy_Case_Refusal('native_lifecycle_conflict');} $this->commit();return array('status'=>'native_storage_active','dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'native_lifecycle_activate');}finally{$this->finish();} } /** Fresh-store activation proves the legacy footprint is still empty while * holding the lifecycle mutex. A resumed installer may never substitute a * stale earlier observation or a hard-coded drain assertion. */ public function activateFreshNativeLifecycle(array $context,string $wordpressPrefix,?string $wordpressBasePrefix=null): array { try { foreach(array('install','generation','suppression_namespace') as $key){self::token($context[$key]??'');} $this->connect(1);$this->validateSchema();$this->validateBlocks();$this->validateCases(); $this->validateTableSpecs($this->nativeLifecycleAllSpecs()); list($row,$control)=$this->nativeLifecycleRow($context,true); if($row['mode']!=='fresh'){throw new OG_Privacy_Case_Refusal('native_lifecycle_mode_changed');} $this->refuseLegacyWordPress($wordpressPrefix,$wordpressBasePrefix); if($row['state']==='active'){$this->commit();return array('status'=>'native_storage_active','dispatch_enabled'=>false);} $mac=self::nativeLifecycleMac($context['install'],$context['generation'],'fresh','active',$control['secret']); if($this->sql('UPDATE '.$this->table('native_lifecycle_v1').' SET state=?,body_hmac=? WHERE singleton=1 AND mode=? AND state=? AND body_hmac=?',array('active',$mac,'fresh','prepared',$row['body_hmac']))!==1){throw new OG_Privacy_Case_Refusal('native_lifecycle_conflict');} $this->commit();return array('status'=>'native_storage_active','dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'native_lifecycle_fresh_activate');}finally{$this->finish();} } }