HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/api.php
<?php
// includes/api.php
if (!defined('ABSPATH')) exit;

if (!defined('ALLSPICE_SETTINGS_OPTION')) define('ALLSPICE_SETTINGS_OPTION', 'allspice_settings');

function allspice_opt_get(): array {
    $v = get_option(ALLSPICE_SETTINGS_OPTION);
    if (!is_array($v)) $v = array();
    if (!isset($v['partner_id'])) $v['partner_id'] = '';
    if (!isset($v['domain_id'])) $v['domain_id'] = '';
    if (!isset($v['webhook_token'])) $v['webhook_token'] = '';
    if (!isset($v['buttons_cache'])) $v['buttons_cache'] = '';
    if (!isset($v['buttons_count'])) $v['buttons_count'] = 0;
    if (!isset($v['cache_fetched_at'])) $v['cache_fetched_at'] = 0;
    if (!isset($v['cache_last_error'])) $v['cache_last_error'] = '';
    if (!isset($v['cache_ttl'])) $v['cache_ttl'] = 12 * HOUR_IN_SECONDS;
    if (!isset($v['manual_refreshed_at'])) $v['manual_refreshed_at'] = 0;
    if (!isset($v['manual_last_error'])) $v['manual_last_error'] = '';
    if (!isset($v['recipes_count'])) $v['recipes_count'] = 0;
    if (!isset($v['recipes_synced_at'])) $v['recipes_synced_at'] = 0;
    if (!isset($v['recipes_last_error'])) $v['recipes_last_error'] = '';
    if (!isset($v['theme_policies_cache'])) $v['theme_policies_cache'] = '';
    if (!isset($v['theme_policies_synced_at'])) $v['theme_policies_synced_at'] = 0;
    if (!isset($v['theme_policies_last_error'])) $v['theme_policies_last_error'] = '';
    if (!isset($v['premium'])) $v['premium'] = true;
    if (!isset($v['premium_checked_at'])) $v['premium_checked_at'] = 0;
    if (!isset($v['premium_last_error'])) $v['premium_last_error'] = '';
    if (!isset($v['widget_variant'])) { $v['widget_variant'] = 'standard'; }
    /*
     * Adaptive rail appearance. Only consulted when widget_variant === 'float'; the standard
     * variant keeps the legacy floating button + WIDCON-d29x rail and is untouched by all of these.
     */
    if (!isset($v['float_desktop_style'])) { $v['float_desktop_style'] = 'floating'; }
    if (!isset($v['float_inline_bg'])) { $v['float_inline_bg'] = '#ffffff'; }
    if (!isset($v['float_inline_border_color'])) { $v['float_inline_border_color'] = '#e7e8ee'; }
    if (!isset($v['float_inline_border_width'])) { $v['float_inline_border_width'] = '1'; }
    if (!isset($v['float_inline_radius'])) { $v['float_inline_radius'] = '16'; }
    if (!isset($v['float_inline_pad_y'])) { $v['float_inline_pad_y'] = '14'; }
    if (!isset($v['float_mobile_style'])) { $v['float_mobile_style'] = 'floating'; }
    if (!isset($v['float_launcher_side'])) { $v['float_launcher_side'] = 'right'; }
    if (!isset($v['float_rail_anchor'])) { $v['float_rail_anchor'] = 'bottom'; }
    if (!isset($v['float_rail_size'])) { $v['float_rail_size'] = 'default'; }
    if (!isset($v['float_rail_mode'])) { $v['float_rail_mode'] = 'light'; }
    if (!isset($v['float_rail_accent_mode'])) { $v['float_rail_accent_mode'] = 'zone'; }
    if (!isset($v['float_rail_accent'])) { $v['float_rail_accent'] = '#3d7dd6'; }
    if (!isset($v['float_rail_announce'])) { $v['float_rail_announce'] = 'zone'; }
    if (!isset($v['float_rail_announce_ms'])) { $v['float_rail_announce_ms'] = 4000; }
    if (!isset($v['float_rail_announce_search'])) { $v['float_rail_announce_search'] = true; }
    if (!isset($v['float_rail_pulse'])) { $v['float_rail_pulse'] = true; }
    if (!isset($v['float_rail_hover_expand'])) { $v['float_rail_hover_expand'] = true; }
    if (!isset($v['float_rail_show_jump'])) { $v['float_rail_show_jump'] = true; }
    return $v;
}

