Plugin Directory

Changeset 3470796


Ignore:
Timestamp:
02/27/2026 02:29:08 AM (11 days ago)
Author:
lightsyncpro
Message:

Version 2.15 - OpenRouter credentials now use short-lease token refresh

Location:
lightsyncpro/trunk
Files:
2 added
1 edited

Legend:

Unmodified
Added
Removed
  • lightsyncpro/trunk/includes/oauth/class-openrouter-oauth.php

    r3469756 r3470796  
    55use LightSyncPro\Util\Crypto;
    66use LightSyncPro\Util\Logger;
     7
     8if ( ! defined( 'ABSPATH' ) ) exit;
    79
    810
     
    1214 * Handles broker-based OAuth PKCE flow for OpenRouter AI.
    1315 *
    14  * KEY DIFFERENCE from Canva/Figma/Dropbox:
    15  * - OpenRouter returns a persistent API key, NOT an expiring access token
    16  * - No token refresh needed — the key is valid until user revokes it
    17  * - API key is retrieved during OAuth and stored locally (encrypted)
    18  * - Generation calls go DIRECTLY to OpenRouter (not proxied through broker)
    19  * - Broker is only used for OAuth flow and model list
     16 * Follows the SAME pattern as Canva/Figma/Dropbox:
     17 * - Broker handles OAuth and stores the persistent credential (API key)
     18 * - Plugin receives only a broker JWT during pickup
     19 * - Plugin calls broker's /token/refresh to get a short-lease API key
     20 * - Short-lease key is stored as a WordPress transient (auto-expires in 1hr)
     21 * - When transient expires, plugin requests a fresh key from broker
     22 * - No long-lived credentials in the WordPress database
    2023 *
    2124 * Flow:
     
    2326 * 2. Broker redirects to openrouter.ai/auth (PKCE)
    2427 * 3. User authenticates → code returned to broker
    25  * 4. Broker exchanges code for API key, encrypts & stores
     28 * 4. Broker exchanges code for API key, encrypts & stores on broker
    2629 * 5. User redirected back to WP with ?lsp_openrouter_connected=1
    27  * 6. Plugin picks up broker JWT AND API key
    28  * 7. Generation: plugin → OpenRouter directly (with API key)
    29  * 8. Models: plugin → broker (with JWT) for curated list
     30 * 6. Plugin picks up broker JWT (no API key in response)
     31 * 7. Generation: plugin calls broker /token/refresh → gets short-lease key → calls OpenRouter directly
     32 * 8. Models: plugin → OpenRouter public API (no auth needed)
    3033 */
    3134class OpenRouterOAuth {
     
    124127        ];
    125128
    126         // Store API key for direct OpenRouter calls (skips broker proxy)
    127         if (!empty($data['data']['api_key'])) {
    128             $opts['openrouter_api_key_enc'] = Crypto::enc($data['data']['api_key']);
    129             error_log('[LSP OpenRouter] ✓ API key received and encrypted');
    130         } else {
    131             error_log('[LSP OpenRouter] ⚠ No API key in pickup response — only broker token');
    132         }
     129        // Clean up any legacy stored API key from pre-refresh architecture
     130        $opts['openrouter_api_key_enc'] = '';
    133131
    134132        Admin::set_opt($opts);
    135133
    136         error_log('[LSP OpenRouter] ✓ Successfully connected and stored credentials');
     134        // Clear any stale short-lease key transient
     135        delete_transient('lsp_openrouter_access_token');
     136
     137        error_log('[LSP OpenRouter] ✓ Connected — broker token stored, API key stays on broker');
    137138        set_transient('lsp_openrouter_notice', 'success', 60);
    138139
     
    165166
    166167    /**
    167      * Get the OpenRouter API key for direct calls (bypasses broker proxy)
     168     * Get the OpenRouter API key for direct calls (short-lease from broker)
    168169     *
    169      * If not cached locally, fetches from broker using the broker JWT.
    170      * This ensures generation requests go directly to OpenRouter, not through our server.
     170     * Mirrors the Canva/Figma/Dropbox token refresh pattern:
     171     * 1. Check WordPress transient (cached short-lease key, ≤1 hour)
     172     * 2. If expired, call broker's /openrouter/token/refresh endpoint
     173     * 3. Broker decrypts stored API key and returns it with TTL
     174     * 4. Store in transient (auto-expires)
     175     *
     176     * Result: no long-lived credentials in the WordPress database.
    171177     *
    172178     * @return string|WP_Error API key or error
    173179     */
    174180    public static function get_api_key() {
    175         $o = Admin::get_opt();
    176 
    177         // Try locally stored key first
    178         $enc = $o['openrouter_api_key_enc'] ?? '';
    179         if ($enc) {
    180             $key = Crypto::dec($enc);
    181             if ($key) return $key;
    182         }
    183 
    184         // Not stored yet (existing connection from before direct mode) — fetch from broker
     181        // 1. Check transient first (cached short-lease key)
     182        $cached = get_transient('lsp_openrouter_access_token');
     183        if ($cached) {
     184            return $cached;
     185        }
     186
     187        // 2. Get broker token for authentication
    185188        $broker_token = self::get_broker_token();
    186189        if (is_wp_error($broker_token)) {
     
    188191        }
    189192
    190         $resp = wp_remote_get(
    191             self::BROKER_URL . '/wp-json/lsp-broker/v1/openrouter/get-key',
     193        // 3. Request fresh short-lease key from broker
     194        $resp = wp_remote_post(
     195            self::BROKER_URL . '/wp-json/lsp-broker/v1/openrouter/token/refresh',
    192196            [
    193197                'timeout' => 15,
     
    200204
    201205        if (is_wp_error($resp)) {
    202             return new \WP_Error('key_fetch_failed', 'Could not retrieve API key: ' . $resp->get_error_message());
    203         }
    204 
     206            return new \WP_Error('token_refresh_failed', 'Could not refresh OpenRouter key: ' . $resp->get_error_message());
     207        }
     208
     209        $http_code = wp_remote_retrieve_response_code($resp);
    205210        $data = json_decode(wp_remote_retrieve_body($resp), true);
    206211
    207         if (empty($data['success']) || empty($data['data']['api_key'])) {
    208             $error = $data['data']['error'] ?? 'Unknown error retrieving API key';
    209             return new \WP_Error('key_fetch_failed', $error);
    210         }
    211 
    212         // Cache locally for future calls
    213         Admin::set_opt([
    214             'openrouter_api_key_enc' => Crypto::enc($data['data']['api_key']),
    215         ]);
    216 
    217         return $data['data']['api_key'];
     212        // Handle broker errors
     213        if ($http_code !== 200 || empty($data['success']) || empty($data['data']['access_token'])) {
     214            $error = $data['data']['error'] ?? 'Unknown error refreshing OpenRouter key';
     215            $reconnect = !empty($data['data']['reconnect']);
     216
     217            Logger::debug('[LSP OpenRouter] Token refresh failed: ' . $error . ($reconnect ? ' (reconnect needed)' : ''));
     218
     219            return new \WP_Error(
     220                $reconnect ? 'openrouter_reconnect' : 'token_refresh_failed',
     221                $error
     222            );
     223        }
     224
     225        // 4. Store as transient — auto-expires, never persists in options table
     226        $key = $data['data']['access_token'];
     227        $ttl = (int)($data['data']['expires_in'] ?? 3600);
     228
     229        // Refresh 60s early to avoid hitting OpenRouter with a stale key
     230        set_transient('lsp_openrouter_access_token', $key, max($ttl - 60, 300));
     231
     232        Logger::debug('[LSP OpenRouter] Short-lease key refreshed, TTL=' . $ttl . 's');
     233
     234        return $key;
    218235    }
    219236
     
    239256        Admin::set_opt([
    240257            'openrouter_broker_token_enc' => '',
    241             'openrouter_api_key_enc'      => '',
     258            'openrouter_api_key_enc'      => '',  // Clear any legacy stored key
    242259            'openrouter_connected_at'     => 0,
    243260        ]);
    244261
    245         // Clear cached models
     262        // Clear short-lease key transient and cached models
     263        delete_transient('lsp_openrouter_access_token');
    246264        delete_transient('lsp_openrouter_models_v3');
    247265
    248         Logger::debug('[LSP OpenRouter] Disconnected – broker token cleared');
     266        Logger::debug('[LSP OpenRouter] Disconnected – broker token and transients cleared');
    249267    }
    250268
Note: See TracChangeset for help on using the changeset viewer.