@agent-finops/core 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,20 +3,33 @@ import { redactSecrets } from "./discovery.js";
3
3
  export function normalizeOpenAiCostResponse(response, options) {
4
4
  const data = isObject(response) && Array.isArray(response.data) ? response.data : [];
5
5
  const records = [];
6
- for (const bucket of data) {
7
- const startTime = typeof bucket.start_time === "number" ? bucket.start_time : 0;
6
+ for (const bucketValue of data) {
7
+ if (!isRecord(bucketValue))
8
+ continue;
9
+ const startTime = validEpochSeconds(bucketValue.start_time);
10
+ if (typeof startTime !== "number")
11
+ continue;
8
12
  const timestamp = new Date(startTime * 1000).toISOString();
9
- for (const result of bucket.results ?? []) {
13
+ const results = Array.isArray(bucketValue.results) ? bucketValue.results : [];
14
+ for (const resultValue of results) {
15
+ if (!isRecord(resultValue))
16
+ continue;
17
+ const amount = isRecord(resultValue.amount) ? resultValue.amount : undefined;
18
+ const currency = amount?.currency === undefined
19
+ ? "usd"
20
+ : typeof amount.currency === "string"
21
+ ? amount.currency.toLowerCase()
22
+ : undefined;
10
23
  // The live API returns amount.value as a decimal STRING (dollars);
11
24
  // accept both string and number.
12
- const amountUsd = result.amount?.currency?.toLowerCase() === "usd" || !result.amount?.currency
13
- ? parseDollarUsd(result.amount?.value)
25
+ const amountUsd = currency === "usd"
26
+ ? parseDollarUsd(amount?.value)
14
27
  : undefined;
15
28
  if (typeof amountUsd !== "number")
16
29
  continue;
17
- const lineItem = result.line_item ?? "OpenAI organization costs";
18
- const projectId = result.project_id ?? undefined;
19
- const apiKeyId = result.api_key_id ?? undefined;
30
+ const lineItem = stringValue(resultValue.line_item) ?? "OpenAI organization costs";
31
+ const projectId = stringValue(resultValue.project_id);
32
+ const apiKeyId = stringValue(resultValue.api_key_id);
20
33
  records.push({
21
34
  id: slugifySourceId(["openai-costs", String(startTime), projectId, apiKeyId, lineItem].filter(Boolean).join("-")),
22
35
  timestamp,
@@ -35,7 +48,8 @@ export function normalizeOpenAiCostResponse(response, options) {
35
48
  projectId,
36
49
  apiKeyId,
37
50
  providerCostType: "openai_cost",
38
- quantity: typeof result.quantity === "number" ? result.quantity : undefined,
51
+ usageGranularity: "billing_bucket",
52
+ quantity: nonNegativeNumberValue(resultValue.quantity),
39
53
  operation: lineItem
40
54
  });
41
55
  }
@@ -53,12 +67,12 @@ export function normalizeOpenAiUsageResponse(response, options) {
53
67
  const userId = result.user_id ?? undefined;
54
68
  const apiKeyId = result.api_key_id ?? undefined;
55
69
  const model = result.model ?? "openai-usage";
56
- const inputTokens = numberValue(result.input_tokens) ?? 0;
57
- const outputTokens = numberValue(result.output_tokens) ?? 0;
58
- const cachedTokens = numberValue(result.input_cached_tokens) ?? 0;
59
- const audioInputTokens = numberValue(result.input_audio_tokens) ?? 0;
60
- const audioOutputTokens = numberValue(result.output_audio_tokens) ?? 0;
61
- const requestCount = numberValue(result.num_model_requests);
70
+ const inputTokens = nonNegativeIntegerValue(result.input_tokens) ?? 0;
71
+ const outputTokens = nonNegativeIntegerValue(result.output_tokens) ?? 0;
72
+ const cachedTokens = nonNegativeIntegerValue(result.input_cached_tokens) ?? 0;
73
+ const audioInputTokens = nonNegativeIntegerValue(result.input_audio_tokens) ?? 0;
74
+ const audioOutputTokens = nonNegativeIntegerValue(result.output_audio_tokens) ?? 0;
75
+ const requestCount = nonNegativeIntegerValue(result.num_model_requests);
62
76
  if (inputTokens + outputTokens + audioInputTokens + audioOutputTokens === 0 && typeof requestCount !== "number")
63
77
  continue;
64
78
  records.push({
@@ -74,7 +88,8 @@ export function normalizeOpenAiUsageResponse(response, options) {
74
88
  userId,
75
89
  apiKeyId,
76
90
  providerCostType: "openai_usage_evidence",
77
- quantity: numberValue(result.num_model_requests),
91
+ usageGranularity: "usage_bucket",
92
+ quantity: requestCount,
78
93
  operation: "OpenAI completions usage evidence"
79
94
  });
80
95
  }
@@ -92,11 +107,11 @@ export function normalizeAnthropicClaudeCodeUsageResponse(response, options) {
92
107
  const userId = stringValue(actor.email_address) ?? stringValue(actor.api_key_name) ?? stringValue(actor.id) ?? "unknown-claude-code-actor";
93
108
  const core = isRecord(row.core_metrics) ? row.core_metrics : {};
94
109
  const lines = isRecord(core.lines_of_code) ? core.lines_of_code : {};
95
- const sessions = numberValue(core.num_sessions) ?? 0;
96
- const added = numberValue(lines.added) ?? 0;
97
- const removed = numberValue(lines.removed) ?? 0;
98
- const commits = numberValue(core.commits_by_claude_code) ?? 0;
99
- const prs = numberValue(core.pull_requests_by_claude_code) ?? 0;
110
+ const sessions = nonNegativeIntegerValue(core.num_sessions) ?? 0;
111
+ const added = nonNegativeIntegerValue(lines.added) ?? 0;
112
+ const removed = nonNegativeIntegerValue(lines.removed) ?? 0;
113
+ const commits = nonNegativeIntegerValue(core.commits_by_claude_code) ?? 0;
114
+ const prs = nonNegativeIntegerValue(core.pull_requests_by_claude_code) ?? 0;
100
115
  const organizationId = stringValue(row.organization_id) ?? options.accountId;
101
116
  const modelBreakdown = Array.isArray(row.model_breakdown) ? row.model_breakdown : [];
102
117
  for (const item of modelBreakdown) {
@@ -114,13 +129,14 @@ export function normalizeAnthropicClaudeCodeUsageResponse(response, options) {
114
129
  timestamp: new Date(`${date}T00:00:00Z`).toISOString(),
115
130
  source: { id: options.sourceId, name: "Anthropic Claude Code Usage Report", provider: "anthropic", confidence: "estimated", observedFrom: options.observedFrom },
116
131
  model,
117
- inputTokens: (numberValue(tokens.input) ?? 0) + (numberValue(tokens.cache_read) ?? 0) + (numberValue(tokens.cache_creation) ?? 0),
118
- outputTokens: numberValue(tokens.output) ?? 0,
132
+ inputTokens: (nonNegativeIntegerValue(tokens.input) ?? 0) + (nonNegativeIntegerValue(tokens.cache_read) ?? 0) + (nonNegativeIntegerValue(tokens.cache_creation) ?? 0),
133
+ outputTokens: nonNegativeIntegerValue(tokens.output) ?? 0,
119
134
  amountUsd,
120
135
  costConfidence: "estimated",
121
136
  userId,
122
137
  projectId: organizationId,
123
138
  providerCostType: "anthropic_claude_code_usage",
139
+ usageGranularity: "daily_aggregate",
124
140
  quantity: sessions,
125
141
  operation: `Claude Code sessions: ${sessions}; LOC +${added}/-${removed}; commits ${commits}; PRs ${prs}`
126
142
  });
@@ -130,8 +146,6 @@ export function normalizeAnthropicClaudeCodeUsageResponse(response, options) {
130
146
  }
131
147
  export function normalizeGitHubCopilotSeatResponse(response, options) {
132
148
  const seats = extractArray(response, "seats");
133
- const plan = stringValue(isRecord(response) ? response.plan_type : undefined) ?? "business";
134
- const seatUsd = plan === "enterprise" ? 39 : 19;
135
149
  const timestamp = new Date().toISOString();
136
150
  return seats.flatMap((seat) => {
137
151
  if (!isRecord(seat))
@@ -140,6 +154,12 @@ export function normalizeGitHubCopilotSeatResponse(response, options) {
140
154
  const userId = stringValue(assignee.login) ?? stringValue(assignee.email) ?? stringValue(seat.login) ?? stringValue(seat.id);
141
155
  if (!userId)
142
156
  return [];
157
+ // The current GitHub seat schema reports plan_type on each seat. Never
158
+ // inherit a top-level value: an enterprise can contain mixed Business and
159
+ // Enterprise organizations, and an unknown tier is evidence, not $19.
160
+ const reportedPlan = stringValue(seat.plan_type)?.toLowerCase();
161
+ const plan = reportedPlan === "business" || reportedPlan === "enterprise" ? reportedPlan : "unknown";
162
+ const seatUsd = plan === "enterprise" ? 39 : plan === "business" ? 19 : null;
143
163
  const lastActivity = stringValue(seat.last_activity_at);
144
164
  return [{
145
165
  id: slugifySourceId(["github-copilot-seat", options.accountId, userId, plan].filter(Boolean).join("-")),
@@ -149,10 +169,11 @@ export function normalizeGitHubCopilotSeatResponse(response, options) {
149
169
  inputTokens: 0,
150
170
  outputTokens: 0,
151
171
  amountUsd: seatUsd,
152
- costConfidence: "estimated",
172
+ costConfidence: seatUsd === null ? "missing" : "estimated",
153
173
  userId,
154
174
  projectId: options.accountId,
155
175
  providerCostType: "copilot_seat_reconciliation",
176
+ usageGranularity: "seat",
156
177
  quantity: 1,
157
178
  operation: `GitHub Copilot ${plan} seat; ${lastActivity ? `last activity ${lastActivity}` : "no recent activity reported"}`
158
179
  }];
@@ -161,20 +182,33 @@ export function normalizeGitHubCopilotSeatResponse(response, options) {
161
182
  export function normalizeAnthropicCostResponse(response, options) {
162
183
  const data = isObject(response) && Array.isArray(response.data) ? response.data : [];
163
184
  const records = [];
164
- for (const bucket of data) {
165
- const timestamp = bucket.starting_at ?? new Date(0).toISOString();
166
- for (const result of bucket.results ?? []) {
167
- const currency = result.currency?.toLowerCase() ?? "usd";
185
+ for (const bucketValue of data) {
186
+ if (!isRecord(bucketValue))
187
+ continue;
188
+ const timestamp = validDateTimeString(bucketValue.starting_at);
189
+ if (!timestamp)
190
+ continue;
191
+ const results = Array.isArray(bucketValue.results) ? bucketValue.results : [];
192
+ for (const resultValue of results) {
193
+ if (!isRecord(resultValue))
194
+ continue;
195
+ const currency = resultValue.currency === undefined
196
+ ? "usd"
197
+ : typeof resultValue.currency === "string"
198
+ ? resultValue.currency.toLowerCase()
199
+ : undefined;
168
200
  if (currency !== "usd")
169
201
  continue;
170
- const amountUsd = parseMinorUsd(result.amount);
202
+ const amountUsd = parseMinorUsd(resultValue.amount);
171
203
  if (typeof amountUsd !== "number")
172
204
  continue;
173
- const description = result.description ?? result.cost_type ?? "Anthropic organization costs";
174
- const model = result.model ?? description;
175
- const workspaceId = result.workspace_id ?? undefined;
205
+ const costType = stringValue(resultValue.cost_type);
206
+ const description = stringValue(resultValue.description) ?? costType ?? "Anthropic organization costs";
207
+ const model = stringValue(resultValue.model) ?? description;
208
+ const workspaceId = stringValue(resultValue.workspace_id);
209
+ const tokenType = stringValue(resultValue.token_type);
176
210
  records.push({
177
- id: slugifySourceId(["anthropic-costs", timestamp, workspaceId, model, result.token_type ?? result.cost_type].filter(Boolean).join("-")),
211
+ id: slugifySourceId(["anthropic-costs", timestamp, workspaceId, model, tokenType ?? costType].filter(Boolean).join("-")),
178
212
  timestamp: new Date(timestamp).toISOString(),
179
213
  source: {
180
214
  id: options.sourceId,
@@ -190,7 +224,8 @@ export function normalizeAnthropicCostResponse(response, options) {
190
224
  costConfidence: "verified",
191
225
  projectId: workspaceId,
192
226
  workspaceId,
193
- providerCostType: result.cost_type ?? "anthropic_cost",
227
+ providerCostType: costType ?? "anthropic_cost",
228
+ usageGranularity: "billing_bucket",
194
229
  operation: description
195
230
  });
196
231
  }
@@ -222,6 +257,7 @@ export function normalizeGitHubCopilotMetricsResponse(response, options) {
222
257
  costConfidence: "missing",
223
258
  projectId: options.accountId,
224
259
  providerCostType: "copilot_usage_metrics",
260
+ usageGranularity: "daily_aggregate",
225
261
  operation: feature
226
262
  });
227
263
  }
@@ -233,12 +269,13 @@ export function normalizeGitHubCopilotMetricsResponse(response, options) {
233
269
  timestamp,
234
270
  source: { id: options.sourceId, name: "GitHub Copilot metrics API", provider: "github-copilot", confidence: "verified", observedFrom: options.observedFrom },
235
271
  model: "github-copilot-cli",
236
- inputTokens: numberValue(tokenUsage?.prompt_tokens_sum) ?? 0,
237
- outputTokens: numberValue(tokenUsage?.output_tokens_sum) ?? 0,
272
+ inputTokens: nonNegativeIntegerValue(tokenUsage?.prompt_tokens_sum) ?? 0,
273
+ outputTokens: nonNegativeIntegerValue(tokenUsage?.output_tokens_sum) ?? 0,
238
274
  amountUsd: null,
239
275
  costConfidence: "missing",
240
276
  projectId: options.accountId,
241
277
  providerCostType: "copilot_cli_metrics",
278
+ usageGranularity: "daily_aggregate",
242
279
  operation: "CLI requests"
243
280
  });
244
281
  }
@@ -246,13 +283,14 @@ export function normalizeGitHubCopilotMetricsResponse(response, options) {
246
283
  return records;
247
284
  }
248
285
  export function normalizeCursorSpendResponse(response, options) {
249
- const users = extractArray(response, "users").length > 0 ? extractArray(response, "users") : extractArray(response, "data");
250
- const timestamp = new Date().toISOString();
286
+ const users = extractArray(response, "teamMemberSpend");
287
+ const cycleStart = isRecord(response) ? numberValue(response.subscriptionCycleStart) : undefined;
288
+ const timestamp = typeof cycleStart === "number" ? new Date(cycleStart).toISOString() : new Date().toISOString();
251
289
  return users.flatMap((user) => {
252
290
  if (!isRecord(user))
253
291
  return [];
254
- const userId = stringValue(user.email) ?? stringValue(user.emailAddress) ?? stringValue(user.userId) ?? stringValue(user.id);
255
- const cents = numberValue(user.spendCents) ?? numberValue(user.usageBasedCents) ?? numberValue(user.chargedCents);
292
+ const userId = stringValue(user.email) ?? stringValue(user.userId);
293
+ const cents = numberValue(user.spendCents);
256
294
  if (!userId || typeof cents !== "number")
257
295
  return [];
258
296
  return [{
@@ -270,6 +308,7 @@ export function normalizeCursorSpendResponse(response, options) {
270
308
  userId,
271
309
  projectId: options.accountId,
272
310
  providerCostType: "cursor_spend",
311
+ usageGranularity: "user_aggregate",
273
312
  operation: "Cursor team spend"
274
313
  }];
275
314
  });
@@ -278,19 +317,75 @@ export async function fetchProviderUsageRecords(input) {
278
317
  const token = (input.tokenResolver ?? defaultTokenResolver)(input.authReference);
279
318
  const fetcher = input.fetcher ?? defaultFetcher;
280
319
  const sourceId = input.sourceId ?? `${input.provider}-provider-api`;
281
- if (input.provider === "openai") {
282
- return fetchOpenAi(input, token, fetcher, sourceId);
320
+ const credentialVariants = resolvedCredentialVariants(input.provider, token);
321
+ try {
322
+ if (input.provider === "openai") {
323
+ return redactResolvedCredentialValue(await fetchOpenAi(input, token, fetcher, sourceId), credentialVariants);
324
+ }
325
+ if (input.provider === "anthropic") {
326
+ return redactResolvedCredentialValue(await fetchAnthropic(input, token, fetcher, sourceId), credentialVariants);
327
+ }
328
+ if (input.provider === "github-copilot") {
329
+ return redactResolvedCredentialValue(await fetchGitHubCopilot(input, token, fetcher, sourceId), credentialVariants);
330
+ }
331
+ if (input.provider === "cursor") {
332
+ return redactResolvedCredentialValue(await fetchCursor(input, token, fetcher, sourceId), credentialVariants);
333
+ }
334
+ throw new Error(`Provider connector not implemented yet: ${input.provider}`);
335
+ }
336
+ catch (error) {
337
+ // This is the shared credential boundary for CLI, MCP, and future hosts.
338
+ // Provider payloads, status text, fetch implementations, and validation
339
+ // errors are all untrusted after a credential has been resolved. Exact
340
+ // replacement covers opaque tokens that do not match a known key shape.
341
+ throw redactResolvedCredentialError(error, credentialVariants);
342
+ }
343
+ }
344
+ function resolvedCredentialVariants(provider, token) {
345
+ const values = token ? [token] : [];
346
+ if (provider === "cursor" && token) {
347
+ try {
348
+ const encoded = btoaCompat(`${token}:`);
349
+ const unpadded = encoded.replace(/=+$/g, "");
350
+ const base64Url = unpadded.replace(/\+/g, "-").replace(/\//g, "_");
351
+ values.push(encoded, unpadded, base64Url, `Basic ${encoded}`, `Basic ${unpadded}`, `Basic ${base64Url}`);
352
+ }
353
+ catch {
354
+ // Cursor's request will fail on the same unsupported credential. Keep the
355
+ // raw value in the redaction set so that failure is still safe to return.
356
+ }
283
357
  }
284
- if (input.provider === "anthropic") {
285
- return fetchAnthropic(input, token, fetcher, sourceId);
358
+ const encodedValues = values.flatMap((value) => {
359
+ const encoded = encodeURIComponent(value);
360
+ return encoded === value ? [value] : [value, encoded];
361
+ });
362
+ return Array.from(new Set(encodedValues)).sort((left, right) => right.length - left.length);
363
+ }
364
+ function exactRedactCredentialValues(value, credentialVariants) {
365
+ return credentialVariants.reduce((safeValue, credential) => safeValue.split(credential).join("[REDACTED]"), value);
366
+ }
367
+ function redactResolvedCredentialError(error, credentialVariants) {
368
+ const rawMessage = error instanceof Error ? error.message : String(error);
369
+ const withoutResolvedCredential = exactRedactCredentialValues(rawMessage, credentialVariants);
370
+ // Strip controls before a second exact-redaction pass: an adversarial
371
+ // provider can splice ANSI bytes through an opaque credential so the first
372
+ // literal replacement misses it and control stripping reconstructs it.
373
+ const safeMessage = exactRedactCredentialValues(sanitizeProviderMessage(withoutResolvedCredential), credentialVariants).trim();
374
+ return new Error(safeMessage || "Provider connector request failed without a safe error message.");
375
+ }
376
+ function redactResolvedCredentialValue(value, credentialVariants) {
377
+ if (typeof value === "string") {
378
+ const withoutResolvedCredential = exactRedactCredentialValues(value, credentialVariants);
379
+ return exactRedactCredentialValues(sanitizeProviderMessage(withoutResolvedCredential), credentialVariants);
286
380
  }
287
- if (input.provider === "github-copilot") {
288
- return fetchGitHubCopilot(input, token, fetcher, sourceId);
381
+ if (Array.isArray(value)) {
382
+ return value.map((item) => redactResolvedCredentialValue(item, credentialVariants));
289
383
  }
290
- if (input.provider === "cursor") {
291
- return fetchCursor(input, token, fetcher, sourceId);
384
+ if (value && typeof value === "object") {
385
+ return Object.fromEntries(Object.entries(value)
386
+ .map(([key, item]) => [key, redactResolvedCredentialValue(item, credentialVariants)]));
292
387
  }
293
- throw new Error(`Provider connector not implemented yet: ${input.provider}`);
388
+ return value;
294
389
  }
295
390
  async function fetchOpenAi(input, token, fetcher, sourceId) {
296
391
  const request = {
@@ -298,7 +393,9 @@ async function fetchOpenAi(input, token, fetcher, sourceId) {
298
393
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
299
394
  };
300
395
  const costFetch = await fetchPaginatedJson(fetcher, buildOpenAiCostsUrl(input.startTime, input.endTime), request, "openai", "OpenAI costs API");
396
+ markMalformedCostRows(costFetch, "openai", "OpenAI costs API");
301
397
  const usageFetch = await fetchPaginatedJson(fetcher, buildOpenAiUsageUrl(input.startTime, input.endTime), request, "openai", "OpenAI usage API");
398
+ markMalformedUsageRows(usageFetch, "openai", "OpenAI usage API");
302
399
  const records = [
303
400
  ...costFetch.pages.flatMap((page) => normalizeOpenAiCostResponse(page, { sourceId, observedFrom: "OpenAI organization costs API" })),
304
401
  ...usageFetch.pages.flatMap((page) => normalizeOpenAiUsageResponse(page, { sourceId, observedFrom: "OpenAI organization usage API" }))
@@ -311,7 +408,11 @@ async function fetchAnthropic(input, token, fetcher, sourceId) {
311
408
  headers: { "x-api-key": token, "anthropic-version": "2023-06-01", "Content-Type": "application/json" }
312
409
  };
313
410
  const costFetch = await fetchPaginatedJson(fetcher, buildAnthropicCostUrl(input.startTime, input.endTime), costRequest, "anthropic", "Anthropic Admin cost report");
411
+ markMalformedCostRows(costFetch, "anthropic", "Anthropic Admin cost report");
314
412
  const claudeCodeFetches = await fetchDateRangeJson(fetcher, buildAnthropicClaudeCodeUrl, input.startTime, input.endTime, costRequest, "anthropic", "Anthropic Claude Code usage report");
413
+ for (const fetchResult of claudeCodeFetches) {
414
+ markMalformedUsageRows(fetchResult, "anthropic", "Anthropic Claude Code usage report");
415
+ }
315
416
  const records = [
316
417
  ...costFetch.pages.flatMap((page) => normalizeAnthropicCostResponse(page, { sourceId, observedFrom: "Anthropic Admin Cost Report" })),
317
418
  ...claudeCodeFetches.flatMap((fetchResult) => fetchResult.pages.flatMap((page) => normalizeAnthropicClaudeCodeUsageResponse(page, { sourceId, observedFrom: "Anthropic Claude Code Usage Report", accountId: input.accountId })))
@@ -324,38 +425,230 @@ async function fetchGitHubCopilot(input, token, fetcher, sourceId) {
324
425
  throw new Error("GitHub Copilot connector requires --org or --enterprise.");
325
426
  const request = {
326
427
  method: "GET",
327
- headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" }
428
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2026-03-10" }
328
429
  };
329
- const metricsFetch = await fetchPaginatedJson(fetcher, buildGitHubCopilotMetricsUrl(input), request, "github-copilot", "GitHub Copilot metrics");
430
+ const metricsManifestResponse = await fetchJsonOrThrow(fetcher, buildGitHubCopilotMetricsUrl(input), request, "github-copilot", "GitHub Copilot metrics manifest");
431
+ const metricsManifest = metricsManifestResponse.payload;
432
+ const metricsDownloadLinks = requireStringArray(metricsManifest, "download_links", "GitHub Copilot metrics manifest");
433
+ const metricsFetch = await fetchGitHubCopilotSignedReports(fetcher, metricsDownloadLinks, metricsManifest, metricsManifestResponse.rateLimit);
434
+ markMalformedUsageRows(metricsFetch, "github-copilot", "GitHub Copilot metrics reports");
330
435
  const seatFetch = input.org ? await fetchPaginatedJson(fetcher, buildGitHubCopilotSeatsUrl(input.org), request, "github-copilot", "GitHub Copilot seats") : undefined;
436
+ if (seatFetch)
437
+ assessGitHubCopilotSeatCompleteness(seatFetch);
331
438
  const metricsRecords = metricsFetch.pages.flatMap((page) => normalizeGitHubCopilotMetricsResponse(page, { sourceId, observedFrom: "GitHub Copilot metrics API", accountId }));
332
439
  const seatRecords = seatFetch ? seatFetch.pages.flatMap((page) => normalizeGitHubCopilotSeatResponse(page, { sourceId, observedFrom: "GitHub Copilot billing seats API", accountId })) : [];
333
440
  return providerResult("github-copilot", sourceId, input.authReference, [...metricsRecords, ...seatRecords], qaSummary("github-copilot", [metricsFetch, ...(seatFetch ? [seatFetch] : [])]));
334
441
  }
335
442
  async function fetchCursor(input, token, fetcher, sourceId) {
336
443
  const accountId = input.accountId ?? input.org ?? "cursor-team";
337
- const response = await fetchJsonOrThrow(fetcher, "https://api.cursor.com/teams/spend", {
338
- method: "POST",
339
- headers: { Authorization: `Basic ${btoaCompat(`${token}:`)}`, "Content-Type": "application/json" },
340
- body: JSON.stringify({})
341
- }, "cursor", "Cursor Admin API spend");
342
- const page = response.payload;
343
- const records = normalizeCursorSpendResponse(page, { sourceId, observedFrom: "Cursor Admin API", accountId });
344
- // The Cursor connector is matched to the published spec but not live-verified.
345
- // If the API answered with content but no spend fields we recognize, say so
346
- // loudly rather than silently report $0 (which reads as "you spent nothing").
347
- if (records.length === 0 && isRecord(page) && Object.keys(page).length > 0) {
348
- throw new Error("Cursor returned data but no spend fields this connector recognizes " +
349
- `(saw: ${Object.keys(page).slice(0, 8).join(", ")}). The Cursor connector is beta — ` +
350
- "please open an issue with this field list so we can map it: https://github.com/futurastudio/ai-spend-agent/issues");
351
- }
352
- const singleFetch = {
353
- pages: [page],
354
- pagination: { label: "Cursor Admin API spend", pagesFetched: 1, stoppedBecause: "complete", maxPages: 1 },
355
- rateLimits: response.rateLimit ? [response.rateLimit] : [],
356
- responseDrift: detectResponseDrift(page, "cursor", "Cursor Admin API spend")
444
+ const spendFetch = await fetchCursorSpendPages(fetcher, token);
445
+ const records = spendFetch.pages.flatMap((page) => normalizeCursorSpendResponse(page, { sourceId, observedFrom: "Cursor Admin API", accountId }));
446
+ return providerResult("cursor", sourceId, input.authReference, records, qaSummary("cursor", [spendFetch]));
447
+ }
448
+ async function fetchCursorSpendPages(fetcher, token) {
449
+ const label = "Cursor Admin API spend";
450
+ const pages = [];
451
+ const rateLimits = [];
452
+ const responseDrift = [];
453
+ const maxPages = 50;
454
+ const pageSize = 100;
455
+ let expectedTotalPages;
456
+ let expectedTotalMembers;
457
+ let stoppedBecause = "complete";
458
+ let note;
459
+ for (let pageNumber = 1; pageNumber <= (expectedTotalPages ?? 1) && pageNumber <= maxPages; pageNumber += 1) {
460
+ let response;
461
+ try {
462
+ response = await fetchJsonOrThrow(fetcher, "https://api.cursor.com/teams/spend", {
463
+ method: "POST",
464
+ headers: { Authorization: `Basic ${btoaCompat(`${token}:`)}`, "Content-Type": "application/json" },
465
+ body: JSON.stringify({ page: pageNumber, pageSize })
466
+ }, "cursor", label);
467
+ }
468
+ catch (error) {
469
+ if (pages.length === 0)
470
+ throw error;
471
+ stoppedBecause = "fetch_error";
472
+ note = `Stopped after ${pages.length} page(s): ${sanitizeProviderMessage(error instanceof Error ? error.message : String(error))}`;
473
+ break;
474
+ }
475
+ const page = response.payload;
476
+ const pageMetadata = validateCursorSpendPage(page, pageNumber);
477
+ if (expectedTotalPages === undefined)
478
+ expectedTotalPages = pageMetadata.totalPages;
479
+ if (expectedTotalMembers === undefined)
480
+ expectedTotalMembers = pageMetadata.totalMembers;
481
+ if (pageMetadata.totalPages !== expectedTotalPages || pageMetadata.totalMembers !== expectedTotalMembers) {
482
+ stoppedBecause = "fetch_error";
483
+ note = `Cursor pagination metadata changed on page ${pageNumber} (totalPages ${expectedTotalPages}→${pageMetadata.totalPages}, totalMembers ${expectedTotalMembers}→${pageMetadata.totalMembers}); results may be incomplete.`;
484
+ pages.push(page);
485
+ responseDrift.push({ label, field: "totalPages/totalMembers", issue: note });
486
+ break;
487
+ }
488
+ pages.push(page);
489
+ if (response.rateLimit)
490
+ rateLimits.push(response.rateLimit);
491
+ responseDrift.push(...detectResponseDrift(page, "cursor", label));
492
+ }
493
+ if ((expectedTotalPages ?? 0) > maxPages && stoppedBecause === "complete") {
494
+ stoppedBecause = "max_pages";
495
+ note = `Cursor reported ${expectedTotalPages} pages, exceeding the ${maxPages}-page connector limit.`;
496
+ }
497
+ const fetchedMembers = pages.reduce((sum, page) => sum + extractArray(page, "teamMemberSpend").length, 0);
498
+ if (stoppedBecause === "complete" && typeof expectedTotalMembers === "number" && fetchedMembers !== expectedTotalMembers) {
499
+ stoppedBecause = "missing_cursor";
500
+ note = `Cursor reported ${expectedTotalMembers} members but returned ${fetchedMembers}; refusing to mark the sync complete.`;
501
+ responseDrift.push({ label, field: "totalMembers", issue: note });
502
+ }
503
+ return {
504
+ pages,
505
+ pagination: { label, pagesFetched: pages.length, stoppedBecause, maxPages, limitPerPage: pageSize, ...(note ? { note } : {}) },
506
+ rateLimits,
507
+ responseDrift
357
508
  };
358
- return providerResult("cursor", sourceId, input.authReference, records, qaSummary("cursor", [singleFetch]));
509
+ }
510
+ function validateCursorSpendPage(page, pageNumber) {
511
+ if (!isRecord(page) || !Array.isArray(page.teamMemberSpend)) {
512
+ const fields = isRecord(page) ? Object.keys(page).slice(0, 8).join(", ") : typeof page;
513
+ throw new Error(`Cursor spend page ${pageNumber} is missing canonical teamMemberSpend data (saw: ${fields}).`);
514
+ }
515
+ const totalPages = numberValue(page.totalPages);
516
+ if (typeof totalPages !== "number" || !Number.isInteger(totalPages) || totalPages < 1) {
517
+ throw new Error(`Cursor spend page ${pageNumber} has invalid or missing totalPages; completeness cannot be proven.`);
518
+ }
519
+ const totalMembers = numberValue(page.totalMembers);
520
+ if (typeof totalMembers !== "number" || !Number.isInteger(totalMembers) || totalMembers < 0) {
521
+ throw new Error(`Cursor spend page ${pageNumber} has invalid or missing totalMembers; completeness cannot be proven.`);
522
+ }
523
+ for (const [index, member] of page.teamMemberSpend.entries()) {
524
+ const spendCents = isRecord(member) ? numberValue(member.spendCents) : undefined;
525
+ if (!isRecord(member) || (!stringValue(member.email) && !stringValue(member.userId)) || typeof spendCents !== "number" || spendCents < 0) {
526
+ const fields = isRecord(member) ? Object.keys(member).slice(0, 8).join(", ") : typeof member;
527
+ throw new Error(`Cursor spend page ${pageNumber} member ${index + 1} is missing email/userId or a non-negative spendCents value (saw: ${fields}).`);
528
+ }
529
+ if (member.fastPremiumRequests !== undefined && typeof nonNegativeIntegerValue(member.fastPremiumRequests) !== "number") {
530
+ throw new Error(`Cursor spend page ${pageNumber} member ${index + 1} has an invalid fastPremiumRequests quantity; expected a non-negative integer.`);
531
+ }
532
+ }
533
+ return { totalPages, totalMembers };
534
+ }
535
+ async function fetchGitHubCopilotSignedReports(fetcher, downloadLinks, manifest, manifestRateLimit) {
536
+ const label = "GitHub Copilot metrics reports";
537
+ const maxReports = 100;
538
+ if (downloadLinks.length > maxReports) {
539
+ throw new Error(`GitHub Copilot metrics manifest returned ${downloadLinks.length} report files, exceeding the ${maxReports}-file safety limit.`);
540
+ }
541
+ const pages = [];
542
+ const responseDrift = detectResponseDrift(manifest, "github-copilot", "GitHub Copilot metrics manifest");
543
+ for (const [index, candidate] of downloadLinks.entries()) {
544
+ const safeUrl = validateSignedDownloadUrl(candidate);
545
+ if (!safeUrl) {
546
+ throw new Error(`GitHub Copilot metrics report ${index + 1} had an unsafe signed download URL; only public HTTPS URLs without embedded credentials are accepted.`);
547
+ }
548
+ const body = await fetchTextOrThrow(fetcher, safeUrl, {
549
+ method: "GET",
550
+ // Signed report URLs carry their own authorization. Never replay the
551
+ // GitHub bearer token to a storage host.
552
+ headers: { Accept: "application/x-ndjson, application/json" }
553
+ }, "github-copilot", `GitHub Copilot metrics report ${index + 1}`);
554
+ const reports = parseNdjsonReports(body, index + 1);
555
+ if (reports.length === 0) {
556
+ throw new Error(`GitHub Copilot metrics report ${index + 1} was empty; refusing to report a complete sync.`);
557
+ }
558
+ for (const report of reports) {
559
+ if (!isRecord(report) || !Array.isArray(report.day_totals)) {
560
+ const fields = isRecord(report) ? Object.keys(report).slice(0, 8).join(", ") : typeof report;
561
+ throw new Error(`GitHub Copilot metrics report ${index + 1} did not contain the documented day_totals wrapper (saw: ${fields}).`);
562
+ }
563
+ pages.push(report);
564
+ responseDrift.push(...detectResponseDrift(report, "github-copilot", label));
565
+ }
566
+ }
567
+ return {
568
+ pages,
569
+ pagination: { label, pagesFetched: downloadLinks.length, stoppedBecause: "complete", maxPages: maxReports },
570
+ rateLimits: manifestRateLimit ? [manifestRateLimit] : [],
571
+ responseDrift
572
+ };
573
+ }
574
+ async function fetchTextOrThrow(fetcher, url, request, provider, label) {
575
+ let lastError;
576
+ for (let attempt = 0; attempt <= maxFetchRetries; attempt += 1) {
577
+ const response = await fetcher(url, request);
578
+ if (response.ok) {
579
+ if (!response.text)
580
+ throw new Error(`${label} did not expose a readable NDJSON body.`);
581
+ return response.text();
582
+ }
583
+ const payload = await response.json().catch(() => undefined);
584
+ lastError = new Error(providerPermissionPrompt(provider, label, response, payload));
585
+ const retryable = response.status === 429 || response.status >= 500;
586
+ if (!retryable || attempt === maxFetchRetries)
587
+ break;
588
+ const retryAfterSeconds = headerNumber(response.headers, "retry-after");
589
+ const delayMs = typeof retryAfterSeconds === "number"
590
+ ? Math.min(Math.max(retryAfterSeconds, 0) * 1000, maxRetryDelayMs)
591
+ : 500 * 2 ** attempt;
592
+ if (delayMs > 0)
593
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs));
594
+ }
595
+ throw lastError ?? new Error(`${label} request failed.`);
596
+ }
597
+ function parseNdjsonReports(body, reportNumber) {
598
+ const reports = [];
599
+ const lines = body.split(/\r?\n/).filter((line) => line.trim().length > 0);
600
+ for (const [lineIndex, line] of lines.entries()) {
601
+ let parsed;
602
+ try {
603
+ parsed = JSON.parse(line);
604
+ }
605
+ catch {
606
+ throw new Error(`GitHub Copilot metrics report ${reportNumber} contains malformed NDJSON at line ${lineIndex + 1}.`);
607
+ }
608
+ if (Array.isArray(parsed))
609
+ reports.push(...parsed);
610
+ else
611
+ reports.push(parsed);
612
+ }
613
+ return reports;
614
+ }
615
+ function validateSignedDownloadUrl(candidate) {
616
+ try {
617
+ const url = new URL(candidate);
618
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
619
+ if (url.protocol !== "https:" || (url.port && url.port !== "443") || url.username || url.password)
620
+ return undefined;
621
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname === "::" || hostname === "::1" || hostname === "0:0:0:0:0:0:0:1")
622
+ return undefined;
623
+ if (/^0\./.test(hostname) || /^127\./.test(hostname) || /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^169\.254\./.test(hostname))
624
+ return undefined;
625
+ if (/^(?:fc|fd|fe[89ab])/i.test(hostname))
626
+ return undefined;
627
+ const private172 = hostname.match(/^172\.(\d+)\./);
628
+ if (private172 && Number(private172[1]) >= 16 && Number(private172[1]) <= 31)
629
+ return undefined;
630
+ return url.toString();
631
+ }
632
+ catch {
633
+ return undefined;
634
+ }
635
+ }
636
+ function assessGitHubCopilotSeatCompleteness(fetchResult) {
637
+ const expected = fetchResult.pages
638
+ .map((page) => isRecord(page) ? numberValue(page.total_seats) : undefined)
639
+ .find((value) => typeof value === "number");
640
+ const actual = fetchResult.pages.reduce((sum, page) => sum + extractArray(page, "seats").length, 0);
641
+ if (typeof expected !== "number") {
642
+ if (fetchResult.pagination.stoppedBecause === "complete")
643
+ fetchResult.pagination.stoppedBecause = "missing_cursor";
644
+ fetchResult.pagination.note = "GitHub Copilot seats response omitted total_seats; completeness cannot be proven.";
645
+ fetchResult.responseDrift.push({ label: "GitHub Copilot seats", field: "total_seats", issue: fetchResult.pagination.note });
646
+ }
647
+ else if (fetchResult.pagination.stoppedBecause === "complete" && actual !== expected) {
648
+ fetchResult.pagination.stoppedBecause = "missing_cursor";
649
+ fetchResult.pagination.note = `GitHub reported ${expected} seats but returned ${actual}; completeness cannot be proven.`;
650
+ fetchResult.responseDrift.push({ label: "GitHub Copilot seats", field: "total_seats", issue: fetchResult.pagination.note });
651
+ }
359
652
  }
360
653
  async function fetchPaginatedJson(fetcher, initialUrl, request, provider, label) {
361
654
  const pages = [];
@@ -566,6 +859,228 @@ function headerNumber(headers, name) {
566
859
  function hasHeaderGetter(headers) {
567
860
  return typeof headers?.get === "function";
568
861
  }
862
+ function markMalformedCostRows(fetchResult, provider, label) {
863
+ const issues = [];
864
+ const maxDetailedIssues = 25;
865
+ let issueCount = 0;
866
+ const report = (field, issue) => {
867
+ issueCount += 1;
868
+ if (issues.length < maxDetailedIssues)
869
+ issues.push({ label, field, issue });
870
+ };
871
+ for (const page of fetchResult.pages) {
872
+ if (!isRecord(page) || !Array.isArray(page.data)) {
873
+ report("data", "cost response omitted the canonical data array; no financial completeness claim is safe");
874
+ continue;
875
+ }
876
+ for (const [bucketIndex, bucketValue] of page.data.entries()) {
877
+ const bucketPath = `data[${bucketIndex}]`;
878
+ if (!isRecord(bucketValue)) {
879
+ report(bucketPath, "cost response contained a non-object billing bucket");
880
+ continue;
881
+ }
882
+ if (provider === "openai") {
883
+ if (typeof validEpochSeconds(bucketValue.start_time) !== "number") {
884
+ report(`${bucketPath}.start_time`, "cost bucket had an invalid timestamp and its rows were excluded");
885
+ continue;
886
+ }
887
+ }
888
+ else if (!validDateTimeString(bucketValue.starting_at)) {
889
+ report(`${bucketPath}.starting_at`, "cost bucket had an invalid timestamp and its rows were excluded");
890
+ continue;
891
+ }
892
+ if (!Array.isArray(bucketValue.results)) {
893
+ report(`${bucketPath}.results`, "cost bucket omitted the canonical results array");
894
+ continue;
895
+ }
896
+ for (const [resultIndex, resultValue] of bucketValue.results.entries()) {
897
+ const resultPath = `${bucketPath}.results[${resultIndex}]`;
898
+ if (!isRecord(resultValue)) {
899
+ report(resultPath, "cost response contained a non-object billed-cost row");
900
+ continue;
901
+ }
902
+ if (provider === "openai") {
903
+ const amount = isRecord(resultValue.amount) ? resultValue.amount : undefined;
904
+ if (!amount) {
905
+ report(`${resultPath}.amount`, "billed-cost row had no canonical amount object and was excluded");
906
+ continue;
907
+ }
908
+ if (amount.currency !== undefined && (typeof amount.currency !== "string" || amount.currency.toLowerCase() !== "usd")) {
909
+ report(`${resultPath}.amount.currency`, "billed-cost row used an invalid or unsupported currency and was excluded from the USD headline");
910
+ continue;
911
+ }
912
+ if (typeof parseDollarUsd(amount.value) !== "number") {
913
+ report(`${resultPath}.amount.value`, "billed-cost row had an invalid dollar amount and was excluded");
914
+ }
915
+ if (resultValue.quantity !== undefined && typeof nonNegativeNumberValue(resultValue.quantity) !== "number") {
916
+ report(`${resultPath}.quantity`, "billed-cost row had a negative or invalid quantity; the quantity was excluded");
917
+ }
918
+ continue;
919
+ }
920
+ if (resultValue.currency !== undefined && (typeof resultValue.currency !== "string" || resultValue.currency.toLowerCase() !== "usd")) {
921
+ report(`${resultPath}.currency`, "billed-cost row used an invalid or unsupported currency and was excluded from the USD headline");
922
+ continue;
923
+ }
924
+ if (typeof parseMinorUsd(resultValue.amount) !== "number") {
925
+ report(`${resultPath}.amount`, "billed-cost row had an invalid minor-unit amount and was excluded");
926
+ }
927
+ }
928
+ }
929
+ }
930
+ if (issueCount === 0)
931
+ return;
932
+ if (issueCount > maxDetailedIssues) {
933
+ issues.push({
934
+ label,
935
+ field: "data[].results[]",
936
+ issue: `${issueCount - maxDetailedIssues} additional malformed billed-cost schema issue(s) were omitted from QA details`
937
+ });
938
+ }
939
+ fetchResult.coverageIncomplete = true;
940
+ fetchResult.responseDrift.push(...issues);
941
+ }
942
+ /**
943
+ * Provider APIs are untrusted even after transport succeeds. A negative or
944
+ * fractional token count cannot satisfy UsageRecord's finance-grade schema,
945
+ * and a negative count/quantity cannot support a completeness claim. Keep any
946
+ * independently valid evidence, but mark the whole source pull partial and
947
+ * omit the invalid values during normalization.
948
+ */
949
+ function markMalformedUsageRows(fetchResult, provider, label) {
950
+ const issues = [];
951
+ const maxDetailedIssues = 25;
952
+ let issueCount = 0;
953
+ const report = (field, issue) => {
954
+ issueCount += 1;
955
+ if (issues.length < maxDetailedIssues)
956
+ issues.push({ label, field, issue });
957
+ };
958
+ const checkInteger = (value, field, kind) => {
959
+ if (value !== undefined && typeof nonNegativeIntegerValue(value) !== "number") {
960
+ report(field, `${kind} must be a non-negative integer; the invalid value was excluded`);
961
+ }
962
+ };
963
+ for (const page of fetchResult.pages) {
964
+ if (provider === "openai") {
965
+ if (!isRecord(page) || !Array.isArray(page.data)) {
966
+ report("data", "usage response omitted the canonical data array; completeness cannot be proven");
967
+ continue;
968
+ }
969
+ for (const [bucketIndex, bucketValue] of page.data.entries()) {
970
+ const bucketPath = `data[${bucketIndex}]`;
971
+ if (!isRecord(bucketValue) || !Array.isArray(bucketValue.results)) {
972
+ report(bucketPath, "usage response contained a malformed bucket or omitted its results array");
973
+ continue;
974
+ }
975
+ for (const [resultIndex, resultValue] of bucketValue.results.entries()) {
976
+ const resultPath = `${bucketPath}.results[${resultIndex}]`;
977
+ if (!isRecord(resultValue)) {
978
+ report(resultPath, "usage response contained a non-object usage row");
979
+ continue;
980
+ }
981
+ for (const field of [
982
+ "input_tokens",
983
+ "input_uncached_tokens",
984
+ "input_cache_write_tokens",
985
+ "input_cached_tokens",
986
+ "input_text_tokens",
987
+ "input_image_tokens",
988
+ "input_audio_tokens",
989
+ "input_cached_text_tokens",
990
+ "input_cached_image_tokens",
991
+ "input_cached_audio_tokens",
992
+ "output_tokens",
993
+ "output_text_tokens",
994
+ "output_image_tokens",
995
+ "output_audio_tokens"
996
+ ]) {
997
+ checkInteger(resultValue[field], `${resultPath}.${field}`, "token count");
998
+ }
999
+ checkInteger(resultValue.num_model_requests, `${resultPath}.num_model_requests`, "quantity");
1000
+ }
1001
+ }
1002
+ continue;
1003
+ }
1004
+ if (provider === "anthropic") {
1005
+ if (!isRecord(page) || !Array.isArray(page.data)) {
1006
+ report("data", "Claude Code usage response omitted the canonical data array; completeness cannot be proven");
1007
+ continue;
1008
+ }
1009
+ for (const [rowIndex, rowValue] of page.data.entries()) {
1010
+ const rowPath = `data[${rowIndex}]`;
1011
+ if (!isRecord(rowValue)) {
1012
+ report(rowPath, "Claude Code usage response contained a non-object row");
1013
+ continue;
1014
+ }
1015
+ const core = isRecord(rowValue.core_metrics) ? rowValue.core_metrics : {};
1016
+ const lines = isRecord(core.lines_of_code) ? core.lines_of_code : {};
1017
+ checkInteger(core.num_sessions, `${rowPath}.core_metrics.num_sessions`, "quantity");
1018
+ checkInteger(lines.added, `${rowPath}.core_metrics.lines_of_code.added`, "quantity");
1019
+ checkInteger(lines.removed, `${rowPath}.core_metrics.lines_of_code.removed`, "quantity");
1020
+ checkInteger(core.commits_by_claude_code, `${rowPath}.core_metrics.commits_by_claude_code`, "quantity");
1021
+ checkInteger(core.pull_requests_by_claude_code, `${rowPath}.core_metrics.pull_requests_by_claude_code`, "quantity");
1022
+ const modelBreakdown = Array.isArray(rowValue.model_breakdown) ? rowValue.model_breakdown : [];
1023
+ for (const [modelIndex, modelValue] of modelBreakdown.entries()) {
1024
+ const modelPath = `${rowPath}.model_breakdown[${modelIndex}]`;
1025
+ if (!isRecord(modelValue)) {
1026
+ report(modelPath, "Claude Code usage response contained a non-object model row");
1027
+ continue;
1028
+ }
1029
+ const tokens = isRecord(modelValue.tokens) ? modelValue.tokens : {};
1030
+ for (const field of ["input", "output", "cache_read", "cache_creation"]) {
1031
+ checkInteger(tokens[field], `${modelPath}.tokens.${field}`, "token count");
1032
+ }
1033
+ }
1034
+ }
1035
+ continue;
1036
+ }
1037
+ if (!isRecord(page) || !Array.isArray(page.day_totals)) {
1038
+ report("day_totals", "Copilot metrics response omitted the canonical day_totals array; completeness cannot be proven");
1039
+ continue;
1040
+ }
1041
+ for (const [dayIndex, dayValue] of page.day_totals.entries()) {
1042
+ const dayPath = `day_totals[${dayIndex}]`;
1043
+ if (!isRecord(dayValue)) {
1044
+ report(dayPath, "Copilot metrics response contained a non-object day row");
1045
+ continue;
1046
+ }
1047
+ checkInteger(dayValue.daily_active_users, `${dayPath}.daily_active_users`, "quantity");
1048
+ const featureRows = Array.isArray(dayValue.totals_by_model_feature) ? dayValue.totals_by_model_feature : [];
1049
+ for (const [featureIndex, featureValue] of featureRows.entries()) {
1050
+ if (!isRecord(featureValue)) {
1051
+ report(`${dayPath}.totals_by_model_feature[${featureIndex}]`, "Copilot metrics response contained a non-object feature row");
1052
+ continue;
1053
+ }
1054
+ const featurePath = `${dayPath}.totals_by_model_feature[${featureIndex}]`;
1055
+ for (const field of ["engaged_users", "total_requests", "user_initiated_interaction_count"]) {
1056
+ checkInteger(featureValue[field], `${featurePath}.${field}`, "quantity");
1057
+ }
1058
+ }
1059
+ const cli = isRecord(dayValue.totals_by_cli) ? dayValue.totals_by_cli : undefined;
1060
+ if (!cli)
1061
+ continue;
1062
+ for (const field of ["request_count", "prompt_count", "session_count", "engaged_users", "total_requests"]) {
1063
+ checkInteger(cli[field], `${dayPath}.totals_by_cli.${field}`, "quantity");
1064
+ }
1065
+ const tokenUsage = isRecord(cli.token_usage) ? cli.token_usage : undefined;
1066
+ if (!tokenUsage)
1067
+ continue;
1068
+ checkInteger(tokenUsage.prompt_tokens_sum, `${dayPath}.totals_by_cli.token_usage.prompt_tokens_sum`, "token count");
1069
+ checkInteger(tokenUsage.output_tokens_sum, `${dayPath}.totals_by_cli.token_usage.output_tokens_sum`, "token count");
1070
+ }
1071
+ }
1072
+ if (issueCount === 0)
1073
+ return;
1074
+ if (issueCount > maxDetailedIssues) {
1075
+ issues.push({
1076
+ label,
1077
+ field: "usage rows",
1078
+ issue: `${issueCount - maxDetailedIssues} additional malformed usage schema issue(s) were omitted from QA details`
1079
+ });
1080
+ }
1081
+ fetchResult.coverageIncomplete = true;
1082
+ fetchResult.responseDrift.push(...issues);
1083
+ }
569
1084
  function detectResponseDrift(payload, provider, label) {
570
1085
  const known = knownProviderFields(provider, label);
571
1086
  const issues = [];
@@ -607,20 +1122,20 @@ function knownProviderFields(provider, label) {
607
1122
  return new Set([...common, "data[].date", "data[].actor", "data[].actor.email_address", "data[].actor.api_key_name", "data[].actor.id", "data[].actor.type", "data[].organization_id", "data[].customer_type", "data[].terminal_type", "data[].subscription_type", "data[].core_metrics", "data[].core_metrics.num_sessions", "data[].core_metrics.lines_of_code", "data[].core_metrics.lines_of_code.added", "data[].core_metrics.lines_of_code.removed", "data[].core_metrics.commits_by_claude_code", "data[].core_metrics.pull_requests_by_claude_code", "data[].model_breakdown", "data[].model_breakdown[]", "data[].model_breakdown[].model", "data[].model_breakdown[].tokens", "data[].model_breakdown[].tokens.input", "data[].model_breakdown[].tokens.output", "data[].model_breakdown[].tokens.cache_read", "data[].model_breakdown[].tokens.cache_creation", "data[].model_breakdown[].estimated_cost", "data[].model_breakdown[].estimated_cost.currency", "data[].model_breakdown[].estimated_cost.amount", "data[].tool_actions", "data[].tool_actions[]"]);
608
1123
  }
609
1124
  if (provider === "github-copilot" && label.toLowerCase().includes("metrics")) {
610
- return new Set([...common, "day_totals", "day_totals[]", "day_totals[].day", "day_totals[].daily_active_users", "day_totals[].totals_by_model_feature", "day_totals[].totals_by_model_feature[]", "day_totals[].totals_by_model_feature[].model", "day_totals[].totals_by_model_feature[].feature", "day_totals[].totals_by_model_feature[].engaged_users", "day_totals[].totals_by_model_feature[].total_requests", "day_totals[].totals_by_model_feature[].user_initiated_interaction_count", "day_totals[].totals_by_cli", "day_totals[].totals_by_cli.request_count", "day_totals[].totals_by_cli.token_usage", "day_totals[].totals_by_cli.token_usage.prompt_tokens_sum", "day_totals[].totals_by_cli.token_usage.output_tokens_sum", "day_totals[].totals_by_cli.engaged_users", "day_totals[].totals_by_cli.total_requests", "report_start_day", "report_end_day", "generated_at"]);
1125
+ return new Set([...common, "download_links", "download_links[]", "day_totals", "day_totals[]", "day_totals[].day", "day_totals[].daily_active_users", "day_totals[].totals_by_model_feature", "day_totals[].totals_by_model_feature[]", "day_totals[].totals_by_model_feature[].model", "day_totals[].totals_by_model_feature[].feature", "day_totals[].totals_by_model_feature[].engaged_users", "day_totals[].totals_by_model_feature[].total_requests", "day_totals[].totals_by_model_feature[].user_initiated_interaction_count", "day_totals[].totals_by_cli", "day_totals[].totals_by_cli.request_count", "day_totals[].totals_by_cli.prompt_count", "day_totals[].totals_by_cli.session_count", "day_totals[].totals_by_cli.token_usage", "day_totals[].totals_by_cli.token_usage.prompt_tokens_sum", "day_totals[].totals_by_cli.token_usage.output_tokens_sum", "day_totals[].totals_by_cli.token_usage.avg_tokens_per_request", "day_totals[].totals_by_cli.engaged_users", "day_totals[].totals_by_cli.total_requests", "report_start_day", "report_end_day", "created_at", "generated_at", "etl_id", "day_partition", "entity_id_partition", "enterprise_id", "organization_id"]);
611
1126
  }
612
1127
  if (provider === "github-copilot" && label.toLowerCase().includes("seats")) {
613
- return new Set([...common, "total_seats", "plan_type", "seats", "seats[]", "seats[].created_at", "seats[].updated_at", "seats[].pending_cancellation_date", "seats[].last_activity_at", "seats[].last_activity_editor", "seats[].plan_type", "seats[].login", "seats[].id", "seats[].assignee", "seats[].assignee.login", "seats[].assignee.email", "seats[].assignee.id", "seats[].assignee.node_id", "seats[].assignee.avatar_url", "seats[].assignee.html_url", "seats[].assignee.type", "seats[].assignee.site_admin", "seats[].assigning_team", "seats[].organization"]);
1128
+ return new Set([...common, "total_seats", "seats", "seats[]", "seats[].created_at", "seats[].updated_at", "seats[].pending_cancellation_date", "seats[].last_activity_at", "seats[].last_activity_editor", "seats[].last_authenticated_at", "seats[].plan_type", "seats[].login", "seats[].id", "seats[].assignee", "seats[].assignee.login", "seats[].assignee.email", "seats[].assignee.id", "seats[].assignee.node_id", "seats[].assignee.avatar_url", "seats[].assignee.html_url", "seats[].assignee.type", "seats[].assignee.site_admin", "seats[].assigning_team", "seats[].organization"]);
614
1129
  }
615
1130
  if (provider === "cursor") {
616
- return new Set([...common, "users", "users[]", "users[].email", "users[].emailAddress", "users[].userId", "users[].id", "users[].name", "users[].role", "users[].spendCents", "users[].usageBasedCents", "users[].chargedCents", "users[].fastPremiumRequests", "users[].hardLimitOverrideDollars", "data[].email", "data[].emailAddress", "data[].userId", "data[].id", "data[].name", "data[].role", "data[].spendCents", "data[].usageBasedCents", "data[].chargedCents", "subscriptionCycleStart", "totalMembers", "totalPages"]);
1131
+ return new Set([...common, "teamMemberSpend", "teamMemberSpend[]", "teamMemberSpend[].userId", "teamMemberSpend[].email", "teamMemberSpend[].name", "teamMemberSpend[].role", "teamMemberSpend[].spendCents", "teamMemberSpend[].fastPremiumRequests", "teamMemberSpend[].hardLimitOverrideDollars", "subscriptionCycleStart", "totalMembers", "totalPages"]);
617
1132
  }
618
1133
  return new Set([...common]);
619
1134
  }
620
1135
  function qaSummary(provider, fetches) {
621
1136
  return {
622
1137
  provider,
623
- coverage: fetches.every((fetchResult) => fetchResult.pagination.stoppedBecause === "complete") ? "complete" : "partial",
1138
+ coverage: fetches.every((fetchResult) => fetchResult.pagination.stoppedBecause === "complete" && fetchResult.coverageIncomplete !== true) ? "complete" : "partial",
624
1139
  requestedEndpoints: Array.from(new Set(fetches.map((fetchResult) => fetchResult.pagination.label))),
625
1140
  pagination: fetches.map((fetchResult) => fetchResult.pagination),
626
1141
  rateLimits: fetches.flatMap((fetchResult) => fetchResult.rateLimits),
@@ -681,8 +1196,26 @@ function extractProviderMessage(payload) {
681
1196
  return stringValue(error?.message) ?? stringValue(payload.message) ?? "";
682
1197
  }
683
1198
  function sanitizeProviderMessage(message) {
684
- // One redaction implementation for the whole product (discovery.ts owns it).
685
- return redactSecrets(message).replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED]");
1199
+ // Provider error bodies/status text are terminal-facing untrusted input.
1200
+ // Strip controls first so an escape sequence cannot split a secret pattern,
1201
+ // then apply the product-wide redaction rules.
1202
+ return redactSecrets(stripTerminalControlSequences(message))
1203
+ .replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]")
1204
+ .replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED]")
1205
+ .trim();
1206
+ }
1207
+ function stripTerminalControlSequences(message) {
1208
+ return message
1209
+ // OSC (window title, hyperlinks, clipboard), terminated by BEL/ST or EOF.
1210
+ .replace(/(?:\u001b\]|\u009d)[\s\S]*?(?:\u0007|\u001b\\|\u009c|$)/gu, "")
1211
+ // DCS/SOS/PM/APC string controls, terminated by ST or EOF.
1212
+ .replace(/(?:\u001b(?:P|X|\^|_)|[\u0090\u0098\u009e\u009f])[\s\S]*?(?:\u001b\\|\u009c|$)/gu, "")
1213
+ // CSI plus remaining two-character ESC sequences.
1214
+ .replace(/(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]/gu, "")
1215
+ .replace(/\u001b[@-_]/gu, "")
1216
+ // Prevent line/status injection while keeping words readable.
1217
+ .replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ")
1218
+ .replace(/\s+/gu, " ");
686
1219
  }
687
1220
  export function summarizeProviderFinancials(records) {
688
1221
  const providerReportedBilledUsd = sumAmounts(records.filter((record) => record.costConfidence === "verified"));
@@ -739,7 +1272,7 @@ function providerResult(provider, sourceId, authReference, records, qa) {
739
1272
  : headlineConfidence;
740
1273
  return {
741
1274
  provider,
742
- source: createProviderConnection({ provider, sourceId, authReference, verifiedRecordCount: records.length, totalUsd: financials.headlineUsd ?? 0, completeness }),
1275
+ source: createProviderConnection({ provider, sourceId, authReference, verifiedRecordCount: records.length, totalUsd: financials.headlineUsd, completeness }),
743
1276
  records,
744
1277
  fetchedAt: new Date().toISOString(),
745
1278
  coverage,
@@ -750,17 +1283,29 @@ function providerResult(provider, sourceId, authReference, records, qa) {
750
1283
  }
751
1284
  export function createProviderConnection(input) {
752
1285
  const source = createProviderConnectorStub(input.provider, "provider_api", input.fetchedAt);
753
- const total = `$${input.totalUsd.toFixed(2)}`;
754
- const verification = input.completeness ?? "verified";
1286
+ const total = input.totalUsd === null ? "an unavailable financial headline" : formatProviderUsd(input.totalUsd);
1287
+ const financialEvidence = input.completeness ?? "verified";
755
1288
  return {
756
1289
  ...source,
757
1290
  id: input.sourceId ?? source.id,
758
- verification,
1291
+ validationCoverage: validationCoverageForCompletedProviderSync(input.provider),
1292
+ financialEvidence,
759
1293
  authReference: input.authReference,
760
1294
  fieldsMissing: [],
761
- scope: `${source.scope} Last successful pull produced ${input.verifiedRecordCount} ${verification} records totaling ${total}.`
1295
+ scope: `${source.scope} Last successful pull produced ${input.verifiedRecordCount} record(s); financial evidence: ${financialEvidence}; financial headline: ${total}.`
762
1296
  };
763
1297
  }
1298
+ function validationCoverageForCompletedProviderSync(provider) {
1299
+ if (provider === "openai" || provider === "anthropic")
1300
+ return "live_verified";
1301
+ if (provider === "cursor" || provider === "github-copilot" || provider === "copilot") {
1302
+ return "fixture_verified";
1303
+ }
1304
+ return "untested";
1305
+ }
1306
+ function formatProviderUsd(value) {
1307
+ return value > 0 && value < 0.01 ? "less than $0.01" : `$${value.toFixed(2)}`;
1308
+ }
764
1309
  export function resolveTokenReference(reference, env = process.env) {
765
1310
  if (!reference.startsWith("env:")) {
766
1311
  throw new Error("Provider auth reference must be a local reference such as env:OPENAI_ADMIN_KEY; raw secrets are not accepted.");
@@ -835,20 +1380,45 @@ async function defaultFetcher(url, init) {
835
1380
  return fetch(url, { ...init, redirect: "manual" });
836
1381
  }
837
1382
  function parseMinorUsd(value) {
838
- const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
839
- return Number.isFinite(numeric) ? numeric / 100 : undefined;
1383
+ const numeric = typeof value === "number"
1384
+ ? value
1385
+ : typeof value === "string" && value.trim().length > 0
1386
+ ? Number(value)
1387
+ : Number.NaN;
1388
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric / 100 : undefined;
840
1389
  }
841
1390
  /** Amount already denominated in dollars, as number or decimal string. */
842
1391
  function parseDollarUsd(value) {
843
- const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
844
- return Number.isFinite(numeric) ? numeric : undefined;
1392
+ const numeric = typeof value === "number"
1393
+ ? value
1394
+ : typeof value === "string" && value.trim().length > 0
1395
+ ? Number(value)
1396
+ : Number.NaN;
1397
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric : undefined;
845
1398
  }
846
1399
  function numberValue(value) {
847
1400
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
848
1401
  }
1402
+ function nonNegativeNumberValue(value) {
1403
+ const numeric = numberValue(value);
1404
+ return typeof numeric === "number" && numeric >= 0 ? numeric : undefined;
1405
+ }
1406
+ function nonNegativeIntegerValue(value) {
1407
+ const numeric = numberValue(value);
1408
+ return typeof numeric === "number" && Number.isInteger(numeric) && numeric >= 0 ? numeric : undefined;
1409
+ }
849
1410
  function stringValue(value) {
850
1411
  return typeof value === "string" && value.length > 0 ? value : undefined;
851
1412
  }
1413
+ function validEpochSeconds(value) {
1414
+ const seconds = numberValue(value);
1415
+ return typeof seconds === "number" && seconds >= 0 && Number.isFinite(new Date(seconds * 1000).getTime())
1416
+ ? seconds
1417
+ : undefined;
1418
+ }
1419
+ function validDateTimeString(value) {
1420
+ return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value)) ? value : undefined;
1421
+ }
852
1422
  function extractArray(value, key) {
853
1423
  if (Array.isArray(value))
854
1424
  return value;
@@ -856,6 +1426,20 @@ function extractArray(value, key) {
856
1426
  return value[key];
857
1427
  return [];
858
1428
  }
1429
+ function requireStringArray(value, key, label) {
1430
+ if (!isRecord(value) || !Array.isArray(value[key]) || value[key].length === 0) {
1431
+ throw new Error(`${label} returned no signed NDJSON ${key}; refusing to report an empty metrics sync.`);
1432
+ }
1433
+ const values = value[key];
1434
+ if (values.some((item) => typeof item !== "string" || item.length === 0)) {
1435
+ throw new Error(`${label} returned a malformed ${key} entry; refusing a partial metrics sync.`);
1436
+ }
1437
+ const strings = values;
1438
+ if (new Set(strings).size !== strings.length) {
1439
+ throw new Error(`${label} returned duplicate ${key}; refusing to double-count a report partition.`);
1440
+ }
1441
+ return strings;
1442
+ }
859
1443
  function isObject(value) {
860
1444
  return typeof value === "object" && value !== null;
861
1445
  }