/**
 * Normalized adaptive-rail options, safe to hand straight to the bootstrap config.
 *
 * Values are re-validated on read (not only on save) so a hand-edited or migrated option row can
 * never emit an unexpected value into the page.
 */
function allspice_get_float_launcher_options(): array {
    $s = allspice_opt_get();
    $pick = static function ($value, array $allowed, string $fallback): string {
        $value = sanitize_key((string)$value);
        return in_array($value, $allowed, true) ? $value : $fallback;
    };
    $hex = static function ($value, string $fallback): string {
        $value = sanitize_hex_color(trim((string)$value));
        return $value ? $value : $fallback;
    };
    $ms = (int)($s['float_rail_announce_ms'] ?? 4000);
    if ($ms < 0) { $ms = 0; }
    if ($ms > 15000) { $ms = 15000; }
    return array(
        'float_launcher_side' => $pick($s['float_launcher_side'] ?? '', ['left', 'right'], 'right'),
        /*
         * Per-device widget style. "floating" = the fixed bottom-corner overlay; "inline" = the
         * same widget mounted in the document flow above the recipe card (never covers ads).
         * The rail launcher serves both; page.js picks the style by viewport at boot.
         */
        'float_desktop_style' => $pick($s['float_desktop_style'] ?? '', ['floating', 'inline'], 'floating'),
        'float_mobile_style' => $pick($s['float_mobile_style'] ?? '', ['floating', 'inline'], 'floating'),
        /* Inline-block chrome, so publishers can blend the in-flow widget into their page. */
        'float_inline_bg' => $hex($s['float_inline_bg'] ?? '', '#ffffff'),
        'float_inline_border_color' => $hex($s['float_inline_border_color'] ?? '', '#e7e8ee'),
        'float_inline_border_width' => $pick($s['float_inline_border_width'] ?? '', ['0', '1', '2'], '1'),
        'float_inline_radius' => $pick($s['float_inline_radius'] ?? '', ['0', '8', '12', '16', '24'], '16'),
        'float_inline_pad_y' => $pick($s['float_inline_pad_y'] ?? '', ['8', '14', '22'], '14'),
        'float_rail_anchor' => $pick($s['float_rail_anchor'] ?? '', ['top', 'middle', 'bottom'], 'bottom'),
        'float_rail_size' => $pick($s['float_rail_size'] ?? '', ['default', 'small'], 'default'),
        'float_rail_mode' => $pick($s['float_rail_mode'] ?? '', ['dark', 'light', 'auto'], 'light'),
        'float_rail_accent_mode' => $pick($s['float_rail_accent_mode'] ?? '', ['zone', 'single'], 'zone'),
        'float_rail_accent' => $hex($s['float_rail_accent'] ?? '', '#3d7dd6'),
        'float_rail_announce' => $pick($s['float_rail_announce'] ?? '', ['zone', 'jump'], 'zone'),
        'float_rail_announce_ms' => $ms,
        'float_rail_announce_search' => !empty($s['float_rail_announce_search']),
        'float_rail_pulse' => !empty($s['float_rail_pulse']),
        'float_rail_hover_expand' => !empty($s['float_rail_hover_expand']),
        'float_rail_show_jump' => !empty($s['float_rail_show_jump']),
    );
}

function allspice_get_widget_asset_base(): string {
    return allspice_is_float_variant() ? ALLSPICE_WIDGET_FLOAT_BASE : ALLSPICE_WIDGET_STANDARD_BASE;
}

function allspice_get_widget_variant(): string {
    $settings = allspice_opt_get();
    $variant = sanitize_key((string)($settings['widget_variant'] ?? 'standard'));
    return $variant === 'float' ? 'float' : 'standard';
}

function allspice_is_float_variant(): bool {
    return allspice_get_widget_variant() === 'float';
}

function allspice_opt_update(array $patch): bool {
    $cur = allspice_opt_get();
    $new = array_merge($cur, $patch);
    $ok = update_option(ALLSPICE_SETTINGS_OPTION, $new, false);
    wp_cache_delete(ALLSPICE_SETTINGS_OPTION, 'options');
    wp_cache_delete('alloptions', 'options');
    return (bool)$ok;
}

