[ 'id' => $rotation_id, 'stage' => 'new', 'attempts' => 0 ] ] ); AICOM_Base_Connection::audit( 'base.rotation_started', 'success', [ 'rotation_id' => $rotation_id ] ); return self::step(); } /** Continue an unfinished rotation (called at the top of every heartbeat). */ public static function resume(): void { if ( AICOM_Base_State::get( 'rotation' ) ) { self::step(); } } private static function step(): bool { $r = (array) AICOM_Base_State::get( 'rotation', [] ); if ( empty( $r['id'] ) ) { return false; } if ( (int) $r['attempts'] >= self::MAX_ATTEMPTS ) { AICOM_Base_Identity::abort_rotation(); AICOM_Base_State::update( [ 'rotation' => null ] ); AICOM_Base_Connection::audit( 'base.rotation_failed', 'error', [ 'rotation_id' => $r['id'], 'reason' => 'max_attempts' ] ); return false; } $r['attempts'] = (int) $r['attempts'] + 1; AICOM_Base_State::update( [ 'rotation' => $r ] ); $pending = AICOM_Base_Identity::begin_rotation( (string) $r['id'] ); if ( $r['stage'] === 'new' ) { $res = AICOM_Base_Client::site_request( 'POST', '/api/v1/site/rotate/propose', [ 'rotation_id' => $r['id'], 'new_public_key' => $pending['public_key'], ], [ 'key' => 'active' ] ); if ( ! $res['ok'] ) { return self::failed( $r, $res, 'propose' ); } $r['stage'] = 'proposed'; AICOM_Base_State::update( [ 'rotation' => $r ] ); } $res = AICOM_Base_Client::site_request( 'POST', '/api/v1/site/rotate/confirm', [ 'rotation_id' => $r['id'] ], [ 'key' => 'pending' ] ); if ( ! $res['ok'] ) { return self::failed( $r, $res, 'confirm' ); } AICOM_Base_Identity::commit_rotation(); AICOM_Base_State::update( [ 'rotation' => null, 'rotated_at' => time() ] ); AICOM_Base_Connection::audit( 'base.rotation_completed', 'success', [ 'rotation_id' => $r['id'], 'key_id' => AICOM_Base_Identity::public_info()['key_id'] ?? '' ] ); return true; } private static function failed( array $r, array $res, string $stage ): bool { // A definitive 4xx (other than rate limit / auth-time issues) means AICOMBase will not accept this rotation. if ( $res['status'] >= 400 && $res['status'] < 500 && ! in_array( $res['status'], [ 401, 429 ], true ) ) { AICOM_Base_Identity::abort_rotation(); AICOM_Base_State::update( [ 'rotation' => null ] ); } AICOM_Base_Connection::audit( 'base.rotation_failed', 'error', [ 'rotation_id' => $r['id'], 'stage' => $stage, 'error' => $res['error'] ] ); return false; } }