*/ protected array $sensitiveKeys = [ // raw IDs / morph types (uuid is the public identifier) 'id', 'team_id', 'tokenable_id', 'tokenable_type', 'server_id', 'private_key_id', 'cloud_provider_token_id', 'hetzner_server_id', 'environment_id', 'destination_id', 'source_id', 'repository_project_id', 'application_id', 'service_id', 'project_id', 'parent_id', 'resourceable', 'resourceable_id', 'resourceable_type', 'destination_type', 'source_type', 'tokenable', // sentinel / observability secrets 'sentinel_token', 'sentinel_custom_url', 'logdrain_newrelic_license_key', 'logdrain_axiom_api_key', 'logdrain_custom_config', 'logdrain_custom_config_parser', // database passwords 'postgres_password', 'dragonfly_password', 'keydb_password', 'redis_password', 'mongo_initdb_root_password', 'mariadb_password', 'mariadb_root_password', 'mysql_password', 'mysql_root_password', 'clickhouse_admin_password', // app/env secrets 'value', 'real_value', 'http_basic_auth_password', // database connection strings embed credentials 'internal_db_url', 'external_db_url', 'init_scripts', // webhook secrets 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', // bulky / unsafe blobs 'dockerfile', 'docker_compose', 'docker_compose_raw', 'custom_labels', 'environment_variables', 'environment_variables_preview', 'validation_logs', 'server_metadata', ]; /** * Recursively remove sensitive keys from any nested array structure. * * @param array $data * @return array */ protected function scrubSensitive(array $data): array { $deny = array_flip($this->sensitiveKeys); $walk = function ($value) use (&$walk, $deny) { if (! is_array($value)) { return $value; } $out = []; foreach ($value as $key => $inner) { if (is_string($key) && isset($deny[$key])) { continue; } $out[$key] = $walk($inner); } return $out; }; return $walk($data); } /** * @param array|array $data * @param array> $actions * @param array|null $pagination */ protected function respond(array $data, array $actions = [], ?array $pagination = null): Response { $payload = ['data' => $data]; if ($actions !== []) { $payload['_actions'] = $actions; } if ($pagination !== null) { $payload['_pagination'] = $pagination; } return Response::json($payload); } /** * @return array{page:int, per_page:int, offset:int} */ protected function paginationArgs(Request $request): array { $page = max(1, (int) ($request->get('page') ?? 1)); $perPage = (int) ($request->get('per_page') ?? $this->defaultPerPage); $perPage = max(1, min($this->maxPerPage, $perPage)); return [ 'page' => $page, 'per_page' => $perPage, 'offset' => ($page - 1) * $perPage, ]; } /** * @param array{page:int, per_page:int, offset:int} $args * @return array|null */ protected function paginationMeta(string $tool, array $args, int $total, array $extraArgs = []): ?array { $page = $args['page']; $perPage = $args['per_page']; $totalPages = (int) ceil($total / $perPage); $meta = [ 'page' => $page, 'per_page' => $perPage, 'total' => $total, 'total_pages' => $totalPages, ]; if ($page < $totalPages) { $meta['next'] = [ 'tool' => $tool, 'args' => array_merge($extraArgs, ['page' => $page + 1, 'per_page' => $perPage]), ]; } return $meta; } /** * HATEOAS-style action suggestions for an application. * * @return array> */ protected function actionsForApplication(string $uuid, ?string $status = null): array { $actions = [ ['tool' => 'get_application', 'args' => ['uuid' => $uuid], 'hint' => 'Full details'], ]; $s = strtolower((string) $status); if (str_contains($s, 'running')) { $actions[] = ['tool' => 'control', 'args' => ['resource' => 'application', 'action' => 'restart', 'uuid' => $uuid], 'hint' => 'Restart']; $actions[] = ['tool' => 'control', 'args' => ['resource' => 'application', 'action' => 'stop', 'uuid' => $uuid], 'hint' => 'Stop']; } else { $actions[] = ['tool' => 'control', 'args' => ['resource' => 'application', 'action' => 'start', 'uuid' => $uuid], 'hint' => 'Start']; } return $actions; } /** * @return array> */ protected function actionsForDatabase(string $uuid, ?string $status = null): array { $actions = [ ['tool' => 'get_database', 'args' => ['uuid' => $uuid], 'hint' => 'Full details'], ]; $s = strtolower((string) $status); if (str_contains($s, 'running')) { $actions[] = ['tool' => 'control', 'args' => ['resource' => 'database', 'action' => 'restart', 'uuid' => $uuid], 'hint' => 'Restart']; $actions[] = ['tool' => 'control', 'args' => ['resource' => 'database', 'action' => 'stop', 'uuid' => $uuid], 'hint' => 'Stop']; } else { $actions[] = ['tool' => 'control', 'args' => ['resource' => 'database', 'action' => 'start', 'uuid' => $uuid], 'hint' => 'Start']; } return $actions; } /** * @return array> */ protected function actionsForService(string $uuid, ?string $status = null): array { $actions = [ ['tool' => 'get_service', 'args' => ['uuid' => $uuid], 'hint' => 'Full details'], ]; $s = strtolower((string) $status); if (str_contains($s, 'running')) { $actions[] = ['tool' => 'control', 'args' => ['resource' => 'service', 'action' => 'restart', 'uuid' => $uuid], 'hint' => 'Restart']; $actions[] = ['tool' => 'control', 'args' => ['resource' => 'service', 'action' => 'stop', 'uuid' => $uuid], 'hint' => 'Stop']; } else { $actions[] = ['tool' => 'control', 'args' => ['resource' => 'service', 'action' => 'start', 'uuid' => $uuid], 'hint' => 'Start']; } return $actions; } /** * @return array> */ protected function actionsForServer(string $uuid): array { return [ ['tool' => 'get_server', 'args' => ['uuid' => $uuid], 'hint' => 'Full details'], ]; } }