function allspice_opt_update_force(array $patch): bool {
    $cur = allspice_opt_get();
    $new = array_merge($cur, $patch);
    delete_option(ALLSPICE_SETTINGS_OPTION);
    $ok = add_option(ALLSPICE_SETTINGS_OPTION, $new, '', false);
    wp_cache_delete(ALLSPICE_SETTINGS_OPTION, 'options');
    wp_cache_delete('alloptions', 'options');
    return (bool)$ok;
}

function allspice_webhook_base(): string {
    $base = defined('ALLSPICE_WEBHOOK_BASE') ? (string)ALLSPICE_WEBHOOK_BASE : ALLSPICE_WEBHOOK_BASE_DEFAULT;
    $base = rtrim($base, '/');
    return apply_filters('allspice_webhook_base', $base);
}

function allspice_buttons_endpoint(): string {
    $path = '/loadButtons';
    return apply_filters('allspice_buttons_endpoint', allspice_webhook_base() . $path);
}

function allspice_wp_event_endpoint(): string {
    $path = '/wp';
    return apply_filters('allspice_wp_event_endpoint', allspice_webhook_base() . $path);
}

function allspice_buttons_cache_count_from_payload($json): int {
    if (is_array($json) && isset($json['buttons']) && is_array($json['buttons'])) return count($json['buttons']);
    if (is_array($json) && array_is_list($json)) return count($json);
    return 0;
}

function allspice_buttons_cache_get(): array {
    $s = allspice_opt_get();
    $raw = (string)$s['buttons_cache'];
    if ($raw === '') return array();
    $json = json_decode($raw, true);
    if (!is_array($json)) return array();
    if (isset($json['buttons']) && is_array($json['buttons'])) return $json['buttons'];
    if (isset($json[0]) && is_array($json)) return $json;
    return array();
}

function allspice_refresh_buttons_cache(bool $force = false): array {
    $s = allspice_opt_get();
    $partner_id = trim((string)$s['partner_id']);
    $domain_id = trim((string)$s['domain_id']);
    $token = trim((string)$s['webhook_token']);
    $ttl = (int)$s['cache_ttl'];
    if ($ttl < 300) $ttl = 300;
    $now = time();
    if (!$force && (int)$s['cache_fetched_at'] > 0 && ($now - (int)$s['cache_fetched_at']) < $ttl && (string)$s['buttons_cache'] !== '') {
        return array('ok' => true, 'cached' => true, 'count' => (int)$s['buttons_count'], 'error' => '', 'wrote' => false);
    }
    if ($partner_id === '' || $domain_id === '' || $token === '') {
        $msg = 'Missing partner_id, domain_id, or webhook_token';
        $wrote = allspice_opt_update(array('cache_last_error' => $msg));
        return array('ok' => $wrote, 'cached' => false, 'count' => 0, 'error' => $msg, 'wrote' => $wrote);
    }
    $endpoint = allspice_buttons_endpoint();
    $url = add_query_arg(array('partner_id' => $partner_id, 'domain_id' => $domain_id), $endpoint);
    $headers = array(
        'Accept' => 'application/json',
        'Authorization' => 'Bearer ' . $token,
        'X-Allspice-Partner-Id' => $partner_id,
        'X-Allspice-Domain-Id' => $domain_id,
        'Cache-Control' => 'no-cache',
        'Pragma' => 'no-cache',
    );
    $resp = wp_remote_get($url, array('timeout' => 15, 'headers' => $headers));
    if (is_wp_error($resp)) {
        $msg = trim((string)$resp->get_error_message());
        if ($msg === '') {
            $codes = $resp->get_error_codes();
            $msg = !empty($codes)
                ? ('WP_Error: ' . implode(', ', $codes))
                : 'WP_Error with no message (wp_remote_get)';
        }
        $wrote = allspice_opt_update(array('cache_last_error' => $msg));
        return array('ok' => $wrote, 'cached' => false, 'count' => 0, 'error' => $msg, 'wrote' => $wrote);
    }
    $code = (int)wp_remote_retrieve_response_code($resp);
    $body = (string)wp_remote_retrieve_body($resp);
    if ($code < 200 || $code >= 300) {
        $msg = 'HTTP ' . $code . ': ' . substr($body, 0, 300);
        $wrote = allspice_opt_update(array('cache_last_error' => $msg));
        return array('ok' => $wrote, 'cached' => false, 'count' => 0, 'error' => $msg, 'wrote' => $wrote);
    }
    $json = json_decode($body, true);
    if (!is_array($json)) {
        $msg = 'Invalid JSON: ' . substr($body, 0, 300);
        $wrote = allspice_opt_update(array('cache_last_error' => $msg));
        return array('ok' => $wrote, 'cached' => false, 'count' => 0, 'error' => $msg, 'wrote' => $wrote);
    }
    $count = allspice_buttons_cache_count_from_payload($json);
    $wrote = allspice_opt_update(array(
        'buttons_cache' => wp_json_encode($json),
        'buttons_count' => $count,
        'cache_fetched_at' => $now,
        'cache_last_error' => '',
        'cache_ttl' => $ttl,
        'allspice_debug_nonce' => wp_generate_uuid4(),
    ));
    $s2 = allspice_opt_get();
//    error_log('[Allspice] refresh finished. now=' . $now . ' stored_fetched_at=' . (int)$s2['cache_fetched_at']);
    return array('ok' => true, 'cached' => false, 'count' => $count, 'error' => $wrote ? '' : 'Fetched OK, but option write failed (update_option returned false)', 'wrote' => $wrote);
}
        
