*/ public function run(?string $candidate = null): array { $secret = $candidate ?: app(SecretVault::class)->get('stripe.secret'); if (blank($secret)) { return ['ok' => false, 'reason' => 'missing']; } try { $account = Http::withToken($secret)->acceptJson()->timeout(15) ->get($this->url('account')); } catch (Throwable) { return ['ok' => false, 'reason' => 'unreachable']; } if ($account->status() === 401) { return ['ok' => false, 'reason' => 'rejected']; } if (! $account->successful()) { return ['ok' => false, 'reason' => 'error', 'status' => $account->status()]; } return [ 'ok' => true, 'account' => $account->json('id'), 'business' => $account->json('settings.dashboard.display_name') ?: $account->json('business_profile.name'), // Stripe does not label the key; the mode is inferred from the // prefix, which is the only thing that is true of both key kinds. // Asked of OperatingMode, not spelled out again here: the same // question decides which slot the migration files a key into and // whether the readiness page reports a key sitting in the wrong // one. Unrecognisable (null) stays false, exactly as before. 'live' => OperatingMode::ofStripeKey($secret) === OperatingMode::Live, 'restricted' => str_starts_with($secret, 'rk_'), 'webhooks' => $this->webhooks($secret), ]; } /** * The configured webhook endpoints and which events each one subscribes to. * * This is the part nobody can see from the outside: a key can be perfectly * valid while the endpoint listens for the wrong five events, and nothing * fails until a payment is not recorded. * * @return array>|null null when the key may not read them */ private function webhooks(string $secret): ?array { try { $response = Http::withToken($secret)->acceptJson()->timeout(15) ->get($this->url('webhook_endpoints'), ['limit' => 10]); } catch (Throwable) { return null; } if (! $response->successful()) { // A restricted key legitimately cannot list these. Saying "unknown" // beats reporting "none configured", which would send someone // hunting for a problem that is not there. return null; } return collect($response->json('data', [])) ->map(fn (array $endpoint) => [ 'url' => $endpoint['url'] ?? '—', 'status' => $endpoint['status'] ?? 'unknown', 'events' => $endpoint['enabled_events'] ?? [], ]) ->all(); } private function url(string $path): string { return rtrim((string) config('services.stripe.api_base', 'https://api.stripe.com/v1'), '/').'/'.$path; } }