$namespace, 'settings' => array_map(fn (array $row): array => $this->payload($row), $this->settings->listNamespace($namespace)), ]; } public function update(string $namespace, string $key, mixed $value, string $actorUserId): array { $this->validateValue($namespace, $key, $value); $id = $this->settings->createVersion($this->ids->generate(), $namespace, $key, $value); $this->audit->record('CONFIG_SETTING_UPDATED', $actorUserId, null, 'configuration_settings', $id, 'SUCCESS', [ 'namespace' => $namespace, 'key' => $key, ]); return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))]; } public function publish(string $namespace, string $key, string $actorUserId): array { $setting = $this->settings->latest($namespace, $key); if ($setting === null) { throw new ApiException('CONFIG_NOT_FOUND', 'Configuration setting was not found.', [], 404); } $this->settings->publish($setting->id, $actorUserId); $this->audit->record('CONFIG_SETTING_PUBLISHED', $actorUserId, null, 'configuration_settings', $setting->id, 'SUCCESS', [ 'namespace' => $namespace, 'key' => $key, ]); return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))]; } public function versions(string $namespace, string $key): array { return [ 'namespace' => $namespace, 'key' => $key, 'versions' => array_map(fn (array $row): array => $this->payload($row), $this->settings->versions($namespace, $key)), ]; } private function payload(array $row): array { return [ 'id' => $row['id'], 'namespace' => $row['namespace'], 'key' => $row['setting_key'], 'value' => json_decode($row['setting_value'], true, flags: JSON_THROW_ON_ERROR), 'status' => $row['status'], 'version_number' => (int) $row['version_number'], 'published_at' => $row['published_at'], 'updated_at' => $row['updated_at'], ]; } private function validateValue(string $namespace, string $key, mixed $value): void { if (str_contains(strtolower($key), 'secret') || str_contains(strtolower($key), 'password')) { throw new ApiException('SECRET_VALUE_NOT_ALLOWED', 'Raw secrets must not be stored in configuration settings.', [], 422); } if (str_starts_with($namespace.'.'.$key, 'fees.') && (! is_numeric($value) || (float) $value < 0)) { throw new ApiException('INVALID_CONFIG_VALUE', 'Fee values must be non-negative numbers.', [], 422); } if (str_contains($key, 'days') && (! is_int($value) || $value < 0)) { throw new ApiException('INVALID_CONFIG_VALUE', 'Day values must be non-negative integers.', [], 422); } if (str_contains($key, 'minutes') && (! is_int($value) || $value < 1)) { throw new ApiException('INVALID_CONFIG_VALUE', 'Minute values must be positive integers.', [], 422); } } }