function allspice_recipes_endpoint(): string {
    $path = '/loadRecipesForWPSync';
    return apply_filters('allspice_recipes_endpoint', allspice_webhook_base() . $path);
}
        
function allspice_recipes_list_from_payload($json): array {
    if (is_array($json) && isset($json['recipes']) && is_array($json['recipes'])) return $json['recipes'];
    if (is_array($json) && isset($json['items']) && is_array($json['items'])) return $json['items'];
    if (is_array($json) && isset($json[0]) && is_array($json)) return $json;
    return array();
}
        
function allspice_refresh_recipes_cache(bool $force = false): array {
    $s = allspice_opt_get();
    $partner_id = trim((string)$s['partner_id']);
    $domain_id = trim((string)$s['domain_id']);
    $token = trim((string)$s['webhook_token']);
    $now = time();
    if ($partner_id === '' || $domain_id === '' || $token === '') {
        $msg = 'Missing partner_id, domain_id, or webhook_token';
        allspice_opt_update(array('recipes_last_error' => $msg));
        return array('ok' => false, 'count' => 0, 'error' => $msg);
    }
    $endpoint = allspice_recipes_endpoint();
    $url = add_query_arg(array('partner_id' => $partner_id, 'domain_id' => $domain_id), $endpoint);
    $headers = array(
        'Accept' => 'application/json',
        'Authorization' => 'Bearer ' . $token,
        'X-Allspice-Partner-Id' => $partner_id,
        'X-Allspice-Domain-Id' => $domain_id,
        'Cache-Control' => 'no-cache',
        'Pragma' => 'no-cache',
    );
    $resp = wp_remote_get($url, array('timeout' => 30, 'headers' => $headers));
    if (is_wp_error($resp)) {
        $msg = trim((string)$resp->get_error_message());
        if ($msg === '') {
            $codes = $resp->get_error_codes();
            $msg = !empty($codes)
                ? ('WP_Error: ' . implode(', ', $codes))
                : 'WP_Error with no message (wp_remote_get)';
        }
        allspice_opt_update(array('recipes_last_error' => $msg));
        return array('ok' => false, 'count' => 0, 'error' => $msg);
    }
    $code = (int)wp_remote_retrieve_response_code($resp);
    $body = (string)wp_remote_retrieve_body($resp);
    if ($code < 200 || $code >= 300) {
        $msg = 'HTTP ' . $code . ': ' . substr($body, 0, 300);
        allspice_opt_update(array('recipes_last_error' => $msg));
        return array('ok' => false, 'count' => 0, 'error' => $msg);
    }
    $json = json_decode($body, true);
    if (!is_array($json)) {
        $msg = 'Invalid JSON: ' . substr($body, 0, 300);
        allspice_opt_update(array('recipes_last_error' => $msg));
        return array('ok' => false, 'count' => 0, 'error' => $msg);
    }
    $premium = array_key_exists('premium', $json) ? (bool)$json['premium'] : true;
    $items = allspice_recipes_list_from_payload($json);
    if (!function_exists('allspice_upsert_recipes') || !function_exists('allspice_recipes_count')) {
        $msg = 'Recipe DB helpers not loaded (allspice_upsert_recipes / allspice_recipes_count missing)';
        allspice_opt_update(array('recipes_last_error' => $msg));
        return array('ok' => false, 'count' => 0, 'error' => $msg);
    }
    if (function_exists('allspice_recipes_table_ensure_columns')) {
        allspice_recipes_table_ensure_columns();
    }
    $written = allspice_upsert_recipes($items);
    $count = allspice_recipes_count();
    allspice_opt_update(array(
        'recipes_count' => $count,
        'recipes_synced_at' => $now,
        'recipes_last_error' => '',
        'premium' => $premium,
        'premium_checked_at' => $now,
        'premium_last_error' => '',
    ));
    return array(
        'ok' => true,
        'count' => $count,
        'error' => '',
        'written' => $written,
        'premium' => $premium,
    );
}

