File size: 8,841 Bytes
9d2d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/**
 * Unified infrastructure service module -- replaces two legacy services:
 *   - src/services/outages.ts (Cloudflare Radar internet outages)
 *   - ServiceStatusPanel's direct /api/service-status fetch
 *
 * All data now flows through the InfrastructureServiceClient RPC.
 */

import { getRpcBaseUrl } from '@/services/rpc-client';
import type { ListInternetDdosAttacksResponse, ListInternetOutagesResponse, ListInternetTrafficAnomaliesResponse, ListServiceStatusesResponse, InternetOutage as ProtoOutage, ServiceStatus as ProtoServiceStatus } from '@/generated/client/worldmonitor/infrastructure/v1/service_client';
import type { InternetOutage } from '@/types';
import { createCircuitBreaker } from '@/utils';
import { isFeatureAvailable } from '../runtime-config';
import { getHydratedData } from '@/services/bootstrap';
import { InfrastructureServiceClient } from '@/services/generated-rpc-clients';

// ---- Client + Circuit Breakers ----

const client = new InfrastructureServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) });
const outageBreaker = createCircuitBreaker<ListInternetOutagesResponse>({ name: 'Internet Outages', cacheTtlMs: 30 * 60 * 1000, persistCache: true });
const statusBreaker = createCircuitBreaker<ListServiceStatusesResponse>({ name: 'Service Statuses', cacheTtlMs: 30 * 60 * 1000, persistCache: true });
const ddosBreaker = createCircuitBreaker<ListInternetDdosAttacksResponse>({ name: 'DDoS Attacks', cacheTtlMs: 30 * 60 * 1000, persistCache: true });
const trafficAnomaliesBreaker = createCircuitBreaker<ListInternetTrafficAnomaliesResponse>({ name: 'Traffic Anomalies', cacheTtlMs: 30 * 60 * 1000, persistCache: true });

const emptyOutageFallback: ListInternetOutagesResponse = { outages: [], pagination: undefined };
const emptyStatusFallback: ListServiceStatusesResponse = { statuses: [] };
const emptyDdosFallback: ListInternetDdosAttacksResponse = { protocol: [], vector: [], dateRangeStart: '', dateRangeEnd: '', topTargetLocations: [] };
const emptyAnomaliesFallback: ListInternetTrafficAnomaliesResponse = { anomalies: [], totalCount: 0 };

// ---- Proto enum -> legacy string adapters ----

const SEVERITY_REVERSE: Record<string, 'partial' | 'major' | 'total'> = {
  OUTAGE_SEVERITY_PARTIAL: 'partial',
  OUTAGE_SEVERITY_MAJOR: 'major',
  OUTAGE_SEVERITY_TOTAL: 'total',
};

const STATUS_REVERSE: Record<string, 'operational' | 'degraded' | 'outage' | 'unknown'> = {
  SERVICE_OPERATIONAL_STATUS_OPERATIONAL: 'operational',
  SERVICE_OPERATIONAL_STATUS_DEGRADED: 'degraded',
  SERVICE_OPERATIONAL_STATUS_PARTIAL_OUTAGE: 'degraded',
  SERVICE_OPERATIONAL_STATUS_MAJOR_OUTAGE: 'outage',
  SERVICE_OPERATIONAL_STATUS_MAINTENANCE: 'degraded',
  SERVICE_OPERATIONAL_STATUS_UNSPECIFIED: 'unknown',
};

// ---- Adapter: proto InternetOutage -> legacy InternetOutage ----

function toOutage(proto: ProtoOutage): InternetOutage {
  return {
    id: proto.id,
    title: proto.title,
    link: proto.link,
    description: proto.description,
    pubDate: proto.detectedAt ? new Date(proto.detectedAt) : new Date(),
    country: proto.country,
    region: proto.region || undefined,
    lat: proto.location?.latitude ?? 0,
    lon: proto.location?.longitude ?? 0,
    severity: SEVERITY_REVERSE[proto.severity] || 'partial',
    categories: proto.categories,
    cause: proto.cause || undefined,
    outageType: proto.outageType || undefined,
    endDate: proto.endedAt ? new Date(proto.endedAt) : undefined,
  };
}

// ========================================================================
// Internet Outages -- replaces src/services/outages.ts
// ========================================================================

let outagesConfigured: boolean | null = null;

export function isOutagesConfigured(): boolean | null {
  return outagesConfigured;
}

