$entry) { $key = is_array($entry) ? trim((string)($entry['apiKey'] ?? '')) : ''; if ($key !== '') $secrets['providerCredentials'][(string)$provider]['apiKey'] = $key; } foreach ((array)($settings['imageServices'] ?? []) as $id => $entry) { $key = is_array($entry) ? trim((string)($entry['apiKey'] ?? '')) : ''; if ($key !== '') $secrets['imageServices'][(string)$id]['apiKey'] = $key; } $brave = trim((string)($settings['research']['brave']['apiKey'] ?? '')); if ($brave !== '') $secrets['research']['brave']['apiKey'] = $brave; $pexels = trim((string)($settings['stock']['pexels']['apiKey'] ?? '')); if ($pexels !== '') $secrets['stock']['pexels']['apiKey'] = $pexels; return $secrets; } /** The settings array without any key. */ function raijuAiStripSecrets(array $settings): array { unset($settings['apiKey']); if (is_array($settings['providerCredentials'] ?? null)) { foreach ($settings['providerCredentials'] as $provider => $entry) { if (is_array($entry)) unset($settings['providerCredentials'][$provider]['apiKey']); } } unset($settings['imageServices']); if (is_array($settings['research']['brave'] ?? null)) { unset($settings['research']['brave']['apiKey']); if (!$settings['research']['brave']) unset($settings['research']['brave']); } if (is_array($settings['stock']['pexels'] ?? null)) { unset($settings['stock']['pexels']['apiKey']); if (!$settings['stock']['pexels']) unset($settings['stock']['pexels']); if (!$settings['stock']) unset($settings['stock']); } return $settings; } /** Write the secrets file: owner-only where the system allows it, replaced atomically. */ function raijuAiWriteSecrets(array $secrets): bool { if (!$secrets && !is_file(RAIJU_AI_SECRETS_PATH)) return true; if (!raijuJsonAtomicWrite(RAIJU_AI_SECRETS_PATH, $secrets)) return false; @chmod(RAIJU_AI_SECRETS_PATH, 0600); return true; } /** * Store the settings: the keys in the secrets file, the rest in the settings file. The secrets go first, so a * failure never leaves a settings file without the keys it used to hold. */ function raijuAiWriteStoredSettings(array $settings): bool { if (!raijuAiWriteSecrets(raijuAiExtractSecrets($settings))) return false; $dir = dirname(RAIJU_AI_SETTINGS_PATH); if (!is_dir($dir)) @mkdir($dir, 0755, true); $json = json_encode(raijuAiStripSecrets($settings), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return $json !== false && file_put_contents(RAIJU_AI_SETTINGS_PATH, $json, LOCK_EX) !== false; } /** * Move keys that an older version left in the settings file into the secrets file. Does nothing when there * are none. Safe to call often. */ function raijuAiMigrateSecrets(): bool { $raw = raijuAiReadJsonObject(RAIJU_AI_SETTINGS_PATH); if (!$raw || raijuAiExtractSecrets($raw) === [] && !isset($raw['apiKey'])) return false; $moved = false; $done = raijuJsonUpdate(RAIJU_AI_SETTINGS_PATH, static function (array &$data) use (&$moved): bool { $secrets = raijuAiExtractSecrets($data); if ($secrets === [] && !isset($data['apiKey'])) return false; // Keys that are already in the secrets file win; the file may have been written by a newer save. $secrets = array_replace_recursive($secrets, raijuAiReadJsonObject(RAIJU_AI_SECRETS_PATH)); if (!raijuAiWriteSecrets($secrets)) return false; $data = raijuAiStripSecrets($data); $moved = true; return true; }); return $done && $moved; } /** * Delete one stored key right away. $target: "provider:", "image:", "brave" or "pexels". Anything else is refused. * Returns whether a key existed. */ function raijuAiClearSecret(string $target): bool { raijuAiMigrateSecrets(); // a key an older version left in the settings file must go too $secrets = raijuAiReadJsonObject(RAIJU_AI_SECRETS_PATH); $existed = false; if (str_starts_with($target, 'provider:')) { $provider = substr($target, 9); if (!in_array($provider, ['openai-compatible', 'openrouter', 'anthropic', 'kie'], true)) throw new InvalidArgumentException('Unknown provider.'); $existed = trim((string)($secrets['providerCredentials'][$provider]['apiKey'] ?? '')) !== ''; unset($secrets['providerCredentials'][$provider]['apiKey']); if (empty($secrets['providerCredentials'][$provider])) unset($secrets['providerCredentials'][$provider]); if (empty($secrets['providerCredentials'])) unset($secrets['providerCredentials']); } elseif (str_starts_with($target, 'image:')) { $service = substr($target, 6); if (!in_array($service, ['openai-compatible', 'openrouter', 'kie', 'fal', 'comet', 'custom'], true)) throw new InvalidArgumentException('Unknown image service.'); $existed = trim((string)($secrets['imageServices'][$service]['apiKey'] ?? '')) !== ''; unset($secrets['imageServices'][$service]); if (empty($secrets['imageServices'])) unset($secrets['imageServices']); } elseif ($target === 'brave') { $existed = trim((string)($secrets['research']['brave']['apiKey'] ?? '')) !== ''; unset($secrets['research']); } elseif ($target === 'pexels') { $existed = trim((string)($secrets['stock']['pexels']['apiKey'] ?? '')) !== ''; unset($secrets['stock']); } else { throw new InvalidArgumentException('Unknown key.'); } if (!raijuAiWriteSecrets($secrets)) throw new RuntimeException('Could not remove the key.'); return $existed; }