function allspice_theme_policies_endpoint(): string {
    $path = '/loadThemeAndPolicies';
    return apply_filters('allspice_theme_policies_endpoint', allspice_webhook_base() . $path);
}
        
function allspice_theme_policies_extract($json): array {
    if (!is_array($json)) return ['theme' => null, 'policy' => null, 'domain' => ''];
    if (array_key_exists('theme', $json) || array_key_exists('policy', $json) || array_key_exists('domain', $json) || array_key_exists('domain_name', $json)) {
        return [
            'theme'  => $json['theme']  ?? null,
            'policy' => $json['policy'] ?? null,
            'domain' => (string)($json['domain'] ?? $json['domain_name'] ?? ''),
        ];
    }
    if (isset($json['data']) && is_array($json['data']) && (isset($json['data']['theme']) || isset($json['data']['policy']) || isset($json['data']['domain']) || isset($json['data']['domain_name']))) {
        return [
            'theme'  => $json['data']['theme']  ?? null,
            'policy' => $json['data']['policy'] ?? null,
            'domain' => (string)($json['data']['domain'] ?? $json['data']['domain_name'] ?? ''),
        ];
    }
    foreach ($json as $v) {
        if (!is_array($v)) continue;
        if (isset($v['theme']) || isset($v['policy']) || isset($v['domain']) || isset($v['domain_name'])) {
            return [
                'theme'  => $v['theme']  ?? null,
                'policy' => $v['policy'] ?? null,
                'domain' => (string)($v['domain'] ?? $v['domain_name'] ?? ''),
            ];
        }
    }
    return ['theme' => null, 'policy' => null, 'domain' => ''];
}
        
function allspice_theme_policies_cache_get(): array {
    $s = allspice_opt_get();
    $raw = (string)($s['theme_policies_cache'] ?? '');
    if ($raw === '') return ['theme' => null, 'policy' => null, 'domain' => '', 'raw' => null];
    $json = json_decode($raw, true);
    if (!is_array($json)) return ['theme' => null, 'policy' => null, 'domain' => '', 'raw' => null];
    $ex = allspice_theme_policies_extract($json);
    return [
        'theme' => $ex['theme'],
        'policy' => $ex['policy'],
        'domain' => (string)($ex['domain'] ?? ''),
        'raw' => $json
    ];
}