export async function fetchInternetOutages(): Promise<InternetOutage[]> {
  if (!isFeatureAvailable('internetOutages')) {
    outagesConfigured = false;
    return [];
  }

  const hydrated = getHydratedData('outages') as ListInternetOutagesResponse | undefined;
  const resp = (hydrated?.outages?.length ? hydrated : null) ?? await outageBreaker.execute(async () => {
    return client.listInternetOutages({
      country: '',
      start: 0,
      end: 0,
      pageSize: 0,
      cursor: '',
    });
  }, emptyOutageFallback, { shouldCache: (r) => r.outages.length > 0 });

  if (resp.outages.length === 0) {
    if (outagesConfigured === null) outagesConfigured = false;
    return [];
  }

  outagesConfigured = true;
  return resp.outages.map(toOutage);
}

export function getOutagesStatus(): string {
  return outageBreaker.getStatus();
}

// ========================================================================
// DDoS Attacks -- L3/L4 attack summaries from Cloudflare Radar
// ========================================================================

export async function fetchDdosAttacks(): Promise<ListInternetDdosAttacksResponse> {
  const hydrated = getHydratedData('ddosAttacks') as ListInternetDdosAttacksResponse | undefined;
  if (hydrated?.protocol?.length || hydrated?.vector?.length) return hydrated;

  return ddosBreaker.execute(async () => {
    return client.listInternetDdosAttacks({});
  }, emptyDdosFallback, { shouldCache: (r) => r.protocol.length > 0 || r.vector.length > 0 });
}

// ========================================================================
// Traffic Anomalies -- anomalous traffic patterns from Cloudflare Radar
// ========================================================================

export async function fetchTrafficAnomalies(country?: string): Promise<ListInternetTrafficAnomaliesResponse> {
  const hydrated = getHydratedData('trafficAnomalies') as ListInternetTrafficAnomaliesResponse | undefined;
  if (hydrated?.anomalies !== undefined && !country) return hydrated;

  return trafficAnomaliesBreaker.execute(async () => {
    return client.listInternetTrafficAnomalies({ country: country || '' });
  }, emptyAnomaliesFallback, { shouldCache: (r) => r.anomalies.length > 0 });
}

// ========================================================================
// Service Statuses -- replaces direct /api/service-status fetch
// ========================================================================

export interface ServiceStatusResult {
  id: string;
  name: string;
  category: string;
  status: 'operational' | 'degraded' | 'outage' | 'unknown';
  description: string;
}

export interface ServiceStatusSummary {
  operational: number;
  degraded: number;
  outage: number;
  unknown: number;
}

export interface ServiceStatusResponse {
  success: boolean;
  timestamp: string;
  summary: ServiceStatusSummary;
  services: ServiceStatusResult[];
}

// Category map for the service IDs (matches the handler's SERVICES list)
const CATEGORY_MAP: Record<string, string> = {
  aws: 'cloud', azure: 'cloud', gcp: 'cloud', cloudflare: 'cloud', vercel: 'cloud',
  netlify: 'cloud', digitalocean: 'cloud', render: 'cloud', railway: 'cloud',
  github: 'dev', gitlab: 'dev', npm: 'dev', docker: 'dev', bitbucket: 'dev',
  circleci: 'dev', jira: 'dev', confluence: 'dev', linear: 'dev',
  slack: 'comm', discord: 'comm', zoom: 'comm', notion: 'comm',
  openai: 'ai', anthropic: 'ai', replicate: 'ai',
  stripe: 'saas', twilio: 'saas', datadog: 'saas', sentry: 'saas', supabase: 'saas',
};

function toServiceResult(proto: ProtoServiceStatus): ServiceStatusResult {
  return {
    id: proto.id,
    name: proto.name,
    category: CATEGORY_MAP[proto.id] || 'saas',
    status: STATUS_REVERSE[proto.status] || 'unknown',
    description: proto.description,
  };
}

function computeSummary(services: ServiceStatusResult[]): ServiceStatusSummary {
  return {
    operational: services.filter((s) => s.status === 'operational').length,
    degraded: services.filter((s) => s.status === 'degraded').length,
    outage: services.filter((s) => s.status === 'outage').length,
    unknown: services.filter((s) => s.status === 'unknown').length,
  };
}

export async function fetchServiceStatuses(): Promise<ServiceStatusResponse> {
  const hydrated = getHydratedData('serviceStatuses') as { statuses?: ProtoServiceStatus[] } | undefined;
  if (hydrated?.statuses?.length) {
    const services = hydrated.statuses.map(toServiceResult);
    return { success: true, timestamp: new Date().toISOString(), summary: computeSummary(services), services };
  }

  const resp = await statusBreaker.execute(async () => {
    return client.listServiceStatuses({
      status: 'SERVICE_OPERATIONAL_STATUS_UNSPECIFIED',
    });
  }, emptyStatusFallback, { shouldCache: (r) => r.statuses.length > 0 });

  const services = resp.statuses.map(toServiceResult);

  return {
    success: true,
    timestamp: new Date().toISOString(),
    summary: computeSummary(services),
    services,
  };
}