diff --git a/bavarian-rank-engine/includes/Helpers/BulkQueue.php b/bavarian-rank-engine/includes/Helpers/BulkQueue.php new file mode 100644 index 0000000..11f9fcb --- /dev/null +++ b/bavarian-rank-engine/includes/Helpers/BulkQueue.php @@ -0,0 +1,28 @@ +post_content ?? '' ); + $text = preg_replace( '/\s+/', ' ', $text ); + $text = trim( $text ); + + if ( $text === '' ) { + return ''; + } + + if ( mb_strlen( $text ) <= self::MAX ) { + return $text; + } + + // Try to end on a sentence boundary within MAX chars + $candidate = mb_substr( $text, 0, self::MAX ); + $last_period = mb_strrpos( $candidate, '. ' ); + $last_exclaim = mb_strrpos( $candidate, '! ' ); + $last_question = mb_strrpos( $candidate, '? ' ); + + $last_sentence = max( + $last_period === false ? -1 : $last_period, + $last_exclaim === false ? -1 : $last_exclaim, + $last_question === false ? -1 : $last_question + ); + + if ( $last_sentence >= 0 && $last_sentence >= self::MIN - 1 ) { + return mb_substr( $text, 0, $last_sentence + 1 ); + } + + // Fall back to last word boundary within MAX + $last_space = mb_strrpos( $candidate, ' ' ); + if ( $last_space !== false && $last_space >= self::MIN - 20 ) { + return mb_substr( $text, 0, $last_space ) . '…'; + } + + // Last resort: hard cut with ellipsis + return mb_substr( $text, 0, self::MAX - 1 ) . '…'; + } +} diff --git a/bavarian-rank-engine/includes/Helpers/KeyVault.php b/bavarian-rank-engine/includes/Helpers/KeyVault.php new file mode 100644 index 0000000..8165a57 --- /dev/null +++ b/bavarian-rank-engine/includes/Helpers/KeyVault.php @@ -0,0 +1,79 @@ +". + * + * Note: XOR with a static salt is obfuscation, not encryption. It prevents + * plain-text keys from appearing in database backups or export files, but + * does not protect against an attacker with access to both the database + * AND the wp-config.php salts. For stronger protection, users can define + * BRE__KEY constants in wp-config.php and leave the DB field empty. + */ +class KeyVault { + private const PREFIX = 'bre1:'; + + /** + * Obfuscate a plain API key for database storage. + */ + public static function encrypt( string $key ): string { + if ( $key === '' ) { + return ''; + } + return self::PREFIX . base64_encode( self::xor( $key, self::salt() ) ); + } + + /** + * Recover the plain API key from a stored obfuscated value. + * Returns empty string if the stored value is not in bre1: format (legacy/invalid). + */ + public static function decrypt( string $stored ): string { + if ( $stored === '' ) { + return ''; + } + if ( ! str_starts_with( $stored, self::PREFIX ) ) { + // Legacy OpenSSL-encrypted value or unknown format — return empty so user re-enters. + return ''; + } + $raw = base64_decode( substr( $stored, strlen( self::PREFIX ) ), true ); + if ( $raw === false ) { + return ''; + } + return self::xor( $raw, self::salt() ); + } + + /** + * Returns masked version for display: ••••••Ab3c9 + */ + public static function mask( string $plain ): string { + if ( $plain === '' ) { + return ''; + } + return str_repeat( '•', max( 0, mb_strlen( $plain ) - 5 ) ) . mb_substr( $plain, -5 ); + } + + /** + * XOR each byte of $data with the corresponding byte of $key (wrapping). + */ + private static function xor( string $data, string $key ): string { + $out = ''; + $keyLen = strlen( $key ); + for ( $i = 0, $n = strlen( $data ); $i < $n; $i++ ) { + $out .= $data[ $i ] ^ $key[ $i % $keyLen ]; + } + return $out; + } + + /** + * Derives a 64-character hex salt from WP's AUTH_KEY and SECURE_AUTH_KEY. + * Falls back to known strings if the constants are not defined (local dev / unit tests). + */ + private static function salt(): string { + $a = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'bre-fallback-a'; + $b = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : 'bre-fallback-b'; + return hash( 'sha256', $a . $b ); // 64 hex chars, no extension needed + } +} diff --git a/bavarian-rank-engine/includes/Helpers/TokenEstimator.php b/bavarian-rank-engine/includes/Helpers/TokenEstimator.php new file mode 100644 index 0000000..f1590a6 --- /dev/null +++ b/bavarian-rank-engine/includes/Helpers/TokenEstimator.php @@ -0,0 +1,103 @@ + array( + 'gpt-4.1' => array( + 'input' => 0.002, + 'output' => 0.008, + ), + 'gpt-4o' => array( + 'input' => 0.0025, + 'output' => 0.01, + ), + 'gpt-4o-mini' => array( + 'input' => 0.00015, + 'output' => 0.0006, + ), + 'gpt-3.5-turbo' => array( + 'input' => 0.0005, + 'output' => 0.0015, + ), + ), + 'anthropic' => array( + 'claude-sonnet-4-6' => array( + 'input' => 0.003, + 'output' => 0.015, + ), + 'claude-opus-4-6' => array( + 'input' => 0.015, + 'output' => 0.075, + ), + 'claude-haiku-4-5-20251001' => array( + 'input' => 0.00025, + 'output' => 0.00125, + ), + ), + 'gemini' => array( + 'gemini-2.0-flash' => array( + 'input' => 0.00010, + 'output' => 0.00040, + ), + 'gemini-2.0-flash-lite' => array( + 'input' => 0.000038, + 'output' => 0.00015, + ), + 'gemini-1.5-pro' => array( + 'input' => 0.00125, + 'output' => 0.005, + ), + ), + 'grok' => array( + 'grok-3' => array( + 'input' => 0.003, + 'output' => 0.015, + ), + 'grok-3-mini' => array( + 'input' => 0.0003, + 'output' => 0.0005, + ), + ), + ); + + /** Estimate token count (~4 chars per token) */ + public static function estimate( string $text ): int { + return (int) ceil( mb_strlen( $text ) / 4 ); + } + + /** Truncate text to approximately $max_tokens */ + public static function truncate( string $text, int $max_tokens ): string { + $max_chars = $max_tokens * 4; + if ( mb_strlen( $text ) <= $max_chars ) { + return $text; + } + return mb_substr( $text, 0, $max_chars ); + } + + /** + * Estimate cost in USD. + * + * @param int $tokens Number of tokens + * @param string $provider Provider ID + * @param string $model Model ID + * @param string $type 'input' or 'output' + */ + public static function estimateCost( int $tokens, string $provider, string $model, string $type = 'input' ): float { + $price_per_1k = self::PRICING[ $provider ][ $model ][ $type ] ?? 0.002; + return round( ( $tokens / 1000 ) * $price_per_1k, 6 ); + } + + /** Human-readable cost string e.g. "~0,05 €" */ + public static function formatCost( float $usd ): string { + $eur = $usd * 0.92; + if ( $eur < 0.01 ) { + return '< 0,01 €'; + } + return '~' . number_format( $eur, 2, ',', '.' ) . ' €'; + } +}