function allspice_refresh_theme_policies_cache(bool $force = false): array {
    $s = allspice_opt_get();
    $partner_id = trim((string)$s['partner_id']);
    $domain_id  = trim((string)$s['domain_id']);
    $token = trim((string)$s['webhook_token']);
    $ttl = (int)($s['cache_ttl'] ?? 12 * HOUR_IN_SECONDS);
    if ($ttl < 300) $ttl = 300;
    $now = time();
    if (
        !$force &&
        (int)($s['theme_policies_synced_at'] ?? 0) > 0 &&
        ($now - (int)($s['theme_policies_synced_at'] ?? 0)) < $ttl &&
        (string)($s['theme_policies_cache'] ?? '') !== ''
    ) {
        $cached = allspice_theme_policies_cache_get();
        $theme_ok = ($cached['theme'] !== null);
        $pol_ok = ($cached['policy'] !== null);
        $domain_ok = (trim((string)($cached['domain'] ?? '')) !== '');
        return [
            'ok' => true,
            'cached' => true,
            'theme_ok' => $theme_ok,
            'policy_ok' => $pol_ok,
            'domain_ok' => $domain_ok,
            'error' => '',
            'wrote' => false,
        ];
    }
    if ($partner_id === '' || $domain_id === '' || $token === '') {
        $msg = 'Missing partner_id, domain_id, or webhook_token';
        $wrote = allspice_opt_update(['theme_policies_last_error' => $msg]);
        return ['ok' => $wrote, 'cached' => false, 'theme_ok' => false, 'policy_ok' => false, 'domain_ok' => false, 'error' => $msg, 'wrote' => $wrote];
    }
    $endpoint = allspice_theme_policies_endpoint();
    $url = add_query_arg(['partner_id' => $partner_id, 'domain_id' => $domain_id], $endpoint);
    $headers = [
        'Accept' => 'application/json',
        'Authorization' => 'Bearer ' . $token,
        'X-Allspice-Partner-Id' => $partner_id,
        'X-Allspice-Domain-Id' => $domain_id,
        'Cache-Control' => 'no-cache',
        'Pragma' => 'no-cache',
    ];
    $resp = wp_remote_get($url, ['timeout' => 15, 'headers' => $headers]);
    if (is_wp_error($resp)) {
        $msg = trim((string)$resp->get_error_message());
        if ($msg === '') {
            $codes = $resp->get_error_codes();
            $msg = !empty($codes) ? ('WP_Error: ' . implode(', ', $codes)) : 'WP_Error with no message (wp_remote_get)';
        }
        $wrote = allspice_opt_update(['theme_policies_last_error' => $msg]);
        return ['ok' => $wrote, 'cached' => false, 'theme_ok' => false, 'policy_ok' => false, 'domain_ok' => false, 'error' => $msg, 'wrote' => $wrote];
    }
    $code = (int)wp_remote_retrieve_response_code($resp);
    $body = (string)wp_remote_retrieve_body($resp);
    if ($code < 200 || $code >= 300) {
        $msg = 'HTTP ' . $code . ': ' . substr($body, 0, 300);
        $wrote = allspice_opt_update(['theme_policies_last_error' => $msg]);
        return ['ok' => $wrote, 'cached' => false, 'theme_ok' => false, 'policy_ok' => false, 'domain_ok' => false, 'error' => $msg, 'wrote' => $wrote];
    }
    $json = json_decode($body, true);
    if (!is_array($json)) {
        $msg = 'Invalid JSON: ' . substr($body, 0, 300);
        $wrote = allspice_opt_update(['theme_policies_last_error' => $msg]);
        return ['ok' => $wrote, 'cached' => false, 'theme_ok' => false, 'policy_ok' => false, 'domain_ok' => false, 'error' => $msg, 'wrote' => $wrote];
    }
    $ex = allspice_theme_policies_extract($json);
    $theme_ok = ($ex['theme'] !== null);
    $pol_ok = ($ex['policy'] !== null);
    $domain_ok = (trim((string)($ex['domain'] ?? '')) !== '');
    $wrote = allspice_opt_update([
        'theme_policies_cache' => wp_json_encode($json),
        'theme_policies_synced_at' => $now,
        'theme_policies_last_error' => '',
    ]);
    return [
        'ok' => true,
        'cached' => false,
        'theme_ok' => $theme_ok,
        'policy_ok' => $pol_ok,
        'domain_ok' => $domain_ok,
        'error' => $wrote ? '' : 'Fetched OK, but option write failed (update_option returned false)',
        'wrote' => $wrote,
    ];
}
        
function allspice_theme_policy_payload(): array {
    $s = allspice_opt_get();
    $tp = function_exists('allspice_theme_policies_cache_get')
        ? allspice_theme_policies_cache_get()
        : ['theme' => null, 'policy' => null, 'domain' => '', 'raw' => null];
    $raw = is_array($tp['raw']) ? $tp['raw'] : [];
    return [
        'partner_id' => (string)$s['partner_id'],
        'domain_id' => (string)$s['domain_id'],
        'synced_at' => (int)($s['theme_policies_synced_at'] ?? 0),
        'last_error' => (string)($s['theme_policies_last_error'] ?? ''),
        'ok' => isset($raw['ok']) ? (bool)$raw['ok'] : null,
        'hash' => (string)($raw['hash'] ?? ''),
        'fetched_at' => (string)($raw['fetched_at'] ?? ''),
        'theme_id' => (string)($raw['theme_id'] ?? ''),
        'domain' => (string)($tp['domain'] ?? ''),
        'theme' => $tp['theme'],
        'policy' => $tp['policy'],
    ];
}