@ganakailabs/cloudeval-cli 0.18.4
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.
- package/LICENSE +77 -0
- package/NOTICE +10 -0
- package/README.md +23 -0
- package/THIRD_PARTY_NOTICES.md +124 -0
- package/dist/App-64BACPSQ.js +7023 -0
- package/dist/Banner-BIUMDX72.js +12 -0
- package/dist/Onboarding-HVCYIPTL.js +8 -0
- package/dist/ReportDashboard-6JFJJVRW.js +229 -0
- package/dist/chunk-3DVPEIVB.js +185 -0
- package/dist/chunk-4QIKW5TJ.js +3472 -0
- package/dist/chunk-GY4W535V.js +131 -0
- package/dist/chunk-RQ2GBK43.js +6 -0
- package/dist/chunk-TA4WC462.js +174 -0
- package/dist/chunk-UOCT7M4J.js +43 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +14026 -0
- package/dist/dist-I4IPYCRC.js +144 -0
- package/package.json +108 -0
- package/sbom.spdx.json +1627 -0
|
@@ -0,0 +1,3472 @@
|
|
|
1
|
+
// ../shared/dist/index.js
|
|
2
|
+
var isObject = (value) => typeof value === "object" && value !== null;
|
|
3
|
+
var stringOr = (value, fallback) => typeof value === "string" && value.trim() ? value : fallback;
|
|
4
|
+
var normalizeProvider = (value) => {
|
|
5
|
+
if (value === "azure" || value === "aws" || value === "gcp") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return "unknown";
|
|
9
|
+
};
|
|
10
|
+
var normalizeSections = (value) => {
|
|
11
|
+
if (!Array.isArray(value)) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return value.map((section, index) => {
|
|
15
|
+
if (!isObject(section)) {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: stringOr(section.id, `section-${index + 1}`),
|
|
20
|
+
title: stringOr(section.title, `Section ${index + 1}`),
|
|
21
|
+
markdown: stringOr(section.markdown, "")
|
|
22
|
+
};
|
|
23
|
+
}).filter((section) => Boolean(section));
|
|
24
|
+
};
|
|
25
|
+
var normalizeReportEnvelope = (input) => {
|
|
26
|
+
const rawInput = isObject(input) && isObject(input.report) ? input.report : input;
|
|
27
|
+
if (!isObject(rawInput)) {
|
|
28
|
+
throw new Error("Report response must be an object.");
|
|
29
|
+
}
|
|
30
|
+
const source = isObject(rawInput.source) ? rawInput.source : {};
|
|
31
|
+
const formatted = isObject(rawInput.formatted) ? rawInput.formatted : void 0;
|
|
32
|
+
const period = isObject(rawInput.period) ? {
|
|
33
|
+
start: stringOr(rawInput.period.start, ""),
|
|
34
|
+
end: stringOr(rawInput.period.end, "")
|
|
35
|
+
} : void 0;
|
|
36
|
+
return {
|
|
37
|
+
id: stringOr(rawInput.id, "unknown-report"),
|
|
38
|
+
kind: rawInput.kind === "waf" ? "waf" : "cost",
|
|
39
|
+
projectId: stringOr(rawInput.projectId ?? rawInput.project_id, "unknown-project"),
|
|
40
|
+
generatedAt: stringOr(
|
|
41
|
+
rawInput.generatedAt ?? rawInput.generated_at,
|
|
42
|
+
(/* @__PURE__ */ new Date(0)).toISOString()
|
|
43
|
+
),
|
|
44
|
+
period,
|
|
45
|
+
source: {
|
|
46
|
+
provider: normalizeProvider(source.provider ?? rawInput.provider),
|
|
47
|
+
backendVersion: typeof source.backendVersion === "string" ? source.backendVersion : typeof source.backend_version === "string" ? source.backend_version : void 0,
|
|
48
|
+
evidenceRefs: Array.isArray(source.evidenceRefs) ? source.evidenceRefs.filter((ref) => typeof ref === "string") : Array.isArray(source.evidence_refs) ? source.evidence_refs.filter((ref) => typeof ref === "string") : void 0
|
|
49
|
+
},
|
|
50
|
+
raw: rawInput.raw ?? {},
|
|
51
|
+
parsed: rawInput.parsed ?? {},
|
|
52
|
+
formatted: formatted ? {
|
|
53
|
+
title: stringOr(formatted.title, "Report"),
|
|
54
|
+
summary: stringOr(formatted.summary, ""),
|
|
55
|
+
sections: normalizeSections(formatted.sections)
|
|
56
|
+
} : void 0
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
var normalizeReportList = (input) => {
|
|
60
|
+
const list = Array.isArray(input) ? input : isObject(input) && Array.isArray(input.reports) ? input.reports : isObject(input) && Array.isArray(input.data) ? input.data : isObject(input) && Array.isArray(input.items) ? input.items : [];
|
|
61
|
+
return list.map((item) => normalizeReportEnvelope(item));
|
|
62
|
+
};
|
|
63
|
+
var SECRET_REDACTION = "[redacted]";
|
|
64
|
+
var SENSITIVE_KEY_PATTERN = /token|authorization|cookie|secret|password|api[_-]?key|access[_-]?key|client[_-]?secret|refresh|device[_-]?code|user[_-]?code/i;
|
|
65
|
+
var SENSITIVE_QUERY_PARAM_PATTERN = /token|authorization|cookie|secret|password|api[_-]?key|access[_-]?key|client[_-]?secret|refresh|device[_-]?code|user[_-]?code|code/i;
|
|
66
|
+
var CLOUDEVAL_ACCESS_KEY_VALUE_PATTERN = /\bcev_[a-z0-9]+_ak_[A-Za-z0-9]+_[A-Za-z0-9._~+-]+(?:_[A-Za-z0-9._~+-]+)*\b/gi;
|
|
67
|
+
var AUTHORIZATION_BEARER_PATTERN = /\b(authorization\s*:\s*bearer\s+)([^\s'",}]+)/gi;
|
|
68
|
+
var isSensitiveSecretKey = (key) => SENSITIVE_KEY_PATTERN.test(key);
|
|
69
|
+
var redactSensitiveText = (value) => {
|
|
70
|
+
let text = value;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(text);
|
|
73
|
+
if (parsed && typeof parsed === "object") {
|
|
74
|
+
return JSON.stringify(redactSensitiveSecrets(parsed));
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(text);
|
|
80
|
+
let changed = false;
|
|
81
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
82
|
+
if (SENSITIVE_QUERY_PARAM_PATTERN.test(key)) {
|
|
83
|
+
url.searchParams.set(key, SECRET_REDACTION);
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (changed) {
|
|
88
|
+
text = url.toString();
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return text.replace(AUTHORIZATION_BEARER_PATTERN, (_match, prefix) => `${prefix}${SECRET_REDACTION}`).replace(CLOUDEVAL_ACCESS_KEY_VALUE_PATTERN, SECRET_REDACTION);
|
|
93
|
+
};
|
|
94
|
+
var redactSensitiveSecrets = (value) => {
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.map((item) => redactSensitiveSecrets(item));
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
return redactSensitiveText(value);
|
|
100
|
+
}
|
|
101
|
+
if (value && typeof value === "object") {
|
|
102
|
+
const redacted = {};
|
|
103
|
+
for (const [key, item] of Object.entries(value)) {
|
|
104
|
+
redacted[key] = item === null || item === void 0 ? item : isSensitiveSecretKey(key) ? SECRET_REDACTION : redactSensitiveSecrets(item);
|
|
105
|
+
}
|
|
106
|
+
return redacted;
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
};
|
|
110
|
+
var baseSettings = {
|
|
111
|
+
mode: "agent",
|
|
112
|
+
response_length: "Detailed",
|
|
113
|
+
technicality: "Expert",
|
|
114
|
+
reasoning_effort: "medium",
|
|
115
|
+
enable_judge: true,
|
|
116
|
+
enable_hitl: true
|
|
117
|
+
};
|
|
118
|
+
var BUNDLED_AGENT_PROFILES = [
|
|
119
|
+
{
|
|
120
|
+
id: "architecture",
|
|
121
|
+
display_name: "Architecture",
|
|
122
|
+
description: "Reviews topology, dependencies, blast radius, reliability, security, cost, operational excellence, and performance signals.",
|
|
123
|
+
personality: "Systems-minded, concise, and dependency-aware.",
|
|
124
|
+
accent_key: "blue",
|
|
125
|
+
icon_key: "network",
|
|
126
|
+
default_mode: "agent",
|
|
127
|
+
starter_prompt: "Review this project architecture for the highest-impact risks and next actions.",
|
|
128
|
+
starter_prompts: {
|
|
129
|
+
template: "Review this template architecture for topology, dependency, and platform-design risks.",
|
|
130
|
+
sync: "Review this live environment architecture for topology, dependency, and blast-radius risks."
|
|
131
|
+
},
|
|
132
|
+
starter_prompt_variants: [
|
|
133
|
+
{
|
|
134
|
+
id: "architecture-template-agent",
|
|
135
|
+
project_source: "template",
|
|
136
|
+
mode: "agent",
|
|
137
|
+
text: "Review this template architecture for topology, dependency, and platform-design risks.",
|
|
138
|
+
weight: 1
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "architecture-sync-agent",
|
|
142
|
+
project_source: "sync",
|
|
143
|
+
mode: "agent",
|
|
144
|
+
text: "Review this live environment architecture for topology, dependency, and blast-radius risks.",
|
|
145
|
+
weight: 1
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
system_instructions: "Use the Architecture lens. Prioritize topology, dependencies, blast radius, reliability, security, cost, operations, and performance evidence. Return risk-ranked findings with concrete validation steps.",
|
|
149
|
+
default_settings: { ...baseSettings },
|
|
150
|
+
required_capabilities: ["projects:read", "reports:read", "ask:run"],
|
|
151
|
+
allowed_toolsets: ["projects", "reports", "graph", "rules"],
|
|
152
|
+
output_contract: {
|
|
153
|
+
sections: ["interpretation", "top_signals", "actions", "checkpoint"]
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "cost",
|
|
158
|
+
display_name: "Cost",
|
|
159
|
+
description: "Reviews project cost drivers, waste signals, and practical optimization opportunities.",
|
|
160
|
+
personality: "Commercially pragmatic, specific, and action-oriented.",
|
|
161
|
+
accent_key: "emerald",
|
|
162
|
+
icon_key: "wallet",
|
|
163
|
+
default_mode: "agent",
|
|
164
|
+
starter_prompt: "Review this project for cost drivers, waste signals, and practical savings actions.",
|
|
165
|
+
starter_prompts: {
|
|
166
|
+
template: "Review this template for cost drivers, expensive defaults, and safer sizing choices.",
|
|
167
|
+
sync: "Review this live environment for waste signals, savings opportunities, and ownership gaps."
|
|
168
|
+
},
|
|
169
|
+
starter_prompt_variants: [
|
|
170
|
+
{
|
|
171
|
+
id: "cost-template-agent",
|
|
172
|
+
project_source: "template",
|
|
173
|
+
mode: "agent",
|
|
174
|
+
text: "Review this template for cost drivers, expensive defaults, and safer sizing choices.",
|
|
175
|
+
weight: 1
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "cost-sync-agent",
|
|
179
|
+
project_source: "sync",
|
|
180
|
+
mode: "agent",
|
|
181
|
+
text: "Review this live environment for waste signals, savings opportunities, and ownership gaps.",
|
|
182
|
+
weight: 1
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
system_instructions: "Use the Cost lens. Prioritize spend drivers, over-provisioning, idle or low-value resources, tagging gaps, pricing model choices, and savings actions. Keep recommendations commercially practical.",
|
|
186
|
+
default_settings: { ...baseSettings },
|
|
187
|
+
required_capabilities: [
|
|
188
|
+
"projects:read",
|
|
189
|
+
"reports:read",
|
|
190
|
+
"billing:read",
|
|
191
|
+
"ask:run"
|
|
192
|
+
],
|
|
193
|
+
allowed_toolsets: ["projects", "reports", "billing", "cost"],
|
|
194
|
+
output_contract: {
|
|
195
|
+
sections: ["cost_drivers", "waste_signals", "savings_actions", "checkpoint"]
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "triage",
|
|
200
|
+
display_name: "Triage",
|
|
201
|
+
description: "Helps investigate likely failure domains, affected resources, and first response actions.",
|
|
202
|
+
personality: "Calm, diagnostic, and time-sensitive.",
|
|
203
|
+
accent_key: "orange",
|
|
204
|
+
icon_key: "activity",
|
|
205
|
+
default_mode: "agent",
|
|
206
|
+
starter_prompt: "Triage this project for likely failure domains, impact paths, and first checks.",
|
|
207
|
+
starter_prompts: {
|
|
208
|
+
template: "Triage this template for likely deployment failure domains and first checks.",
|
|
209
|
+
sync: "Triage this live environment for likely failure domains, impacted paths, and containment checks."
|
|
210
|
+
},
|
|
211
|
+
starter_prompt_variants: [
|
|
212
|
+
{
|
|
213
|
+
id: "triage-template-agent",
|
|
214
|
+
project_source: "template",
|
|
215
|
+
mode: "agent",
|
|
216
|
+
text: "Triage this template for likely deployment failure domains and first checks.",
|
|
217
|
+
weight: 1
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "triage-sync-agent",
|
|
221
|
+
project_source: "sync",
|
|
222
|
+
mode: "agent",
|
|
223
|
+
text: "Triage this live environment for likely failure domains, impacted paths, and containment checks.",
|
|
224
|
+
weight: 1
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
system_instructions: "Use the Triage lens. Prioritize failure domains, affected resources, impact paths, first checks, rollback or containment options, and uncertainty that needs quick validation.",
|
|
228
|
+
default_settings: { ...baseSettings },
|
|
229
|
+
required_capabilities: ["projects:read", "reports:read", "ask:run"],
|
|
230
|
+
allowed_toolsets: ["projects", "reports", "graph", "rules"],
|
|
231
|
+
output_contract: {
|
|
232
|
+
sections: ["hypothesis", "impact", "first_checks", "containment"]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "remediation",
|
|
237
|
+
display_name: "Remediation",
|
|
238
|
+
description: "Turns findings into ordered implementation steps with owners, dependencies, and validation checks.",
|
|
239
|
+
personality: "Delivery-focused, practical, and sequencing-aware.",
|
|
240
|
+
accent_key: "rose",
|
|
241
|
+
icon_key: "list-checks",
|
|
242
|
+
default_mode: "agent",
|
|
243
|
+
starter_prompt: "Create an ordered remediation plan for this project with dependencies and validation checks.",
|
|
244
|
+
starter_prompts: {
|
|
245
|
+
template: "Create a template remediation plan with ordered changes, dependencies, and validation checks.",
|
|
246
|
+
sync: "Create a live-environment remediation plan with owners, dependencies, and validation checks."
|
|
247
|
+
},
|
|
248
|
+
starter_prompt_variants: [
|
|
249
|
+
{
|
|
250
|
+
id: "remediation-template-agent",
|
|
251
|
+
project_source: "template",
|
|
252
|
+
mode: "agent",
|
|
253
|
+
text: "Create a template remediation plan with ordered changes, dependencies, and validation checks.",
|
|
254
|
+
weight: 1
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "remediation-sync-agent",
|
|
258
|
+
project_source: "sync",
|
|
259
|
+
mode: "agent",
|
|
260
|
+
text: "Create a live-environment remediation plan with owners, dependencies, and validation checks.",
|
|
261
|
+
weight: 1
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
system_instructions: "Use the Remediation lens. Convert evidence into ordered fixes, owners, dependencies, risk sequencing, validation checks, rollback notes, and measurable completion criteria.",
|
|
265
|
+
default_settings: { ...baseSettings },
|
|
266
|
+
required_capabilities: ["projects:read", "reports:read", "ask:run"],
|
|
267
|
+
allowed_toolsets: ["projects", "reports", "rules"],
|
|
268
|
+
output_contract: {
|
|
269
|
+
sections: ["plan", "dependencies", "validation", "rollback"]
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
];
|
|
273
|
+
var getBundledAgentProfiles = () => BUNDLED_AGENT_PROFILES.map((profile) => ({
|
|
274
|
+
...profile,
|
|
275
|
+
starter_prompts: { ...profile.starter_prompts ?? {} },
|
|
276
|
+
starter_prompt_variants: profile.starter_prompt_variants?.map((variant) => ({
|
|
277
|
+
...variant
|
|
278
|
+
})),
|
|
279
|
+
default_settings: { ...profile.default_settings },
|
|
280
|
+
required_capabilities: [...profile.required_capabilities],
|
|
281
|
+
allowed_toolsets: [...profile.allowed_toolsets],
|
|
282
|
+
output_contract: profile.output_contract ? { ...profile.output_contract } : void 0
|
|
283
|
+
}));
|
|
284
|
+
var getBundledAgentProfile = (profileId) => getBundledAgentProfiles().find((profile) => profile.id === profileId);
|
|
285
|
+
|
|
286
|
+
// ../core/dist/index.js
|
|
287
|
+
import fs from "fs";
|
|
288
|
+
import os from "os";
|
|
289
|
+
import path from "path";
|
|
290
|
+
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
291
|
+
import { randomUUID } from "crypto";
|
|
292
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
293
|
+
var DEFAULT_BASE_URL = "https://cloudeval.ai/api/proxy/v1";
|
|
294
|
+
var DEFAULT_FRONTEND_URL = "https://cloudeval.ai";
|
|
295
|
+
var TOKEN_EXPIRY_SKEW_MS = 12e4;
|
|
296
|
+
var ACCESS_SECRET_LABEL = "access-token";
|
|
297
|
+
var REFRESH_SECRET_LABEL = "refresh-token";
|
|
298
|
+
var INSECURE_FILE_FALLBACK_ENV = "CLOUDEVAL_ALLOW_INSECURE_FILE_STORAGE";
|
|
299
|
+
var CONCURRENT_REFRESH_WAIT_STEPS_MS = [50, 100, 150, 250];
|
|
300
|
+
var REFRESH_LOCK_WAIT_STEP_MS = 100;
|
|
301
|
+
var REFRESH_LOCK_STALE_MS = 3e4;
|
|
302
|
+
var CLI_DEBUG_ENV = "CLOUDEVAL_CLI_DEBUG";
|
|
303
|
+
var SENSITIVE_DEBUG_KEY_PATTERN = /token|authorization|cookie|secret|password|api[_-]?key|access[_-]?key|client_secret|refresh|device[_-]?code|user[_-]?code/i;
|
|
304
|
+
var SENSITIVE_DEBUG_QUERY_PARAM_PATTERN = /token|authorization|cookie|secret|password|api[_-]?key|access[_-]?key|client_secret|refresh|device[_-]?code|user[_-]?code|code/i;
|
|
305
|
+
var KEYCHAIN_SERVICE = "cloudeval-cli";
|
|
306
|
+
var KEYCHAIN_LABEL = "Cloudeval CLI";
|
|
307
|
+
var isCliDebugEnabled = () => {
|
|
308
|
+
const value = process.env[CLI_DEBUG_ENV];
|
|
309
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
310
|
+
};
|
|
311
|
+
var redactDebugValue = (value) => {
|
|
312
|
+
if (Array.isArray(value)) {
|
|
313
|
+
return value.map((item) => redactDebugValue(item));
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === "string") {
|
|
316
|
+
try {
|
|
317
|
+
const url = new URL(value);
|
|
318
|
+
let changed = false;
|
|
319
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
320
|
+
if (SENSITIVE_DEBUG_QUERY_PARAM_PATTERN.test(key)) {
|
|
321
|
+
url.searchParams.set(key, "[REDACTED]");
|
|
322
|
+
changed = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return redactSensitiveText(changed ? url.toString() : value);
|
|
326
|
+
} catch {
|
|
327
|
+
return redactSensitiveText(value);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (value && typeof value === "object") {
|
|
331
|
+
const redacted = {};
|
|
332
|
+
for (const [key, item] of Object.entries(value)) {
|
|
333
|
+
redacted[key] = SENSITIVE_DEBUG_KEY_PATTERN.test(key) ? "[REDACTED]" : redactDebugValue(item);
|
|
334
|
+
}
|
|
335
|
+
return redacted;
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
};
|
|
339
|
+
var cliDebug = (message, data) => {
|
|
340
|
+
if (!isCliDebugEnabled()) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const prefix = `[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI DEBUG]`;
|
|
344
|
+
if (data === void 0) {
|
|
345
|
+
console.error(`${prefix} ${message}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
console.error(
|
|
350
|
+
`${prefix} ${message}
|
|
351
|
+
${JSON.stringify(redactDebugValue(data), null, 2)}`
|
|
352
|
+
);
|
|
353
|
+
} catch {
|
|
354
|
+
console.error(`${prefix} ${message}`, redactDebugValue(data));
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
var cachedToken = null;
|
|
358
|
+
var stored = null;
|
|
359
|
+
var refreshInFlight = null;
|
|
360
|
+
var warnedAboutSecretBackend = false;
|
|
361
|
+
var memorySecrets = /* @__PURE__ */ new Map();
|
|
362
|
+
var now = () => Date.now();
|
|
363
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
364
|
+
var errorMessage = (error) => error instanceof Error ? error.message : typeof error === "string" ? error : String(error ?? "Unknown error");
|
|
365
|
+
var isRejectedRefreshTokenError = (error) => {
|
|
366
|
+
const message = errorMessage(error).toLowerCase();
|
|
367
|
+
return message.includes("invalid_grant") || message.includes("consent_required") || message.includes("aadsts65001") || message.includes("interaction_required") || message.includes("refresh token") && (message.includes("revoked") || message.includes("expired") || message.includes("invalid")) || message.includes("aadsts700082") || message.includes("aadsts70000");
|
|
368
|
+
};
|
|
369
|
+
var isProcessAlive = (pid) => {
|
|
370
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
process.kill(pid, 0);
|
|
375
|
+
return true;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return error?.code === "EPERM";
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
var configDir = path.join(os.homedir(), ".config", "cloudeval");
|
|
381
|
+
var configPath = path.join(configDir, "config.json");
|
|
382
|
+
var secretFilePath = path.join(configDir, "secrets.json");
|
|
383
|
+
var refreshLockPath = path.join(configDir, "refresh.lock");
|
|
384
|
+
var commandExists = (cmd) => {
|
|
385
|
+
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
386
|
+
try {
|
|
387
|
+
execFileSync(whichCmd, [cmd], { stdio: "ignore" });
|
|
388
|
+
return true;
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
var detectSecretBackend = () => {
|
|
394
|
+
if (process.env[INSECURE_FILE_FALLBACK_ENV] === "1") {
|
|
395
|
+
return "insecure-file";
|
|
396
|
+
}
|
|
397
|
+
if (process.platform === "darwin" && commandExists("security")) {
|
|
398
|
+
return "macos-keychain";
|
|
399
|
+
}
|
|
400
|
+
if (process.platform === "linux" && commandExists("secret-tool")) {
|
|
401
|
+
return "linux-libsecret";
|
|
402
|
+
}
|
|
403
|
+
if (process.platform === "win32" && commandExists("powershell")) {
|
|
404
|
+
return "windows-dpapi";
|
|
405
|
+
}
|
|
406
|
+
return "memory";
|
|
407
|
+
};
|
|
408
|
+
var ensureConfigDir = () => {
|
|
409
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
410
|
+
};
|
|
411
|
+
var acquireRefreshLock = async () => {
|
|
412
|
+
ensureConfigDir();
|
|
413
|
+
while (true) {
|
|
414
|
+
try {
|
|
415
|
+
const fd = fs.openSync(refreshLockPath, "wx", 384);
|
|
416
|
+
let released = false;
|
|
417
|
+
try {
|
|
418
|
+
fs.writeFileSync(fd, String(process.pid), { encoding: "utf8" });
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
return () => {
|
|
422
|
+
if (released) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
released = true;
|
|
426
|
+
try {
|
|
427
|
+
fs.closeSync(fd);
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
fs.unlinkSync(refreshLockPath);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error?.code !== "EEXIST") {
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const pid = Number(fs.readFileSync(refreshLockPath, "utf8").trim());
|
|
441
|
+
if (pid && !isProcessAlive(pid)) {
|
|
442
|
+
fs.unlinkSync(refreshLockPath);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const stat = fs.statSync(refreshLockPath);
|
|
449
|
+
if (now() - stat.mtimeMs > REFRESH_LOCK_STALE_MS) {
|
|
450
|
+
fs.unlinkSync(refreshLockPath);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
await sleep(REFRESH_LOCK_WAIT_STEP_MS);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
var readSecretsFile = () => {
|
|
461
|
+
try {
|
|
462
|
+
const raw = fs.readFileSync(secretFilePath, "utf8");
|
|
463
|
+
const parsed = JSON.parse(raw);
|
|
464
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
465
|
+
} catch {
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var writeSecretsFile = (secrets) => {
|
|
470
|
+
ensureConfigDir();
|
|
471
|
+
fs.writeFileSync(secretFilePath, JSON.stringify(secrets, null, 2), {
|
|
472
|
+
encoding: "utf8",
|
|
473
|
+
mode: 384
|
|
474
|
+
});
|
|
475
|
+
try {
|
|
476
|
+
fs.chmodSync(secretFilePath, 384);
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var dpapiProtect = (plainText) => {
|
|
481
|
+
const script = "[Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($env:CLOUDEVAL_SECRET), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser))";
|
|
482
|
+
return execFileSync(
|
|
483
|
+
"powershell",
|
|
484
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
485
|
+
{
|
|
486
|
+
encoding: "utf8",
|
|
487
|
+
env: {
|
|
488
|
+
...process.env,
|
|
489
|
+
CLOUDEVAL_SECRET: plainText
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
).trim();
|
|
493
|
+
};
|
|
494
|
+
var dpapiUnprotect = (cipherTextB64) => {
|
|
495
|
+
const script = "$bytes=[System.Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($env:CLOUDEVAL_SECRET_B64), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser); [Text.Encoding]::UTF8.GetString($bytes)";
|
|
496
|
+
return execFileSync(
|
|
497
|
+
"powershell",
|
|
498
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
499
|
+
{
|
|
500
|
+
encoding: "utf8",
|
|
501
|
+
env: {
|
|
502
|
+
...process.env,
|
|
503
|
+
CLOUDEVAL_SECRET_B64: cipherTextB64
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
).trim();
|
|
507
|
+
};
|
|
508
|
+
var setSecret = (key, value) => {
|
|
509
|
+
const backend = detectSecretBackend();
|
|
510
|
+
try {
|
|
511
|
+
if (backend === "macos-keychain") {
|
|
512
|
+
execFileSync("security", [
|
|
513
|
+
"add-generic-password",
|
|
514
|
+
"-a",
|
|
515
|
+
key,
|
|
516
|
+
"-s",
|
|
517
|
+
KEYCHAIN_SERVICE,
|
|
518
|
+
"-l",
|
|
519
|
+
KEYCHAIN_LABEL,
|
|
520
|
+
"-w",
|
|
521
|
+
value,
|
|
522
|
+
"-U"
|
|
523
|
+
]);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
if (backend === "linux-libsecret") {
|
|
527
|
+
const result = spawnSync(
|
|
528
|
+
"secret-tool",
|
|
529
|
+
[
|
|
530
|
+
"store",
|
|
531
|
+
"--label",
|
|
532
|
+
KEYCHAIN_LABEL,
|
|
533
|
+
"service",
|
|
534
|
+
KEYCHAIN_SERVICE,
|
|
535
|
+
"account",
|
|
536
|
+
key
|
|
537
|
+
],
|
|
538
|
+
{
|
|
539
|
+
input: value,
|
|
540
|
+
encoding: "utf8"
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
return result.status === 0;
|
|
544
|
+
}
|
|
545
|
+
if (backend === "windows-dpapi") {
|
|
546
|
+
const encrypted = dpapiProtect(value);
|
|
547
|
+
const secrets = readSecretsFile();
|
|
548
|
+
secrets[key] = encrypted;
|
|
549
|
+
writeSecretsFile(secrets);
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
if (backend === "insecure-file") {
|
|
553
|
+
const secrets = readSecretsFile();
|
|
554
|
+
secrets[key] = value;
|
|
555
|
+
writeSecretsFile(secrets);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
memorySecrets.set(key, value);
|
|
559
|
+
return false;
|
|
560
|
+
} catch {
|
|
561
|
+
memorySecrets.set(key, value);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
var getSecret = (key) => {
|
|
566
|
+
const backend = detectSecretBackend();
|
|
567
|
+
try {
|
|
568
|
+
if (backend === "macos-keychain") {
|
|
569
|
+
return execFileSync("security", [
|
|
570
|
+
"find-generic-password",
|
|
571
|
+
"-a",
|
|
572
|
+
key,
|
|
573
|
+
"-s",
|
|
574
|
+
KEYCHAIN_SERVICE,
|
|
575
|
+
"-w"
|
|
576
|
+
], { encoding: "utf8" }).trim();
|
|
577
|
+
}
|
|
578
|
+
if (backend === "linux-libsecret") {
|
|
579
|
+
return execFileSync(
|
|
580
|
+
"secret-tool",
|
|
581
|
+
["lookup", "service", KEYCHAIN_SERVICE, "account", key],
|
|
582
|
+
{ encoding: "utf8" }
|
|
583
|
+
).trim();
|
|
584
|
+
}
|
|
585
|
+
if (backend === "windows-dpapi") {
|
|
586
|
+
const secrets = readSecretsFile();
|
|
587
|
+
const encrypted = secrets[key];
|
|
588
|
+
if (!encrypted) {
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
591
|
+
return dpapiUnprotect(encrypted);
|
|
592
|
+
}
|
|
593
|
+
if (backend === "insecure-file") {
|
|
594
|
+
const secrets = readSecretsFile();
|
|
595
|
+
return secrets[key];
|
|
596
|
+
}
|
|
597
|
+
return memorySecrets.get(key);
|
|
598
|
+
} catch {
|
|
599
|
+
return memorySecrets.get(key);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
var deleteSecret = (key) => {
|
|
603
|
+
const backend = detectSecretBackend();
|
|
604
|
+
try {
|
|
605
|
+
if (backend === "macos-keychain") {
|
|
606
|
+
execFileSync(
|
|
607
|
+
"security",
|
|
608
|
+
[
|
|
609
|
+
"delete-generic-password",
|
|
610
|
+
"-a",
|
|
611
|
+
key,
|
|
612
|
+
"-s",
|
|
613
|
+
KEYCHAIN_SERVICE
|
|
614
|
+
],
|
|
615
|
+
{ stdio: "ignore" }
|
|
616
|
+
);
|
|
617
|
+
} else if (backend === "linux-libsecret") {
|
|
618
|
+
execFileSync(
|
|
619
|
+
"secret-tool",
|
|
620
|
+
[
|
|
621
|
+
"clear",
|
|
622
|
+
"service",
|
|
623
|
+
KEYCHAIN_SERVICE,
|
|
624
|
+
"account",
|
|
625
|
+
key
|
|
626
|
+
],
|
|
627
|
+
{ stdio: "ignore" }
|
|
628
|
+
);
|
|
629
|
+
} else if (backend === "windows-dpapi" || backend === "insecure-file") {
|
|
630
|
+
const secrets = readSecretsFile();
|
|
631
|
+
if (secrets[key]) {
|
|
632
|
+
delete secrets[key];
|
|
633
|
+
writeSecretsFile(secrets);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
memorySecrets.delete(key);
|
|
639
|
+
};
|
|
640
|
+
var warnOnInsecureSecretStorage = () => {
|
|
641
|
+
if (warnedAboutSecretBackend) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
warnedAboutSecretBackend = true;
|
|
645
|
+
const backend = detectSecretBackend();
|
|
646
|
+
if (backend === "memory") {
|
|
647
|
+
console.warn(
|
|
648
|
+
"Secure credential storage is unavailable on this system. Session refresh tokens will not persist across CLI restarts."
|
|
649
|
+
);
|
|
650
|
+
console.warn(
|
|
651
|
+
`To force file fallback (less secure), set ${INSECURE_FILE_FALLBACK_ENV}=1.`
|
|
652
|
+
);
|
|
653
|
+
} else if (backend === "insecure-file") {
|
|
654
|
+
console.warn(
|
|
655
|
+
`Using insecure file secret fallback because ${INSECURE_FILE_FALLBACK_ENV}=1 is set.`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
var isLocalHostname = (hostname) => {
|
|
660
|
+
const lower = hostname.toLowerCase();
|
|
661
|
+
return lower === "localhost" || lower === "127.0.0.1" || lower === "::1" || lower === "[::1]";
|
|
662
|
+
};
|
|
663
|
+
var assertSecureBaseUrl = (rawBaseUrl) => {
|
|
664
|
+
let parsed;
|
|
665
|
+
try {
|
|
666
|
+
parsed = new URL(rawBaseUrl);
|
|
667
|
+
} catch {
|
|
668
|
+
throw new Error(`Invalid base URL: ${rawBaseUrl}`);
|
|
669
|
+
}
|
|
670
|
+
if (parsed.protocol === "https:") {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (parsed.protocol === "http:" && isLocalHostname(parsed.hostname)) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
throw new Error(
|
|
677
|
+
`Refusing insecure base URL (${rawBaseUrl}). Use HTTPS for non-localhost endpoints.`
|
|
678
|
+
);
|
|
679
|
+
};
|
|
680
|
+
var normalizeApiBase = (baseUrl) => {
|
|
681
|
+
const raw = baseUrl || process.env.CLOUDEVAL_BASE_URL || DEFAULT_BASE_URL;
|
|
682
|
+
assertSecureBaseUrl(raw);
|
|
683
|
+
const trimmed = raw.replace(/\/+$/, "");
|
|
684
|
+
try {
|
|
685
|
+
const url = new URL(trimmed);
|
|
686
|
+
const hostname = url.hostname.toLowerCase();
|
|
687
|
+
const path2 = url.pathname.replace(/\/+$/, "") || "/";
|
|
688
|
+
if (hostname === "cloudeval.ai" && ["/", "/api", "/api/v1"].includes(path2)) {
|
|
689
|
+
url.pathname = "/api/proxy/v1";
|
|
690
|
+
url.search = "";
|
|
691
|
+
url.hash = "";
|
|
692
|
+
return url.toString().replace(/\/+$/, "");
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
if (trimmed.endsWith("/api/v1") || trimmed.endsWith("/api/proxy/v1")) {
|
|
697
|
+
return trimmed;
|
|
698
|
+
}
|
|
699
|
+
if (trimmed.endsWith("/api/proxy")) {
|
|
700
|
+
return `${trimmed}/v1`;
|
|
701
|
+
}
|
|
702
|
+
return trimmed.replace(/\/api\/?$/, "") + "/api/v1";
|
|
703
|
+
};
|
|
704
|
+
var resolveAuthBootstrapBase = (apiBase) => {
|
|
705
|
+
try {
|
|
706
|
+
const url = new URL(apiBase);
|
|
707
|
+
const path2 = url.pathname.replace(/\/+$/, "");
|
|
708
|
+
if (url.hostname.toLowerCase() === "cloudeval.ai" && /\/api\/proxy\/v1$/i.test(path2)) {
|
|
709
|
+
url.pathname = path2.replace(/\/api\/proxy\/v1$/i, "/api/v1");
|
|
710
|
+
url.search = "";
|
|
711
|
+
url.hash = "";
|
|
712
|
+
return url.toString().replace(/\/+$/, "");
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
return apiBase;
|
|
717
|
+
};
|
|
718
|
+
var sanitizeStoredForDisk = (data) => {
|
|
719
|
+
const clone = { ...data };
|
|
720
|
+
delete clone.token;
|
|
721
|
+
delete clone.refreshToken;
|
|
722
|
+
return clone;
|
|
723
|
+
};
|
|
724
|
+
var migrateLegacySecrets = (parsed) => {
|
|
725
|
+
const migrated = { ...parsed };
|
|
726
|
+
if (parsed.token && !parsed.tokenRef) {
|
|
727
|
+
const ref = ACCESS_SECRET_LABEL;
|
|
728
|
+
const persisted = setSecret(ref, parsed.token);
|
|
729
|
+
if (!persisted) {
|
|
730
|
+
warnOnInsecureSecretStorage();
|
|
731
|
+
}
|
|
732
|
+
migrated.tokenRef = ref;
|
|
733
|
+
delete migrated.token;
|
|
734
|
+
}
|
|
735
|
+
if (parsed.refreshToken && !parsed.refreshTokenRef) {
|
|
736
|
+
const ref = REFRESH_SECRET_LABEL;
|
|
737
|
+
const persisted = setSecret(ref, parsed.refreshToken);
|
|
738
|
+
if (!persisted) {
|
|
739
|
+
warnOnInsecureSecretStorage();
|
|
740
|
+
}
|
|
741
|
+
migrated.refreshTokenRef = ref;
|
|
742
|
+
delete migrated.refreshToken;
|
|
743
|
+
}
|
|
744
|
+
return migrated;
|
|
745
|
+
};
|
|
746
|
+
var loadStoredFromDisk = () => {
|
|
747
|
+
try {
|
|
748
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
749
|
+
const parsed = JSON.parse(raw);
|
|
750
|
+
const nextStored = migrateLegacySecrets(parsed);
|
|
751
|
+
const accessToken = getAccessToken(nextStored);
|
|
752
|
+
if (accessToken) {
|
|
753
|
+
cachedToken = {
|
|
754
|
+
token: accessToken,
|
|
755
|
+
expiresAt: nextStored.tokenExpiresAt ?? 0
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (nextStored !== parsed) {
|
|
759
|
+
const sanitized = sanitizeStoredForDisk(nextStored);
|
|
760
|
+
fs.writeFileSync(configPath, JSON.stringify(sanitized, null, 2), {
|
|
761
|
+
encoding: "utf8",
|
|
762
|
+
mode: 384
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return nextStored;
|
|
766
|
+
} catch {
|
|
767
|
+
return {};
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var readStored = () => {
|
|
771
|
+
if (stored) {
|
|
772
|
+
return stored;
|
|
773
|
+
}
|
|
774
|
+
stored = loadStoredFromDisk();
|
|
775
|
+
return stored;
|
|
776
|
+
};
|
|
777
|
+
var reloadStored = () => {
|
|
778
|
+
stored = loadStoredFromDisk();
|
|
779
|
+
return stored;
|
|
780
|
+
};
|
|
781
|
+
var writeStored = (data) => {
|
|
782
|
+
const sanitized = sanitizeStoredForDisk(data);
|
|
783
|
+
try {
|
|
784
|
+
ensureConfigDir();
|
|
785
|
+
fs.writeFileSync(configPath, JSON.stringify(sanitized, null, 2), {
|
|
786
|
+
encoding: "utf8",
|
|
787
|
+
mode: 384
|
|
788
|
+
});
|
|
789
|
+
try {
|
|
790
|
+
fs.chmodSync(configPath, 384);
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
stored = sanitized;
|
|
794
|
+
} catch {
|
|
795
|
+
stored = sanitized;
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
var getRefreshToken = (data) => {
|
|
799
|
+
if (data.refreshTokenRef) {
|
|
800
|
+
return getSecret(data.refreshTokenRef);
|
|
801
|
+
}
|
|
802
|
+
return void 0;
|
|
803
|
+
};
|
|
804
|
+
var getAccessToken = (data) => {
|
|
805
|
+
if (data.tokenRef) {
|
|
806
|
+
return getSecret(data.tokenRef);
|
|
807
|
+
}
|
|
808
|
+
return data.token;
|
|
809
|
+
};
|
|
810
|
+
var saveAccessToken = (data, accessToken) => {
|
|
811
|
+
const next = { ...data };
|
|
812
|
+
if (!accessToken) {
|
|
813
|
+
return next;
|
|
814
|
+
}
|
|
815
|
+
const ref = next.tokenRef || ACCESS_SECRET_LABEL;
|
|
816
|
+
const persisted = setSecret(ref, accessToken);
|
|
817
|
+
if (!persisted) {
|
|
818
|
+
warnOnInsecureSecretStorage();
|
|
819
|
+
}
|
|
820
|
+
next.tokenRef = ref;
|
|
821
|
+
return next;
|
|
822
|
+
};
|
|
823
|
+
var saveRefreshToken = (data, refreshToken) => {
|
|
824
|
+
const next = { ...data };
|
|
825
|
+
if (!refreshToken) {
|
|
826
|
+
return next;
|
|
827
|
+
}
|
|
828
|
+
const ref = next.refreshTokenRef || REFRESH_SECRET_LABEL;
|
|
829
|
+
const persisted = setSecret(ref, refreshToken);
|
|
830
|
+
if (!persisted) {
|
|
831
|
+
warnOnInsecureSecretStorage();
|
|
832
|
+
}
|
|
833
|
+
next.refreshTokenRef = ref;
|
|
834
|
+
return next;
|
|
835
|
+
};
|
|
836
|
+
var clearLocalAuth = (data) => {
|
|
837
|
+
cachedToken = null;
|
|
838
|
+
refreshInFlight = null;
|
|
839
|
+
const current = data ?? readStored();
|
|
840
|
+
if (current.tokenRef) {
|
|
841
|
+
deleteSecret(current.tokenRef);
|
|
842
|
+
}
|
|
843
|
+
if (current.refreshTokenRef) {
|
|
844
|
+
deleteSecret(current.refreshTokenRef);
|
|
845
|
+
}
|
|
846
|
+
stored = {};
|
|
847
|
+
writeStored({});
|
|
848
|
+
try {
|
|
849
|
+
if (fs.existsSync(configPath)) {
|
|
850
|
+
fs.unlinkSync(configPath);
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
var clearAuthSession = () => {
|
|
856
|
+
clearLocalAuth();
|
|
857
|
+
};
|
|
858
|
+
var setCachedToken = (token, expiresInSeconds) => {
|
|
859
|
+
cachedToken = {
|
|
860
|
+
token,
|
|
861
|
+
expiresAt: now() + expiresInSeconds * 1e3 - TOKEN_EXPIRY_SKEW_MS
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
var persistAuthTokens = (tokenResponse, context) => {
|
|
865
|
+
setCachedToken(tokenResponse.access_token, tokenResponse.expires_in ?? 3600);
|
|
866
|
+
const current = readStored();
|
|
867
|
+
let next = {
|
|
868
|
+
...current,
|
|
869
|
+
token: tokenResponse.access_token,
|
|
870
|
+
tokenExpiresAt: cachedToken?.expiresAt,
|
|
871
|
+
sessionId: tokenResponse.session_id ?? current.sessionId,
|
|
872
|
+
accountId: tokenResponse.account_id ?? current.accountId,
|
|
873
|
+
baseUrl: context.baseUrl,
|
|
874
|
+
lastRefreshAt: now()
|
|
875
|
+
};
|
|
876
|
+
next = saveAccessToken(next, tokenResponse.access_token);
|
|
877
|
+
next = saveRefreshToken(next, tokenResponse.refresh_token);
|
|
878
|
+
writeStored(next);
|
|
879
|
+
return tokenResponse.access_token;
|
|
880
|
+
};
|
|
881
|
+
var getCLIClientId = () => process.env.CLOUDEVAL_CLI_CLIENT_ID ?? "cloudeval-cli";
|
|
882
|
+
var getDeviceVerificationOverride = () => (process.env.CLOUDEVAL_DEVICE_VERIFICATION_URI || process.env.CLOUDEVAL_FRONTEND_URL || process.env.CLOUDEVAL_WEB_URL || "").trim();
|
|
883
|
+
var DEVICE_LOGIN_PROMPT = "select_account";
|
|
884
|
+
var forceDeviceLoginAccountChooser = (value) => {
|
|
885
|
+
const url = new URL(value);
|
|
886
|
+
url.searchParams.set("prompt", DEVICE_LOGIN_PROMPT);
|
|
887
|
+
return url.toString();
|
|
888
|
+
};
|
|
889
|
+
var buildDeviceVerificationUrl = (override, userCode) => {
|
|
890
|
+
const url = new URL(override);
|
|
891
|
+
if (!url.pathname || url.pathname === "/") {
|
|
892
|
+
url.pathname = "/device/login";
|
|
893
|
+
}
|
|
894
|
+
if (userCode) {
|
|
895
|
+
url.searchParams.set("user_code", userCode);
|
|
896
|
+
}
|
|
897
|
+
url.searchParams.set("prompt", DEVICE_LOGIN_PROMPT);
|
|
898
|
+
return url.toString();
|
|
899
|
+
};
|
|
900
|
+
var isLocalDeviceVerificationUrl = (value) => {
|
|
901
|
+
try {
|
|
902
|
+
const url = new URL(value);
|
|
903
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
904
|
+
} catch {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var resolveDeviceVerificationUrl = (deviceCodeData) => {
|
|
909
|
+
const override = getDeviceVerificationOverride();
|
|
910
|
+
if (override) {
|
|
911
|
+
try {
|
|
912
|
+
return buildDeviceVerificationUrl(override, deviceCodeData.user_code);
|
|
913
|
+
} catch {
|
|
914
|
+
console.warn(
|
|
915
|
+
`Ignoring invalid device verification URL override: ${override}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const backendUrl = deviceCodeData.verification_uri_complete || (() => {
|
|
920
|
+
try {
|
|
921
|
+
return buildDeviceVerificationUrl(
|
|
922
|
+
deviceCodeData.verification_uri,
|
|
923
|
+
deviceCodeData.user_code
|
|
924
|
+
);
|
|
925
|
+
} catch {
|
|
926
|
+
return deviceCodeData.verification_uri;
|
|
927
|
+
}
|
|
928
|
+
})();
|
|
929
|
+
if (isLocalDeviceVerificationUrl(backendUrl)) {
|
|
930
|
+
return buildDeviceVerificationUrl(DEFAULT_FRONTEND_URL, deviceCodeData.user_code);
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
return forceDeviceLoginAccountChooser(backendUrl);
|
|
934
|
+
} catch {
|
|
935
|
+
return backendUrl;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
var openBrowser = (url) => {
|
|
939
|
+
try {
|
|
940
|
+
if (process.platform === "darwin") {
|
|
941
|
+
const child = spawn("open", [url], {
|
|
942
|
+
detached: true,
|
|
943
|
+
stdio: "ignore"
|
|
944
|
+
});
|
|
945
|
+
child.unref();
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (process.platform === "win32") {
|
|
949
|
+
const child = spawn("cmd", ["/c", "start", "", url], {
|
|
950
|
+
detached: true,
|
|
951
|
+
stdio: "ignore"
|
|
952
|
+
});
|
|
953
|
+
child.unref();
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
if (commandExists("xdg-open")) {
|
|
957
|
+
const child = spawn("xdg-open", [url], {
|
|
958
|
+
detached: true,
|
|
959
|
+
stdio: "ignore"
|
|
960
|
+
});
|
|
961
|
+
child.unref();
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
return false;
|
|
965
|
+
} catch {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
var PLAYGROUND_PROJECT_NAME = "Playground";
|
|
970
|
+
var getCLIHeaders = (token) => {
|
|
971
|
+
const headers = {
|
|
972
|
+
"X-Client-Type": "cloudeval-cli",
|
|
973
|
+
"X-Client-Version": "0.1.0",
|
|
974
|
+
"Content-Type": "application/json"
|
|
975
|
+
};
|
|
976
|
+
if (token) {
|
|
977
|
+
headers.Authorization = `Bearer ${token}`;
|
|
978
|
+
}
|
|
979
|
+
return headers;
|
|
980
|
+
};
|
|
981
|
+
var getProjects = async (baseUrl, token, userId) => {
|
|
982
|
+
try {
|
|
983
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
984
|
+
cliDebug("getProjects request", {
|
|
985
|
+
url: `${apiBase}/projects/user/${userId}`,
|
|
986
|
+
userId
|
|
987
|
+
});
|
|
988
|
+
const response = await fetch(`${apiBase}/projects/user/${userId}`, {
|
|
989
|
+
method: "GET",
|
|
990
|
+
headers: getCLIHeaders(token)
|
|
991
|
+
});
|
|
992
|
+
cliDebug("getProjects response", {
|
|
993
|
+
status: response.status,
|
|
994
|
+
ok: response.ok
|
|
995
|
+
});
|
|
996
|
+
if (!response.ok) {
|
|
997
|
+
if (response.status === 404) {
|
|
998
|
+
return [];
|
|
999
|
+
}
|
|
1000
|
+
throw new Error(`Failed to fetch projects: ${response.status}`);
|
|
1001
|
+
}
|
|
1002
|
+
const projects = await response.json();
|
|
1003
|
+
const parsedProjects = Array.isArray(projects) ? projects : [];
|
|
1004
|
+
cliDebug("getProjects parsed", {
|
|
1005
|
+
count: parsedProjects.length,
|
|
1006
|
+
names: parsedProjects.map((project) => project?.name)
|
|
1007
|
+
});
|
|
1008
|
+
return parsedProjects;
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
cliDebug("getProjects failed", {
|
|
1011
|
+
message: error?.message
|
|
1012
|
+
});
|
|
1013
|
+
console.warn("Failed to fetch projects:", error.message);
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
var getAccessibleProjects = async (baseUrl, token) => {
|
|
1018
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
1019
|
+
cliDebug("getAccessibleProjects request", {
|
|
1020
|
+
url: `${apiBase}/projects/`
|
|
1021
|
+
});
|
|
1022
|
+
const response = await fetch(`${apiBase}/projects/`, {
|
|
1023
|
+
method: "GET",
|
|
1024
|
+
headers: getCLIHeaders(token)
|
|
1025
|
+
});
|
|
1026
|
+
cliDebug("getAccessibleProjects response", {
|
|
1027
|
+
status: response.status,
|
|
1028
|
+
ok: response.ok
|
|
1029
|
+
});
|
|
1030
|
+
if (!response.ok) {
|
|
1031
|
+
if (response.status === 404) {
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
throw new Error(`Failed to fetch projects: ${response.status}`);
|
|
1035
|
+
}
|
|
1036
|
+
const projects = await response.json();
|
|
1037
|
+
return Array.isArray(projects) ? projects : [];
|
|
1038
|
+
};
|
|
1039
|
+
var getPlaygroundProject = (projects) => projects.find((project) => project.name === PLAYGROUND_PROJECT_NAME);
|
|
1040
|
+
var getUserDisplayName = (user) => user.fullName || user.full_name || user.name;
|
|
1041
|
+
var quickOnboardPlayground = async (baseUrl, token, user, extraPayload = {}) => {
|
|
1042
|
+
if (!user.email) {
|
|
1043
|
+
throw new Error(
|
|
1044
|
+
"Playground project is missing and the authenticated user email is unavailable. Please login again."
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
1048
|
+
const fullName = getUserDisplayName(user);
|
|
1049
|
+
const body = {
|
|
1050
|
+
email: user.email,
|
|
1051
|
+
...fullName ? { full_name: fullName } : {},
|
|
1052
|
+
...extraPayload
|
|
1053
|
+
};
|
|
1054
|
+
cliDebug("quickOnboardPlayground request", {
|
|
1055
|
+
url: `${apiBase}/onboard/quick`,
|
|
1056
|
+
email: user.email,
|
|
1057
|
+
hasFullName: !!fullName,
|
|
1058
|
+
payloadKeys: Object.keys(body)
|
|
1059
|
+
});
|
|
1060
|
+
const response = await fetch(`${apiBase}/onboard/quick`, {
|
|
1061
|
+
method: "POST",
|
|
1062
|
+
headers: getCLIHeaders(token),
|
|
1063
|
+
body: JSON.stringify(body)
|
|
1064
|
+
});
|
|
1065
|
+
cliDebug("quickOnboardPlayground response", {
|
|
1066
|
+
status: response.status,
|
|
1067
|
+
ok: response.ok
|
|
1068
|
+
});
|
|
1069
|
+
if (!response.ok) {
|
|
1070
|
+
const detail = await readResponseDetail(response);
|
|
1071
|
+
cliDebug("quickOnboardPlayground failed", {
|
|
1072
|
+
status: response.status,
|
|
1073
|
+
detail
|
|
1074
|
+
});
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Failed to run shared Playground onboarding: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ""}`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const data = await response.json();
|
|
1080
|
+
cliDebug("quickOnboardPlayground parsed", {
|
|
1081
|
+
userId: data.user?.id,
|
|
1082
|
+
hasPlaygroundProject: !!data.playground_project?.id,
|
|
1083
|
+
hasDefaultConnection: !!data.default_connection?.id,
|
|
1084
|
+
setupJobs: data.setup_jobs?.map((job) => job.operation)
|
|
1085
|
+
});
|
|
1086
|
+
return data;
|
|
1087
|
+
};
|
|
1088
|
+
var ensurePlaygroundProject = async (baseUrl, token, user, options = {}) => {
|
|
1089
|
+
cliDebug("ensurePlaygroundProject start", {
|
|
1090
|
+
userId: user.id,
|
|
1091
|
+
email: user.email,
|
|
1092
|
+
forceQuickOnboard: !!options.forceQuickOnboard
|
|
1093
|
+
});
|
|
1094
|
+
const existingProjects = await getProjects(baseUrl, token, user.id);
|
|
1095
|
+
const existingPlayground = getPlaygroundProject(existingProjects);
|
|
1096
|
+
if (existingPlayground && !options.forceQuickOnboard) {
|
|
1097
|
+
cliDebug("ensurePlaygroundProject existing Playground found", {
|
|
1098
|
+
projectId: existingPlayground.id
|
|
1099
|
+
});
|
|
1100
|
+
return existingPlayground;
|
|
1101
|
+
}
|
|
1102
|
+
const onboardResponse = await quickOnboardPlayground(baseUrl, token, user);
|
|
1103
|
+
if (onboardResponse.playground_project?.id) {
|
|
1104
|
+
cliDebug("ensurePlaygroundProject repaired from quick response", {
|
|
1105
|
+
projectId: onboardResponse.playground_project.id
|
|
1106
|
+
});
|
|
1107
|
+
return onboardResponse.playground_project;
|
|
1108
|
+
}
|
|
1109
|
+
const refreshedProjects = await getProjects(baseUrl, token, user.id);
|
|
1110
|
+
const refreshedPlayground = getPlaygroundProject(refreshedProjects);
|
|
1111
|
+
if (refreshedPlayground) {
|
|
1112
|
+
cliDebug("ensurePlaygroundProject repaired after project refetch", {
|
|
1113
|
+
projectId: refreshedPlayground.id
|
|
1114
|
+
});
|
|
1115
|
+
return refreshedPlayground;
|
|
1116
|
+
}
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
"Shared onboarding completed, but no Playground project was returned or found."
|
|
1119
|
+
);
|
|
1120
|
+
};
|
|
1121
|
+
var ensureDefaultProject = async (baseUrl, token, user) => ensurePlaygroundProject(
|
|
1122
|
+
baseUrl,
|
|
1123
|
+
token,
|
|
1124
|
+
typeof user === "string" ? { id: user } : user
|
|
1125
|
+
);
|
|
1126
|
+
var readResponseDetail = async (response) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const raw = await response.text();
|
|
1129
|
+
if (!raw || !raw.trim()) {
|
|
1130
|
+
return void 0;
|
|
1131
|
+
}
|
|
1132
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1133
|
+
if (contentType.includes("text/html") || /^\s*</.test(raw)) {
|
|
1134
|
+
return "backend returned an HTML error page; check --base-url/CLOUDEVAL_BASE_URL and backend health";
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
const json = JSON.parse(raw);
|
|
1138
|
+
return json.message || json.error_description || json.error || json.detail || raw;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return raw;
|
|
1141
|
+
}
|
|
1142
|
+
} catch {
|
|
1143
|
+
return void 0;
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
var loginWithDeviceCode = async (baseUrl, options = {}) => {
|
|
1147
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
1148
|
+
const authBase = resolveAuthBootstrapBase(apiBase);
|
|
1149
|
+
const clientId = getCLIClientId();
|
|
1150
|
+
const requestBody = JSON.stringify({ client_id: clientId });
|
|
1151
|
+
cliDebug("backend device-code request", {
|
|
1152
|
+
url: `${authBase}/auth/device/code`,
|
|
1153
|
+
clientId
|
|
1154
|
+
});
|
|
1155
|
+
const deviceCodeResponse = await fetch(`${authBase}/auth/device/code`, {
|
|
1156
|
+
method: "POST",
|
|
1157
|
+
headers: getCLIHeaders(),
|
|
1158
|
+
body: requestBody
|
|
1159
|
+
});
|
|
1160
|
+
cliDebug("backend device-code response", {
|
|
1161
|
+
status: deviceCodeResponse.status,
|
|
1162
|
+
ok: deviceCodeResponse.ok
|
|
1163
|
+
});
|
|
1164
|
+
if (!deviceCodeResponse.ok) {
|
|
1165
|
+
const statusInfo = `${deviceCodeResponse.status} ${deviceCodeResponse.statusText}`;
|
|
1166
|
+
const detail = await readResponseDetail(deviceCodeResponse);
|
|
1167
|
+
if (deviceCodeResponse.status === 401 || deviceCodeResponse.status === 403) {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
`CloudEval backend device-code login is blocked by an authentication layer (${statusInfo}${detail ? ` - ${detail}` : ""}). The CLI needs /api/v1/auth/device/code to stay public so it can show the CloudEval approval URL.`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
let errorMessage2 = `Failed to initiate login: ${statusInfo}`;
|
|
1173
|
+
if (detail) {
|
|
1174
|
+
errorMessage2 = `Failed to initiate login: ${statusInfo} - ${detail}`;
|
|
1175
|
+
}
|
|
1176
|
+
throw new Error(errorMessage2);
|
|
1177
|
+
}
|
|
1178
|
+
return pollDeviceCodeAndPersist(authBase, clientId, deviceCodeResponse, {
|
|
1179
|
+
openInBrowser: options.openInBrowser,
|
|
1180
|
+
browserOpener: options.browserOpener,
|
|
1181
|
+
persistBaseUrl: apiBase
|
|
1182
|
+
});
|
|
1183
|
+
};
|
|
1184
|
+
var pollDeviceCodeAndPersist = async (apiBase, clientId, deviceCodeResponse, options = {}) => {
|
|
1185
|
+
const deviceCodeData = await deviceCodeResponse.json();
|
|
1186
|
+
const verificationUrl = resolveDeviceVerificationUrl(deviceCodeData);
|
|
1187
|
+
const browserOpener = options.browserOpener ?? openBrowser;
|
|
1188
|
+
let openedInBrowser = false;
|
|
1189
|
+
cliDebug("backend device-code parsed", {
|
|
1190
|
+
hasVerificationUriComplete: !!deviceCodeData.verification_uri_complete,
|
|
1191
|
+
verificationUrl,
|
|
1192
|
+
userCode: deviceCodeData.user_code,
|
|
1193
|
+
expiresIn: deviceCodeData.expires_in,
|
|
1194
|
+
interval: deviceCodeData.interval
|
|
1195
|
+
});
|
|
1196
|
+
if (options.openInBrowser && verificationUrl) {
|
|
1197
|
+
console.log("\nOpening browser for authentication...");
|
|
1198
|
+
openedInBrowser = browserOpener(verificationUrl);
|
|
1199
|
+
console.log(`Approval URL: ${verificationUrl}`);
|
|
1200
|
+
}
|
|
1201
|
+
if (!openedInBrowser) {
|
|
1202
|
+
console.log("\nTo sign in, use a web browser to open:");
|
|
1203
|
+
console.log(` ${verificationUrl}`);
|
|
1204
|
+
}
|
|
1205
|
+
console.log(
|
|
1206
|
+
`
|
|
1207
|
+
${openedInBrowser ? "If prompted, enter code" : "Enter code"}: ${deviceCodeData.user_code}
|
|
1208
|
+
`
|
|
1209
|
+
);
|
|
1210
|
+
console.log("Waiting for authentication...");
|
|
1211
|
+
process.stdout.write(" ");
|
|
1212
|
+
const startTime = now();
|
|
1213
|
+
const expiresAt = startTime + deviceCodeData.expires_in * 1e3;
|
|
1214
|
+
let intervalMs = Math.max(1, deviceCodeData.interval || 5) * 1e3;
|
|
1215
|
+
while (now() < expiresAt) {
|
|
1216
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1217
|
+
const tokenResponse = await fetch(`${apiBase}/auth/device/token`, {
|
|
1218
|
+
method: "POST",
|
|
1219
|
+
headers: getCLIHeaders(),
|
|
1220
|
+
body: JSON.stringify({
|
|
1221
|
+
device_code: deviceCodeData.device_code,
|
|
1222
|
+
client_id: clientId
|
|
1223
|
+
})
|
|
1224
|
+
});
|
|
1225
|
+
cliDebug("backend device-token response", {
|
|
1226
|
+
status: tokenResponse.status,
|
|
1227
|
+
ok: tokenResponse.ok
|
|
1228
|
+
});
|
|
1229
|
+
const tokenResponseForDetail = tokenResponse.clone();
|
|
1230
|
+
let tokenData;
|
|
1231
|
+
try {
|
|
1232
|
+
tokenData = await tokenResponse.json();
|
|
1233
|
+
} catch {
|
|
1234
|
+
const detail = await readResponseDetail(tokenResponseForDetail);
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`Device token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}${detail ? ` - ${detail}` : ""}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (tokenResponse.ok && tokenData.access_token) {
|
|
1240
|
+
const accessToken = persistAuthTokens(tokenData, {
|
|
1241
|
+
baseUrl: options.persistBaseUrl ?? apiBase
|
|
1242
|
+
});
|
|
1243
|
+
cliDebug("backend device-token completed", {
|
|
1244
|
+
hasRefreshToken: !!tokenData.refresh_token,
|
|
1245
|
+
sessionId: tokenData.session_id,
|
|
1246
|
+
accountId: tokenData.account_id
|
|
1247
|
+
});
|
|
1248
|
+
console.log("\nAuthentication successful. Session saved.\n");
|
|
1249
|
+
return accessToken;
|
|
1250
|
+
}
|
|
1251
|
+
if (tokenData.error === "authorization_pending") {
|
|
1252
|
+
process.stdout.write(".");
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (tokenData.error === "slow_down" && tokenData.interval) {
|
|
1256
|
+
intervalMs = tokenData.interval * 1e3;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (tokenData.error) {
|
|
1260
|
+
throw new Error(tokenData.error);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
throw new Error("Authentication timeout. Please try again.");
|
|
1264
|
+
};
|
|
1265
|
+
var login = async (baseUrl, options = {}) => {
|
|
1266
|
+
return loginWithDeviceCode(baseUrl, {
|
|
1267
|
+
openInBrowser: !options.headless,
|
|
1268
|
+
browserOpener: options.browserOpener
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1271
|
+
var refreshViaBackend = async (apiBase, refreshToken) => {
|
|
1272
|
+
const authBase = resolveAuthBootstrapBase(apiBase);
|
|
1273
|
+
const response = await fetch(`${authBase}/auth/refresh`, {
|
|
1274
|
+
method: "POST",
|
|
1275
|
+
headers: getCLIHeaders(),
|
|
1276
|
+
body: JSON.stringify({
|
|
1277
|
+
refresh_token: refreshToken,
|
|
1278
|
+
client_id: getCLIClientId()
|
|
1279
|
+
})
|
|
1280
|
+
});
|
|
1281
|
+
if (response.status === 404 || response.status === 405) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
if (!response.ok) {
|
|
1285
|
+
const errorText = await response.text();
|
|
1286
|
+
const lowerErrorText = errorText.toLowerCase();
|
|
1287
|
+
if ((response.status === 401 || response.status === 403) && (lowerErrorText.includes("auth_required_public") || lowerErrorText.includes("authentication required for this endpoint"))) {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
throw new Error(errorText || "Token refresh failed");
|
|
1291
|
+
}
|
|
1292
|
+
return await response.json();
|
|
1293
|
+
};
|
|
1294
|
+
var refreshAuthToken = async (refreshToken, baseUrl) => {
|
|
1295
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
1296
|
+
const backendResponse = await refreshViaBackend(apiBase, refreshToken);
|
|
1297
|
+
if (backendResponse) {
|
|
1298
|
+
return backendResponse;
|
|
1299
|
+
}
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
"Token refresh unavailable from CloudEval backend. Run 'cloudeval login' and retry."
|
|
1302
|
+
);
|
|
1303
|
+
};
|
|
1304
|
+
var waitForConcurrentRefreshToken = async (previousRefreshToken) => {
|
|
1305
|
+
for (const delayMs of CONCURRENT_REFRESH_WAIT_STEPS_MS) {
|
|
1306
|
+
await sleep(delayMs);
|
|
1307
|
+
const latest2 = reloadStored();
|
|
1308
|
+
const latestRefreshToken2 = getRefreshToken(latest2);
|
|
1309
|
+
if (latestRefreshToken2 && latestRefreshToken2 !== previousRefreshToken) {
|
|
1310
|
+
return latestRefreshToken2;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const latest = reloadStored();
|
|
1314
|
+
const latestRefreshToken = getRefreshToken(latest);
|
|
1315
|
+
if (latestRefreshToken && latestRefreshToken !== previousRefreshToken) {
|
|
1316
|
+
return latestRefreshToken;
|
|
1317
|
+
}
|
|
1318
|
+
return void 0;
|
|
1319
|
+
};
|
|
1320
|
+
var resolveRefreshBaseUrl = (requestedBaseUrl, storedBaseUrl) => {
|
|
1321
|
+
if (!requestedBaseUrl) {
|
|
1322
|
+
return storedBaseUrl;
|
|
1323
|
+
}
|
|
1324
|
+
if (storedBaseUrl && normalizeApiBase(requestedBaseUrl) === DEFAULT_BASE_URL) {
|
|
1325
|
+
return storedBaseUrl;
|
|
1326
|
+
}
|
|
1327
|
+
return requestedBaseUrl;
|
|
1328
|
+
};
|
|
1329
|
+
var performRefresh = async (options) => {
|
|
1330
|
+
const disk = readStored();
|
|
1331
|
+
const refreshToken = getRefreshToken(disk);
|
|
1332
|
+
if (!refreshToken) {
|
|
1333
|
+
throw new Error("No refresh token available. Please run 'cloudeval login'.");
|
|
1334
|
+
}
|
|
1335
|
+
const refreshBaseUrl = resolveRefreshBaseUrl(options.baseUrl, disk.baseUrl);
|
|
1336
|
+
const finishRefresh = async (currentRefreshToken, currentBaseUrl) => {
|
|
1337
|
+
const refreshed = await refreshAuthToken(currentRefreshToken, currentBaseUrl);
|
|
1338
|
+
if (!refreshed.access_token) {
|
|
1339
|
+
throw new Error("Token refresh response missing access_token.");
|
|
1340
|
+
}
|
|
1341
|
+
return persistAuthTokens(refreshed, {
|
|
1342
|
+
baseUrl: normalizeApiBase(currentBaseUrl)
|
|
1343
|
+
});
|
|
1344
|
+
};
|
|
1345
|
+
try {
|
|
1346
|
+
return await finishRefresh(refreshToken, refreshBaseUrl);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
let latest = reloadStored();
|
|
1349
|
+
let latestRefreshToken = getRefreshToken(latest);
|
|
1350
|
+
if (!latestRefreshToken || latestRefreshToken === refreshToken) {
|
|
1351
|
+
const waitedRefreshToken = await waitForConcurrentRefreshToken(refreshToken);
|
|
1352
|
+
if (waitedRefreshToken) {
|
|
1353
|
+
latest = reloadStored();
|
|
1354
|
+
latestRefreshToken = waitedRefreshToken;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (latestRefreshToken && latestRefreshToken !== refreshToken) {
|
|
1358
|
+
const latestBaseUrl = resolveRefreshBaseUrl(options.baseUrl, latest.baseUrl) || refreshBaseUrl;
|
|
1359
|
+
return finishRefresh(latestRefreshToken, latestBaseUrl);
|
|
1360
|
+
}
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
var refreshWithSingleFlight = async (options) => {
|
|
1365
|
+
if (refreshInFlight) {
|
|
1366
|
+
return refreshInFlight;
|
|
1367
|
+
}
|
|
1368
|
+
refreshInFlight = (async () => {
|
|
1369
|
+
const releaseRefreshLock = await acquireRefreshLock();
|
|
1370
|
+
try {
|
|
1371
|
+
return await performRefresh(options);
|
|
1372
|
+
} finally {
|
|
1373
|
+
releaseRefreshLock();
|
|
1374
|
+
refreshInFlight = null;
|
|
1375
|
+
}
|
|
1376
|
+
})();
|
|
1377
|
+
return refreshInFlight;
|
|
1378
|
+
};
|
|
1379
|
+
var logout = async (options = {}) => {
|
|
1380
|
+
const disk = readStored();
|
|
1381
|
+
const refreshToken = getRefreshToken(disk);
|
|
1382
|
+
const currentToken = cachedToken?.token || getAccessToken(disk);
|
|
1383
|
+
let revoked = false;
|
|
1384
|
+
if (refreshToken) {
|
|
1385
|
+
try {
|
|
1386
|
+
const apiBase = normalizeApiBase(options.baseUrl || disk.baseUrl);
|
|
1387
|
+
const endpoint = options.allDevices ? "/auth/logout-all" : "/auth/logout";
|
|
1388
|
+
const response = await fetch(`${apiBase}${endpoint}`, {
|
|
1389
|
+
method: "POST",
|
|
1390
|
+
headers: getCLIHeaders(currentToken),
|
|
1391
|
+
body: JSON.stringify({
|
|
1392
|
+
refresh_token: refreshToken,
|
|
1393
|
+
session_id: disk.sessionId
|
|
1394
|
+
})
|
|
1395
|
+
});
|
|
1396
|
+
if (response.ok || response.status === 404 || response.status === 405) {
|
|
1397
|
+
revoked = response.ok;
|
|
1398
|
+
}
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
clearLocalAuth(disk);
|
|
1403
|
+
return { revoked, localCleared: true };
|
|
1404
|
+
};
|
|
1405
|
+
var getAuthStatus = async (baseUrl, options = {}) => {
|
|
1406
|
+
let disk = readStored();
|
|
1407
|
+
let refreshToken = getRefreshToken(disk);
|
|
1408
|
+
let accessToken = getAccessToken(disk);
|
|
1409
|
+
let accessTokenCached = Boolean(
|
|
1410
|
+
cachedToken && cachedToken.expiresAt > now() || accessToken && disk.tokenExpiresAt && disk.tokenExpiresAt > now()
|
|
1411
|
+
);
|
|
1412
|
+
let authenticated = Boolean(accessTokenCached || refreshToken);
|
|
1413
|
+
let authError;
|
|
1414
|
+
if (options.validate && authenticated) {
|
|
1415
|
+
try {
|
|
1416
|
+
const validationBaseUrl = resolveRefreshBaseUrl(baseUrl, disk.baseUrl);
|
|
1417
|
+
const token = await getAuthToken({ baseUrl: validationBaseUrl });
|
|
1418
|
+
if (validationBaseUrl) {
|
|
1419
|
+
await checkUserStatus(validationBaseUrl, token);
|
|
1420
|
+
}
|
|
1421
|
+
disk = readStored();
|
|
1422
|
+
refreshToken = getRefreshToken(disk);
|
|
1423
|
+
accessToken = getAccessToken(disk);
|
|
1424
|
+
accessTokenCached = Boolean(
|
|
1425
|
+
cachedToken && cachedToken.expiresAt > now() || accessToken && disk.tokenExpiresAt && disk.tokenExpiresAt > now()
|
|
1426
|
+
);
|
|
1427
|
+
authenticated = true;
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
authError = errorMessage(error);
|
|
1430
|
+
disk = readStored();
|
|
1431
|
+
refreshToken = getRefreshToken(disk);
|
|
1432
|
+
accessToken = getAccessToken(disk);
|
|
1433
|
+
accessTokenCached = Boolean(
|
|
1434
|
+
cachedToken && cachedToken.expiresAt > now() || accessToken && disk.tokenExpiresAt && disk.tokenExpiresAt > now()
|
|
1435
|
+
);
|
|
1436
|
+
authenticated = false;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
authenticated,
|
|
1441
|
+
accessTokenCached,
|
|
1442
|
+
accessTokenExpiresAt: cachedToken?.expiresAt ?? disk.tokenExpiresAt,
|
|
1443
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
1444
|
+
sessionId: disk.sessionId,
|
|
1445
|
+
accountId: disk.accountId,
|
|
1446
|
+
baseUrl: disk.baseUrl || baseUrl,
|
|
1447
|
+
storageBackend: detectSecretBackend(),
|
|
1448
|
+
validationAttempted: options.validate,
|
|
1449
|
+
authError
|
|
1450
|
+
};
|
|
1451
|
+
};
|
|
1452
|
+
var extractEmailFromToken = (token) => {
|
|
1453
|
+
try {
|
|
1454
|
+
const parts = token.split(".");
|
|
1455
|
+
if (parts.length !== 3) return null;
|
|
1456
|
+
const payload = JSON.parse(
|
|
1457
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
1458
|
+
);
|
|
1459
|
+
return payload.email || payload.upn || payload.preferred_username || null;
|
|
1460
|
+
} catch {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
var AUTH_LOOKUP_ERROR = "CloudEvalAuthLookupError";
|
|
1465
|
+
var authLookupError = async (response, context) => {
|
|
1466
|
+
let detail = "";
|
|
1467
|
+
try {
|
|
1468
|
+
const body = await response.text();
|
|
1469
|
+
if (body) {
|
|
1470
|
+
detail = ` - ${body.slice(0, 300)}`;
|
|
1471
|
+
}
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
const error = new Error(`${context} failed: ${response.status} ${response.statusText}${detail}`);
|
|
1475
|
+
error.name = AUTH_LOOKUP_ERROR;
|
|
1476
|
+
return error;
|
|
1477
|
+
};
|
|
1478
|
+
var isAuthLookupError = (error) => error instanceof Error && error.name === AUTH_LOOKUP_ERROR;
|
|
1479
|
+
var isAuthLookupFailure = (error) => isAuthLookupError(error);
|
|
1480
|
+
var clearStoredAuthIfTokenMatches = (token) => {
|
|
1481
|
+
const disk = readStored();
|
|
1482
|
+
const diskAccessToken = getAccessToken(disk);
|
|
1483
|
+
if (cachedToken?.token === token || diskAccessToken === token) {
|
|
1484
|
+
clearLocalAuth(disk);
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
return false;
|
|
1488
|
+
};
|
|
1489
|
+
var fetchCurrentUserFromServer = async (apiBase, token) => {
|
|
1490
|
+
try {
|
|
1491
|
+
const startedAt = Date.now();
|
|
1492
|
+
cliDebug("fetchCurrentUserFromServer request", {
|
|
1493
|
+
url: `${apiBase}/auth/me`
|
|
1494
|
+
});
|
|
1495
|
+
const response = await fetch(`${apiBase}/auth/me`, {
|
|
1496
|
+
method: "GET",
|
|
1497
|
+
headers: getCLIHeaders(token)
|
|
1498
|
+
});
|
|
1499
|
+
cliDebug("fetchCurrentUserFromServer response", {
|
|
1500
|
+
status: response.status,
|
|
1501
|
+
ok: response.ok,
|
|
1502
|
+
durationMs: Date.now() - startedAt
|
|
1503
|
+
});
|
|
1504
|
+
if (response.status === 401 || response.status === 403) {
|
|
1505
|
+
throw await authLookupError(response, "Current user lookup");
|
|
1506
|
+
}
|
|
1507
|
+
if (!response.ok) {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
const user = await response.json();
|
|
1511
|
+
if (!user?.id) {
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
cliDebug("fetchCurrentUserFromServer parsed", {
|
|
1515
|
+
userId: user.id,
|
|
1516
|
+
email: user.email,
|
|
1517
|
+
onboardingCompleted: !!user.preferences?.onboarding?.completedAt
|
|
1518
|
+
});
|
|
1519
|
+
return user;
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (isAuthLookupError(error)) {
|
|
1522
|
+
clearStoredAuthIfTokenMatches(token);
|
|
1523
|
+
throw error;
|
|
1524
|
+
}
|
|
1525
|
+
cliDebug("fetchCurrentUserFromServer failed", {
|
|
1526
|
+
message: errorMessage(error)
|
|
1527
|
+
});
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
var checkUserStatus = async (baseUrl, token) => {
|
|
1532
|
+
try {
|
|
1533
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
1534
|
+
cliDebug("checkUserStatus start", { apiBase });
|
|
1535
|
+
const currentUser = await fetchCurrentUserFromServer(apiBase, token);
|
|
1536
|
+
if (currentUser) {
|
|
1537
|
+
cliDebug("checkUserStatus resolved from /auth/me", {
|
|
1538
|
+
userId: currentUser.id,
|
|
1539
|
+
email: currentUser.email,
|
|
1540
|
+
onboardingCompleted: !!currentUser.preferences?.onboarding?.completedAt
|
|
1541
|
+
});
|
|
1542
|
+
return {
|
|
1543
|
+
exists: true,
|
|
1544
|
+
onboardingCompleted: !!currentUser.preferences?.onboarding?.completedAt,
|
|
1545
|
+
user: currentUser
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
const email = extractEmailFromToken(token);
|
|
1549
|
+
if (!email) {
|
|
1550
|
+
cliDebug("checkUserStatus no email claim; assuming existing completed user");
|
|
1551
|
+
return { exists: true, onboardingCompleted: true };
|
|
1552
|
+
}
|
|
1553
|
+
const startedAt = Date.now();
|
|
1554
|
+
cliDebug("checkUserStatus /user/email request", {
|
|
1555
|
+
url: `${apiBase}/user/email`,
|
|
1556
|
+
email
|
|
1557
|
+
});
|
|
1558
|
+
const response = await fetch(`${apiBase}/user/email`, {
|
|
1559
|
+
method: "POST",
|
|
1560
|
+
headers: getCLIHeaders(token),
|
|
1561
|
+
body: JSON.stringify({ email })
|
|
1562
|
+
});
|
|
1563
|
+
cliDebug("checkUserStatus /user/email response", {
|
|
1564
|
+
status: response.status,
|
|
1565
|
+
ok: response.ok,
|
|
1566
|
+
durationMs: Date.now() - startedAt
|
|
1567
|
+
});
|
|
1568
|
+
if (response.status === 401 || response.status === 403) {
|
|
1569
|
+
throw await authLookupError(response, "User email lookup");
|
|
1570
|
+
}
|
|
1571
|
+
if (response.ok) {
|
|
1572
|
+
const user = await response.json();
|
|
1573
|
+
cliDebug("checkUserStatus /user/email parsed", {
|
|
1574
|
+
userId: user?.id,
|
|
1575
|
+
email: user?.email,
|
|
1576
|
+
onboardingCompleted: !!user.preferences?.onboarding?.completedAt
|
|
1577
|
+
});
|
|
1578
|
+
return {
|
|
1579
|
+
exists: true,
|
|
1580
|
+
onboardingCompleted: !!user.preferences?.onboarding?.completedAt,
|
|
1581
|
+
user
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if (response.status === 404) {
|
|
1585
|
+
cliDebug("checkUserStatus user missing");
|
|
1586
|
+
return { exists: false, onboardingCompleted: false };
|
|
1587
|
+
}
|
|
1588
|
+
cliDebug("checkUserStatus non-404 fallback", {
|
|
1589
|
+
status: response.status
|
|
1590
|
+
});
|
|
1591
|
+
return { exists: true, onboardingCompleted: true };
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
if (isAuthLookupError(error)) {
|
|
1594
|
+
clearStoredAuthIfTokenMatches(token);
|
|
1595
|
+
throw error;
|
|
1596
|
+
}
|
|
1597
|
+
cliDebug("checkUserStatus failed; assuming completed for compatibility", {
|
|
1598
|
+
message: errorMessage(error)
|
|
1599
|
+
});
|
|
1600
|
+
return { exists: true, onboardingCompleted: true };
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
var completeOnboarding = async (baseUrl, token, data) => {
|
|
1604
|
+
try {
|
|
1605
|
+
const apiBase = normalizeApiBase(baseUrl);
|
|
1606
|
+
cliDebug("completeOnboarding start", {
|
|
1607
|
+
apiBase,
|
|
1608
|
+
name: data.name,
|
|
1609
|
+
role: data.role,
|
|
1610
|
+
teamSize: data.teamSize,
|
|
1611
|
+
goals: data.goals,
|
|
1612
|
+
cloudProvider: data.cloudProvider
|
|
1613
|
+
});
|
|
1614
|
+
const serverUser = await fetchCurrentUserFromServer(apiBase, token);
|
|
1615
|
+
const fallbackEmail = extractEmailFromToken(token);
|
|
1616
|
+
const email = serverUser?.email || fallbackEmail;
|
|
1617
|
+
if (!email) {
|
|
1618
|
+
throw new Error("Could not determine user email. Please login again.");
|
|
1619
|
+
}
|
|
1620
|
+
const userStatus = await checkUserStatus(apiBase, token);
|
|
1621
|
+
const knownUserId = userStatus.user?.id || serverUser?.id;
|
|
1622
|
+
cliDebug("completeOnboarding identity resolved", {
|
|
1623
|
+
email,
|
|
1624
|
+
serverUserId: serverUser?.id,
|
|
1625
|
+
knownUserId,
|
|
1626
|
+
statusExists: userStatus.exists,
|
|
1627
|
+
statusOnboardingCompleted: userStatus.onboardingCompleted
|
|
1628
|
+
});
|
|
1629
|
+
const onboarding = {
|
|
1630
|
+
role: data.role,
|
|
1631
|
+
teamSize: data.teamSize,
|
|
1632
|
+
primaryGoals: data.goals,
|
|
1633
|
+
cloudProvider: data.cloudProvider
|
|
1634
|
+
};
|
|
1635
|
+
const onboardData = await quickOnboardPlayground(
|
|
1636
|
+
apiBase,
|
|
1637
|
+
token,
|
|
1638
|
+
{
|
|
1639
|
+
id: knownUserId || "pending",
|
|
1640
|
+
email,
|
|
1641
|
+
fullName: data.name
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
onboarding
|
|
1645
|
+
}
|
|
1646
|
+
);
|
|
1647
|
+
const userId = onboardData.user?.id || knownUserId;
|
|
1648
|
+
if (!userId) {
|
|
1649
|
+
throw new Error("Onboarding completed but no user ID returned");
|
|
1650
|
+
}
|
|
1651
|
+
const persistedOnboarding = onboardData.user?.preferences?.onboarding;
|
|
1652
|
+
const hasPersistedCompletion = !!persistedOnboarding?.completedAt || !!onboardData.onboarding_completed_at;
|
|
1653
|
+
cliDebug("completeOnboarding quick result", {
|
|
1654
|
+
userId,
|
|
1655
|
+
hasPersistedCompletion,
|
|
1656
|
+
hasPlaygroundProject: !!onboardData.playground_project?.id,
|
|
1657
|
+
setupJobs: onboardData.setup_jobs?.map((job) => job.operation)
|
|
1658
|
+
});
|
|
1659
|
+
if (!hasPersistedCompletion) {
|
|
1660
|
+
const preferences = {
|
|
1661
|
+
onboarding: {
|
|
1662
|
+
...onboarding,
|
|
1663
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
cliDebug("completeOnboarding fallback PATCH request", {
|
|
1667
|
+
url: `${apiBase}/users/${userId}`
|
|
1668
|
+
});
|
|
1669
|
+
const response = await fetch(`${apiBase}/users/${userId}`, {
|
|
1670
|
+
method: "PATCH",
|
|
1671
|
+
headers: getCLIHeaders(token),
|
|
1672
|
+
body: JSON.stringify({ preferences })
|
|
1673
|
+
});
|
|
1674
|
+
cliDebug("completeOnboarding fallback PATCH response", {
|
|
1675
|
+
status: response.status,
|
|
1676
|
+
ok: response.ok
|
|
1677
|
+
});
|
|
1678
|
+
if (!response.ok) {
|
|
1679
|
+
const error = await response.json().catch(() => ({ message: "Failed to complete onboarding" }));
|
|
1680
|
+
throw new Error(error.message || "Failed to complete onboarding");
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (!onboardData.playground_project?.id) {
|
|
1684
|
+
cliDebug("completeOnboarding quick response missing Playground; repairing");
|
|
1685
|
+
await ensurePlaygroundProject(apiBase, token, {
|
|
1686
|
+
id: userId,
|
|
1687
|
+
email,
|
|
1688
|
+
fullName: data.name
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
cliDebug("completeOnboarding finished", {
|
|
1692
|
+
userId,
|
|
1693
|
+
email
|
|
1694
|
+
});
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
cliDebug("completeOnboarding failed", {
|
|
1697
|
+
message: error?.message
|
|
1698
|
+
});
|
|
1699
|
+
if (error?.message) {
|
|
1700
|
+
throw error;
|
|
1701
|
+
}
|
|
1702
|
+
throw new Error("Failed to complete onboarding");
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
var getAuthToken = async (options = {}) => {
|
|
1706
|
+
if (options.accessKey) {
|
|
1707
|
+
return options.accessKey;
|
|
1708
|
+
}
|
|
1709
|
+
const minValidUntil = now() + TOKEN_EXPIRY_SKEW_MS;
|
|
1710
|
+
if (cachedToken && cachedToken.expiresAt > minValidUntil) {
|
|
1711
|
+
return cachedToken.token;
|
|
1712
|
+
}
|
|
1713
|
+
const disk = readStored();
|
|
1714
|
+
const refreshToken = getRefreshToken(disk);
|
|
1715
|
+
const accessToken = getAccessToken(disk);
|
|
1716
|
+
let refreshError;
|
|
1717
|
+
if (accessToken && disk.tokenExpiresAt && disk.tokenExpiresAt > minValidUntil) {
|
|
1718
|
+
cachedToken = { token: accessToken, expiresAt: disk.tokenExpiresAt };
|
|
1719
|
+
return accessToken;
|
|
1720
|
+
}
|
|
1721
|
+
if (refreshToken) {
|
|
1722
|
+
try {
|
|
1723
|
+
return await refreshWithSingleFlight(options);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
refreshError = error;
|
|
1726
|
+
if (isRejectedRefreshTokenError(error)) {
|
|
1727
|
+
clearLocalAuth(disk);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const loginHint = "No authentication available. Run 'cloudeval login' to authenticate or use --access-key-stdin for automation.";
|
|
1732
|
+
if (refreshError) {
|
|
1733
|
+
throw new Error(`${loginHint} Stored refresh failed: ${errorMessage(refreshError)}`);
|
|
1734
|
+
}
|
|
1735
|
+
throw new Error(loginHint);
|
|
1736
|
+
};
|
|
1737
|
+
var getAuthHeader = async (options = {}) => {
|
|
1738
|
+
const token = await getAuthToken(options);
|
|
1739
|
+
return { Authorization: `Bearer ${token}` };
|
|
1740
|
+
};
|
|
1741
|
+
var createIdempotencyKey = () => randomUUID();
|
|
1742
|
+
var withIdempotencyHeader = (headers, idempotencyKey) => ({
|
|
1743
|
+
...headers,
|
|
1744
|
+
"Idempotency-Key": idempotencyKey ?? createIdempotencyKey()
|
|
1745
|
+
});
|
|
1746
|
+
var DEFAULT_PROJECT_TYPE = "sync";
|
|
1747
|
+
var RESPONSE_OUTPUT_NODES = /* @__PURE__ */ new Set([
|
|
1748
|
+
"generate_response",
|
|
1749
|
+
"handle_social_interaction",
|
|
1750
|
+
"response_compose"
|
|
1751
|
+
]);
|
|
1752
|
+
var isLocalHostname2 = (hostname) => {
|
|
1753
|
+
const lower = hostname.toLowerCase();
|
|
1754
|
+
return lower === "localhost" || lower === "127.0.0.1" || lower === "::1" || lower === "[::1]";
|
|
1755
|
+
};
|
|
1756
|
+
var assertSecureBaseUrl2 = (rawBaseUrl) => {
|
|
1757
|
+
const parsed = new URL(rawBaseUrl);
|
|
1758
|
+
if (parsed.protocol === "https:") {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (parsed.protocol === "http:" && isLocalHostname2(parsed.hostname)) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
throw new Error(
|
|
1765
|
+
`Refusing insecure base URL (${rawBaseUrl}). Use HTTPS for non-localhost endpoints.`
|
|
1766
|
+
);
|
|
1767
|
+
};
|
|
1768
|
+
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
1769
|
+
var isValidChunkStatus = (value) => {
|
|
1770
|
+
return typeof value === "string" && (value === "streaming" || value === "completed" || value === "aborted" || value === "error" || value === "pending");
|
|
1771
|
+
};
|
|
1772
|
+
var stringOrUndefined = (value) => typeof value === "string" ? value : void 0;
|
|
1773
|
+
var numberOrUndefined = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1774
|
+
var booleanOrUndefined = (value) => typeof value === "boolean" ? value : void 0;
|
|
1775
|
+
var isResponseOutputChunk = (chunk) => chunk.type === "responding" && (!chunk.node || RESPONSE_OUTPUT_NODES.has(chunk.node));
|
|
1776
|
+
var isResponseCompletionChunk = (chunk) => isResponseOutputChunk(chunk) && chunk.status === "completed";
|
|
1777
|
+
var isTerminalEndChunk = (chunk) => (chunk.type === "thinking" || chunk.type === "responding") && chunk.status === "completed" && chunk.node === "end";
|
|
1778
|
+
var normalizeHitlOption = (raw, index) => {
|
|
1779
|
+
if (!isObject2(raw)) {
|
|
1780
|
+
const label = String(raw ?? `Option ${index + 1}`);
|
|
1781
|
+
return { id: label, label };
|
|
1782
|
+
}
|
|
1783
|
+
const id = stringOrUndefined(raw.id) ?? stringOrUndefined(raw.value) ?? stringOrUndefined(raw.label) ?? `option_${index}`;
|
|
1784
|
+
return {
|
|
1785
|
+
id,
|
|
1786
|
+
label: stringOrUndefined(raw.label) ?? id,
|
|
1787
|
+
description: stringOrUndefined(raw.description),
|
|
1788
|
+
recommended: booleanOrUndefined(raw.recommended)
|
|
1789
|
+
};
|
|
1790
|
+
};
|
|
1791
|
+
var normalizeHitlQuestion = (raw, index) => {
|
|
1792
|
+
if (!isObject2(raw)) {
|
|
1793
|
+
return {
|
|
1794
|
+
id: `question_${index}`,
|
|
1795
|
+
text: String(raw ?? "Action required")
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
const id = stringOrUndefined(raw.id) ?? stringOrUndefined(raw.question_id) ?? `question_${index}`;
|
|
1799
|
+
const text = stringOrUndefined(raw.text) ?? stringOrUndefined(raw.label) ?? stringOrUndefined(raw.message) ?? "Action required";
|
|
1800
|
+
const options = Array.isArray(raw.options) ? raw.options.map(
|
|
1801
|
+
(option, optionIndex) => normalizeHitlOption(option, optionIndex)
|
|
1802
|
+
) : void 0;
|
|
1803
|
+
return {
|
|
1804
|
+
id,
|
|
1805
|
+
text,
|
|
1806
|
+
label: stringOrUndefined(raw.label),
|
|
1807
|
+
kind: stringOrUndefined(raw.kind),
|
|
1808
|
+
intent: stringOrUndefined(raw.intent),
|
|
1809
|
+
tool_label: stringOrUndefined(raw.tool_label),
|
|
1810
|
+
action: stringOrUndefined(raw.action),
|
|
1811
|
+
options,
|
|
1812
|
+
recommended_option_id: stringOrUndefined(raw.recommended_option_id),
|
|
1813
|
+
mode_switch_source: stringOrUndefined(raw.mode_switch_source),
|
|
1814
|
+
mode_switch_target: stringOrUndefined(raw.mode_switch_target),
|
|
1815
|
+
resume_behavior: stringOrUndefined(raw.resume_behavior),
|
|
1816
|
+
selectionMode: stringOrUndefined(raw.selectionMode),
|
|
1817
|
+
minSelections: numberOrUndefined(raw.minSelections),
|
|
1818
|
+
maxSelections: numberOrUndefined(raw.maxSelections)
|
|
1819
|
+
};
|
|
1820
|
+
};
|
|
1821
|
+
var normalizeChunk = (raw, receivedAt) => {
|
|
1822
|
+
if (!isObject2(raw) || typeof raw.type !== "string") {
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
const base = { receivedAt };
|
|
1826
|
+
const data = isObject2(raw.data) ? raw.data : void 0;
|
|
1827
|
+
switch (raw.type) {
|
|
1828
|
+
case "metadata": {
|
|
1829
|
+
const chunk = {
|
|
1830
|
+
type: "metadata",
|
|
1831
|
+
trace_id: typeof raw.trace_id === "string" ? raw.trace_id : void 0,
|
|
1832
|
+
thread_id: typeof raw.thread_id === "string" ? raw.thread_id : void 0,
|
|
1833
|
+
...base
|
|
1834
|
+
};
|
|
1835
|
+
return chunk;
|
|
1836
|
+
}
|
|
1837
|
+
case "thinking": {
|
|
1838
|
+
const chunk = {
|
|
1839
|
+
type: "thinking",
|
|
1840
|
+
node: typeof raw.node === "string" ? raw.node : void 0,
|
|
1841
|
+
status: isValidChunkStatus(raw.status) ? raw.status : void 0,
|
|
1842
|
+
description: typeof raw.description === "string" ? raw.description : void 0,
|
|
1843
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
1844
|
+
content: typeof raw.content === "string" ? raw.content : void 0,
|
|
1845
|
+
...base
|
|
1846
|
+
};
|
|
1847
|
+
return chunk;
|
|
1848
|
+
}
|
|
1849
|
+
case "responding": {
|
|
1850
|
+
const chunk = {
|
|
1851
|
+
type: "responding",
|
|
1852
|
+
node: typeof raw.node === "string" ? raw.node : void 0,
|
|
1853
|
+
status: isValidChunkStatus(raw.status) ? raw.status : void 0,
|
|
1854
|
+
description: typeof raw.description === "string" ? raw.description : void 0,
|
|
1855
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
1856
|
+
content: typeof raw.content === "string" ? raw.content : void 0,
|
|
1857
|
+
source: stringOrUndefined(raw.source),
|
|
1858
|
+
...base
|
|
1859
|
+
};
|
|
1860
|
+
return chunk;
|
|
1861
|
+
}
|
|
1862
|
+
case "error": {
|
|
1863
|
+
const chunk = {
|
|
1864
|
+
type: "error",
|
|
1865
|
+
node: typeof raw.node === "string" ? raw.node : void 0,
|
|
1866
|
+
status: isValidChunkStatus(raw.status) ? raw.status : void 0,
|
|
1867
|
+
description: typeof raw.description === "string" ? raw.description : void 0,
|
|
1868
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
1869
|
+
content: typeof raw.content === "string" ? raw.content : void 0,
|
|
1870
|
+
stacktrace: typeof raw.stacktrace === "string" ? raw.stacktrace : void 0,
|
|
1871
|
+
...base
|
|
1872
|
+
};
|
|
1873
|
+
return chunk;
|
|
1874
|
+
}
|
|
1875
|
+
case "hitl":
|
|
1876
|
+
case "hitl_request": {
|
|
1877
|
+
const rawQuestions = Array.isArray(raw.questions) ? raw.questions : Array.isArray(data?.questions) ? data.questions : [];
|
|
1878
|
+
const chunk = {
|
|
1879
|
+
type: "hitl_request",
|
|
1880
|
+
questions: rawQuestions.map(
|
|
1881
|
+
(question, index) => normalizeHitlQuestion(question, index)
|
|
1882
|
+
),
|
|
1883
|
+
checkpoint_id: stringOrUndefined(raw.checkpoint_id) ?? stringOrUndefined(data?.checkpoint_id),
|
|
1884
|
+
pending_intent_id: stringOrUndefined(raw.pending_intent_id) ?? stringOrUndefined(data?.pending_intent_id),
|
|
1885
|
+
run_id: stringOrUndefined(raw.run_id) ?? stringOrUndefined(data?.run_id),
|
|
1886
|
+
langsmith_trace_id: stringOrUndefined(raw.langsmith_trace_id) ?? stringOrUndefined(data?.langsmith_trace_id),
|
|
1887
|
+
...base
|
|
1888
|
+
};
|
|
1889
|
+
return chunk;
|
|
1890
|
+
}
|
|
1891
|
+
case "hitl_resume": {
|
|
1892
|
+
const chunk = {
|
|
1893
|
+
type: "hitl_resume",
|
|
1894
|
+
status: isValidChunkStatus(raw.status) ? raw.status : void 0,
|
|
1895
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
1896
|
+
pending_intent_id: stringOrUndefined(raw.pending_intent_id) ?? stringOrUndefined(data?.pending_intent_id),
|
|
1897
|
+
...base
|
|
1898
|
+
};
|
|
1899
|
+
return chunk;
|
|
1900
|
+
}
|
|
1901
|
+
default:
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
var buildPayload = (options) => {
|
|
1906
|
+
const user = options.project?.user_id && (!options.user.id || options.user.id === "cli-user") ? { ...options.user, id: options.project.user_id } : options.user;
|
|
1907
|
+
const project = options.project ?? {
|
|
1908
|
+
id: "cli-project",
|
|
1909
|
+
name: "CLI Session",
|
|
1910
|
+
user_id: user.id,
|
|
1911
|
+
cloud_provider: "azure",
|
|
1912
|
+
type: DEFAULT_PROJECT_TYPE
|
|
1913
|
+
};
|
|
1914
|
+
const context = options.context ?? [];
|
|
1915
|
+
const settings = options.settings;
|
|
1916
|
+
const message = options.message;
|
|
1917
|
+
const agentProfileId = options.agentProfileId?.trim();
|
|
1918
|
+
const payload = {
|
|
1919
|
+
thread_id: options.threadId,
|
|
1920
|
+
input: {
|
|
1921
|
+
messages: [{ role: "user", content: message }],
|
|
1922
|
+
user,
|
|
1923
|
+
project,
|
|
1924
|
+
settings,
|
|
1925
|
+
...agentProfileId ? { agent_profile_id: agentProfileId } : {},
|
|
1926
|
+
context
|
|
1927
|
+
},
|
|
1928
|
+
user,
|
|
1929
|
+
message,
|
|
1930
|
+
project,
|
|
1931
|
+
settings,
|
|
1932
|
+
...agentProfileId ? { agent_profile_id: agentProfileId } : {},
|
|
1933
|
+
context,
|
|
1934
|
+
group_size: 1,
|
|
1935
|
+
streaming_mode: options.streamingMode ?? "USER"
|
|
1936
|
+
};
|
|
1937
|
+
if (options.hitlResume?.checkpointId && options.hitlResume.responses.length > 0) {
|
|
1938
|
+
payload.hitl_resume = true;
|
|
1939
|
+
payload.hitl_checkpoint_id = options.hitlResume.checkpointId;
|
|
1940
|
+
payload.hitl_responses = options.hitlResume.responses;
|
|
1941
|
+
if (options.hitlResume.runId) {
|
|
1942
|
+
payload.run_id = options.hitlResume.runId;
|
|
1943
|
+
}
|
|
1944
|
+
if (options.hitlResume.langsmithTraceId) {
|
|
1945
|
+
payload.langsmith_trace_id = options.hitlResume.langsmithTraceId;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return payload;
|
|
1949
|
+
};
|
|
1950
|
+
var compactErrorBody = (body) => {
|
|
1951
|
+
const trimmed = body.trim();
|
|
1952
|
+
if (!trimmed) {
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
const parsed = JSON.parse(trimmed);
|
|
1957
|
+
return redactSensitiveText(JSON.stringify(parsed));
|
|
1958
|
+
} catch {
|
|
1959
|
+
const redacted = redactSensitiveText(trimmed);
|
|
1960
|
+
return redacted.length > 1e3 ? `${redacted.slice(0, 1e3)}...` : redacted;
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
async function* streamChat(options) {
|
|
1964
|
+
assertSecureBaseUrl2(options.baseUrl);
|
|
1965
|
+
const payload = buildPayload(options);
|
|
1966
|
+
const apiBase = normalizeApiBase(options.baseUrl);
|
|
1967
|
+
const url = `${apiBase}/chat/stream`;
|
|
1968
|
+
const streamIdleTimeoutMs = typeof options.streamIdleTimeoutMs === "number" && Number.isFinite(options.streamIdleTimeoutMs) && options.streamIdleTimeoutMs > 0 ? options.streamIdleTimeoutMs : void 0;
|
|
1969
|
+
const headers = withIdempotencyHeader({
|
|
1970
|
+
"Content-Type": "application/json",
|
|
1971
|
+
Accept: "text/event-stream",
|
|
1972
|
+
"X-Client-Type": "cloudeval-cli",
|
|
1973
|
+
"X-Client-Version": "0.1.0"
|
|
1974
|
+
});
|
|
1975
|
+
if (options.authToken) {
|
|
1976
|
+
headers.Authorization = `Bearer ${options.authToken}`;
|
|
1977
|
+
}
|
|
1978
|
+
let connectTimedOut = false;
|
|
1979
|
+
let connectTimeout;
|
|
1980
|
+
let removeExternalAbort;
|
|
1981
|
+
let requestSignal = options.signal;
|
|
1982
|
+
const connectController = streamIdleTimeoutMs ? new AbortController() : void 0;
|
|
1983
|
+
if (connectController) {
|
|
1984
|
+
requestSignal = connectController.signal;
|
|
1985
|
+
if (options.signal) {
|
|
1986
|
+
const abortFromExternal = () => connectController.abort(options.signal?.reason);
|
|
1987
|
+
if (options.signal.aborted) {
|
|
1988
|
+
abortFromExternal();
|
|
1989
|
+
} else {
|
|
1990
|
+
options.signal.addEventListener("abort", abortFromExternal, { once: true });
|
|
1991
|
+
removeExternalAbort = () => options.signal?.removeEventListener("abort", abortFromExternal);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
connectTimeout = setTimeout(() => {
|
|
1995
|
+
connectTimedOut = true;
|
|
1996
|
+
connectController.abort();
|
|
1997
|
+
}, streamIdleTimeoutMs);
|
|
1998
|
+
}
|
|
1999
|
+
let response;
|
|
2000
|
+
try {
|
|
2001
|
+
response = await fetch(url, {
|
|
2002
|
+
method: "POST",
|
|
2003
|
+
headers,
|
|
2004
|
+
body: JSON.stringify(payload),
|
|
2005
|
+
signal: requestSignal
|
|
2006
|
+
});
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
if (connectTimedOut && streamIdleTimeoutMs) {
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
`No chat stream response received within ${streamIdleTimeoutMs}ms. Please retry or check backend availability.`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
throw error;
|
|
2014
|
+
} finally {
|
|
2015
|
+
if (connectTimeout) {
|
|
2016
|
+
clearTimeout(connectTimeout);
|
|
2017
|
+
}
|
|
2018
|
+
removeExternalAbort?.();
|
|
2019
|
+
}
|
|
2020
|
+
if (!response.ok) {
|
|
2021
|
+
const body = compactErrorBody(await response.text().catch(() => ""));
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
`Stream request failed with status ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
if (!response.body) {
|
|
2027
|
+
throw new Error("Streaming response body missing");
|
|
2028
|
+
}
|
|
2029
|
+
const reader = response.body.getReader();
|
|
2030
|
+
const decoder = new TextDecoder("utf-8");
|
|
2031
|
+
let buffer = "";
|
|
2032
|
+
let sseDataLines = [];
|
|
2033
|
+
let doneSeen = false;
|
|
2034
|
+
let responseCompleteDeadline;
|
|
2035
|
+
let streamActivityAt = Date.now();
|
|
2036
|
+
const responseCompletionGraceMs = options.responseCompletionGraceMs ?? 5e3;
|
|
2037
|
+
const parsePayload = (rawPayload) => {
|
|
2038
|
+
const trimmed = rawPayload.trim();
|
|
2039
|
+
if (!trimmed) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (trimmed === "[DONE]") {
|
|
2043
|
+
doneSeen = true;
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
const parsed = JSON.parse(trimmed);
|
|
2048
|
+
const chunk = normalizeChunk(parsed, Date.now());
|
|
2049
|
+
if (chunk && options.debug) {
|
|
2050
|
+
console.error("[stream][chunk]", chunk);
|
|
2051
|
+
}
|
|
2052
|
+
if (chunk) {
|
|
2053
|
+
return chunk;
|
|
2054
|
+
}
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
if (options.debug) {
|
|
2057
|
+
console.error("[stream][parse-error]", error, rawPayload);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
const flushSseEvent = () => {
|
|
2062
|
+
if (sseDataLines.length === 0) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
const payload2 = sseDataLines.join("\n");
|
|
2066
|
+
sseDataLines = [];
|
|
2067
|
+
return parsePayload(payload2);
|
|
2068
|
+
};
|
|
2069
|
+
const parseLine = (line) => {
|
|
2070
|
+
const normalizedLine = line.replace(/\r$/, "");
|
|
2071
|
+
if (!normalizedLine) {
|
|
2072
|
+
return flushSseEvent();
|
|
2073
|
+
}
|
|
2074
|
+
if (normalizedLine.startsWith(":")) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if (normalizedLine.startsWith("data:")) {
|
|
2078
|
+
sseDataLines.push(normalizedLine.slice(5).trimStart());
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (normalizedLine.startsWith("event:") || normalizedLine.startsWith("id:") || normalizedLine.startsWith("retry:")) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (sseDataLines.length > 0) {
|
|
2085
|
+
sseDataLines.push(normalizedLine);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
return parsePayload(normalizedLine);
|
|
2089
|
+
};
|
|
2090
|
+
const readWithOptionalDeadline = async () => {
|
|
2091
|
+
if (!responseCompleteDeadline && !streamIdleTimeoutMs) {
|
|
2092
|
+
return { type: "read", result: await reader.read() };
|
|
2093
|
+
}
|
|
2094
|
+
const deadline = responseCompleteDeadline ?? streamActivityAt + streamIdleTimeoutMs;
|
|
2095
|
+
const remainingMs = deadline - Date.now();
|
|
2096
|
+
if (remainingMs <= 0) {
|
|
2097
|
+
return responseCompleteDeadline ? { type: "deadline" } : { type: "idle-timeout" };
|
|
2098
|
+
}
|
|
2099
|
+
return Promise.race([
|
|
2100
|
+
reader.read().then((result) => ({ type: "read", result })),
|
|
2101
|
+
new Promise(
|
|
2102
|
+
(resolve) => setTimeout(
|
|
2103
|
+
() => resolve(
|
|
2104
|
+
responseCompleteDeadline ? { type: "deadline" } : { type: "idle-timeout" }
|
|
2105
|
+
),
|
|
2106
|
+
remainingMs
|
|
2107
|
+
)
|
|
2108
|
+
)
|
|
2109
|
+
]);
|
|
2110
|
+
};
|
|
2111
|
+
const markResponseComplete = (chunk) => {
|
|
2112
|
+
if (!options.completeAfterResponse) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (isTerminalEndChunk(chunk)) {
|
|
2116
|
+
responseCompleteDeadline ??= Date.now() + responseCompletionGraceMs;
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (!isResponseOutputChunk(chunk)) {
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
if (isResponseCompletionChunk(chunk)) {
|
|
2123
|
+
responseCompleteDeadline ??= Date.now() + responseCompletionGraceMs;
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (chunk.content) {
|
|
2127
|
+
responseCompleteDeadline = Date.now() + responseCompletionGraceMs;
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
try {
|
|
2131
|
+
while (true) {
|
|
2132
|
+
const read = await readWithOptionalDeadline();
|
|
2133
|
+
if (read.type === "deadline") {
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (read.type === "idle-timeout") {
|
|
2137
|
+
throw new Error(
|
|
2138
|
+
`No chat stream data received within ${streamIdleTimeoutMs}ms. Please retry or check backend availability.`
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
const { value, done } = read.result;
|
|
2142
|
+
if (done) break;
|
|
2143
|
+
if (value?.length) {
|
|
2144
|
+
streamActivityAt = Date.now();
|
|
2145
|
+
}
|
|
2146
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2147
|
+
const lines = buffer.split("\n");
|
|
2148
|
+
buffer = lines.pop() ?? "";
|
|
2149
|
+
for (const line of lines) {
|
|
2150
|
+
const chunk = parseLine(line);
|
|
2151
|
+
if (chunk) {
|
|
2152
|
+
yield chunk;
|
|
2153
|
+
markResponseComplete(chunk);
|
|
2154
|
+
}
|
|
2155
|
+
if (doneSeen) {
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (buffer.trim()) {
|
|
2161
|
+
const chunk = parseLine(buffer);
|
|
2162
|
+
if (chunk) {
|
|
2163
|
+
yield chunk;
|
|
2164
|
+
markResponseComplete(chunk);
|
|
2165
|
+
}
|
|
2166
|
+
if (doneSeen) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
const finalChunk = flushSseEvent();
|
|
2171
|
+
if (finalChunk) {
|
|
2172
|
+
yield finalChunk;
|
|
2173
|
+
markResponseComplete(finalChunk);
|
|
2174
|
+
}
|
|
2175
|
+
} finally {
|
|
2176
|
+
await reader.cancel().catch(() => void 0);
|
|
2177
|
+
try {
|
|
2178
|
+
reader.releaseLock();
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
var STREAMING_NODES = /* @__PURE__ */ new Set([
|
|
2184
|
+
"generate_response",
|
|
2185
|
+
"handle_social_interaction",
|
|
2186
|
+
"response_compose"
|
|
2187
|
+
]);
|
|
2188
|
+
var FOLLOW_UP_NODE = "generate_follow_up";
|
|
2189
|
+
var ERROR_FALLBACK_SOURCE_PREFIX = "error_fallback:";
|
|
2190
|
+
var TERMINAL_STEP_STATUSES = /* @__PURE__ */ new Set([
|
|
2191
|
+
"completed",
|
|
2192
|
+
"error",
|
|
2193
|
+
"aborted",
|
|
2194
|
+
"cancelled"
|
|
2195
|
+
]);
|
|
2196
|
+
var cloneMessage = (message) => ({
|
|
2197
|
+
...message,
|
|
2198
|
+
thinkingSteps: message.thinkingSteps ? [...message.thinkingSteps.map((s) => ({ ...s }))] : [],
|
|
2199
|
+
followUpQuestions: message.followUpQuestions ? [...message.followUpQuestions] : void 0
|
|
2200
|
+
});
|
|
2201
|
+
var finalizeOpenSteps = (steps, status, timestamp) => {
|
|
2202
|
+
if (!steps) {
|
|
2203
|
+
return steps;
|
|
2204
|
+
}
|
|
2205
|
+
return steps.map((step) => {
|
|
2206
|
+
if (TERMINAL_STEP_STATUSES.has(step.status ?? "pending")) {
|
|
2207
|
+
return step;
|
|
2208
|
+
}
|
|
2209
|
+
const startedAt = step.startedAt ?? step.timestamp;
|
|
2210
|
+
return {
|
|
2211
|
+
...step,
|
|
2212
|
+
status,
|
|
2213
|
+
timestamp,
|
|
2214
|
+
updatedAt: timestamp,
|
|
2215
|
+
completedAt: timestamp,
|
|
2216
|
+
durationMs: Math.max(0, timestamp - startedAt)
|
|
2217
|
+
};
|
|
2218
|
+
});
|
|
2219
|
+
};
|
|
2220
|
+
var ensureAssistantMessage = (state, timestamp) => {
|
|
2221
|
+
const existing = state.activeMessageId && state.messages.find(
|
|
2222
|
+
(m) => m.id === state.activeMessageId && m.role === "assistant"
|
|
2223
|
+
) || state.messages.find((m) => m.pending && m.role === "assistant");
|
|
2224
|
+
if (existing) {
|
|
2225
|
+
return [{ ...state, activeMessageId: existing.id }, cloneMessage(existing)];
|
|
2226
|
+
}
|
|
2227
|
+
const closedMessages = state.messages.map(
|
|
2228
|
+
(m) => m.role === "assistant" && m.pending ? { ...m, pending: false, updatedAt: timestamp } : m
|
|
2229
|
+
);
|
|
2230
|
+
const message = {
|
|
2231
|
+
id: randomUUID2(),
|
|
2232
|
+
role: "assistant",
|
|
2233
|
+
content: "",
|
|
2234
|
+
pending: true,
|
|
2235
|
+
thinkingSteps: [],
|
|
2236
|
+
createdAt: timestamp,
|
|
2237
|
+
updatedAt: timestamp
|
|
2238
|
+
};
|
|
2239
|
+
return [
|
|
2240
|
+
{
|
|
2241
|
+
...state,
|
|
2242
|
+
activeMessageId: message.id,
|
|
2243
|
+
messages: [...closedMessages, message]
|
|
2244
|
+
},
|
|
2245
|
+
message
|
|
2246
|
+
];
|
|
2247
|
+
};
|
|
2248
|
+
var mergeThinkingStep = (steps, chunk) => {
|
|
2249
|
+
const node = chunk.node ?? "unknown";
|
|
2250
|
+
const mergedSteps = steps ? [...steps] : [];
|
|
2251
|
+
const chunkStatus = chunk.status ?? "streaming";
|
|
2252
|
+
const existingIdx = [...mergedSteps].reverse().findIndex(
|
|
2253
|
+
(s) => s.node === node && !TERMINAL_STEP_STATUSES.has(s.status ?? "pending")
|
|
2254
|
+
);
|
|
2255
|
+
const existingStepIdx = existingIdx >= 0 ? mergedSteps.length - 1 - existingIdx : -1;
|
|
2256
|
+
if (existingStepIdx >= 0) {
|
|
2257
|
+
const existing = mergedSteps[existingStepIdx];
|
|
2258
|
+
const startedAt = existing.startedAt ?? existing.timestamp ?? chunk.receivedAt;
|
|
2259
|
+
const isTerminal = TERMINAL_STEP_STATUSES.has(chunkStatus);
|
|
2260
|
+
mergedSteps[existingStepIdx] = {
|
|
2261
|
+
...existing,
|
|
2262
|
+
content: (existing.content ?? "") + (chunk.content ?? ""),
|
|
2263
|
+
description: chunk.description ?? existing.description,
|
|
2264
|
+
message: chunk.message ?? existing.message,
|
|
2265
|
+
status: chunkStatus,
|
|
2266
|
+
timestamp: chunk.receivedAt,
|
|
2267
|
+
startedAt,
|
|
2268
|
+
updatedAt: chunk.receivedAt,
|
|
2269
|
+
completedAt: isTerminal ? chunk.receivedAt : existing.completedAt,
|
|
2270
|
+
durationMs: Math.max(0, chunk.receivedAt - startedAt)
|
|
2271
|
+
};
|
|
2272
|
+
} else {
|
|
2273
|
+
const isTerminal = TERMINAL_STEP_STATUSES.has(chunkStatus);
|
|
2274
|
+
mergedSteps.push({
|
|
2275
|
+
node,
|
|
2276
|
+
type: chunk.type === "responding" ? "responding" : "thinking",
|
|
2277
|
+
content: chunk.content,
|
|
2278
|
+
description: chunk.description,
|
|
2279
|
+
message: chunk.message,
|
|
2280
|
+
status: chunkStatus,
|
|
2281
|
+
timestamp: chunk.receivedAt,
|
|
2282
|
+
startedAt: chunk.receivedAt,
|
|
2283
|
+
updatedAt: chunk.receivedAt,
|
|
2284
|
+
completedAt: isTerminal ? chunk.receivedAt : void 0,
|
|
2285
|
+
durationMs: 0
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
return mergedSteps;
|
|
2289
|
+
};
|
|
2290
|
+
var mergeHitlStep = (steps, chunk) => {
|
|
2291
|
+
const node = "hitl_middleware";
|
|
2292
|
+
const existingIdx = steps?.findIndex((s) => s.node === node) ?? -1;
|
|
2293
|
+
const mergedSteps = steps ? [...steps] : [];
|
|
2294
|
+
const firstQuestion = chunk.questions[0];
|
|
2295
|
+
const step = {
|
|
2296
|
+
node,
|
|
2297
|
+
type: "hitl",
|
|
2298
|
+
description: "Waiting for your input",
|
|
2299
|
+
message: firstQuestion?.text,
|
|
2300
|
+
content: firstQuestion?.text,
|
|
2301
|
+
status: "pending",
|
|
2302
|
+
timestamp: chunk.receivedAt,
|
|
2303
|
+
startedAt: chunk.receivedAt,
|
|
2304
|
+
updatedAt: chunk.receivedAt,
|
|
2305
|
+
durationMs: 0,
|
|
2306
|
+
hitlQuestions: chunk.questions
|
|
2307
|
+
};
|
|
2308
|
+
if (existingIdx >= 0) {
|
|
2309
|
+
mergedSteps[existingIdx] = {
|
|
2310
|
+
...mergedSteps[existingIdx],
|
|
2311
|
+
...step
|
|
2312
|
+
};
|
|
2313
|
+
} else {
|
|
2314
|
+
mergedSteps.push(step);
|
|
2315
|
+
}
|
|
2316
|
+
return mergedSteps;
|
|
2317
|
+
};
|
|
2318
|
+
var parseFollowUps = (scratch) => (scratch ?? "").split(";").map((q) => q.trim()).filter(Boolean);
|
|
2319
|
+
var isErrorFallbackRespondingChunk = (chunk) => typeof chunk.source === "string" && chunk.source.startsWith(ERROR_FALLBACK_SOURCE_PREFIX);
|
|
2320
|
+
var getFallbackErrorMessage = (chunk) => chunk.content || chunk.description || chunk.message || "Unknown error";
|
|
2321
|
+
var initialChatState = {
|
|
2322
|
+
status: "idle",
|
|
2323
|
+
messages: []
|
|
2324
|
+
};
|
|
2325
|
+
var completeActiveAssistantMessage = (state, timestamp = Date.now()) => {
|
|
2326
|
+
const activeMessage = state.activeMessageId && state.messages.find(
|
|
2327
|
+
(message) => message.id === state.activeMessageId && message.role === "assistant"
|
|
2328
|
+
) || [...state.messages].reverse().find(
|
|
2329
|
+
(message) => message.role === "assistant" && message.pending
|
|
2330
|
+
) || [...state.messages].reverse().find(
|
|
2331
|
+
(message) => message.role === "assistant"
|
|
2332
|
+
);
|
|
2333
|
+
if (!activeMessage) {
|
|
2334
|
+
return {
|
|
2335
|
+
...state,
|
|
2336
|
+
status: state.status === "error" || state.status === "hitl_waiting" ? state.status : "complete"
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
...state,
|
|
2341
|
+
status: state.status === "error" || state.status === "hitl_waiting" ? state.status : "complete",
|
|
2342
|
+
messages: state.messages.map(
|
|
2343
|
+
(message) => message.id === activeMessage.id ? {
|
|
2344
|
+
...message,
|
|
2345
|
+
pending: false,
|
|
2346
|
+
updatedAt: timestamp,
|
|
2347
|
+
thinkingSteps: finalizeOpenSteps(
|
|
2348
|
+
message.thinkingSteps,
|
|
2349
|
+
"completed",
|
|
2350
|
+
timestamp
|
|
2351
|
+
)
|
|
2352
|
+
} : message
|
|
2353
|
+
)
|
|
2354
|
+
};
|
|
2355
|
+
};
|
|
2356
|
+
var reduceChunk = (state, chunk) => {
|
|
2357
|
+
const next = {
|
|
2358
|
+
...state,
|
|
2359
|
+
lastChunk: chunk
|
|
2360
|
+
};
|
|
2361
|
+
if (chunk.type === "metadata") {
|
|
2362
|
+
return {
|
|
2363
|
+
...next,
|
|
2364
|
+
threadId: chunk.thread_id ?? next.threadId,
|
|
2365
|
+
traceId: chunk.trace_id ?? next.traceId
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
if (chunk.type === "hitl_request") {
|
|
2369
|
+
const [stateWithMessage, streamingMessage] = ensureAssistantMessage(
|
|
2370
|
+
next,
|
|
2371
|
+
chunk.receivedAt
|
|
2372
|
+
);
|
|
2373
|
+
const updatedMessage = cloneMessage(streamingMessage);
|
|
2374
|
+
updatedMessage.pending = true;
|
|
2375
|
+
updatedMessage.thinkingSteps = mergeHitlStep(
|
|
2376
|
+
updatedMessage.thinkingSteps,
|
|
2377
|
+
chunk
|
|
2378
|
+
);
|
|
2379
|
+
updatedMessage.updatedAt = chunk.receivedAt;
|
|
2380
|
+
const messages = stateWithMessage.messages.map(
|
|
2381
|
+
(m) => m.id === updatedMessage.id ? updatedMessage : m
|
|
2382
|
+
);
|
|
2383
|
+
return {
|
|
2384
|
+
...stateWithMessage,
|
|
2385
|
+
status: "hitl_waiting",
|
|
2386
|
+
messages,
|
|
2387
|
+
activeMessageId: updatedMessage.id,
|
|
2388
|
+
followUpScratch: void 0,
|
|
2389
|
+
hitl: {
|
|
2390
|
+
waiting: true,
|
|
2391
|
+
questions: chunk.questions,
|
|
2392
|
+
checkpointId: chunk.checkpoint_id,
|
|
2393
|
+
pendingIntentId: chunk.pending_intent_id,
|
|
2394
|
+
runId: chunk.run_id,
|
|
2395
|
+
langsmithTraceId: chunk.langsmith_trace_id,
|
|
2396
|
+
messageId: updatedMessage.id
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
if (chunk.type === "hitl_resume") {
|
|
2401
|
+
return {
|
|
2402
|
+
...next,
|
|
2403
|
+
status: "thinking",
|
|
2404
|
+
hitl: void 0,
|
|
2405
|
+
error: void 0
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
if (chunk.type === "error") {
|
|
2409
|
+
const status = chunk.status === "aborted" ? "canceled" : "error";
|
|
2410
|
+
const error = chunk.description || chunk.content || chunk.message || "Unknown error";
|
|
2411
|
+
const messages = state.messages.map(
|
|
2412
|
+
(m) => m.id === state.activeMessageId ? {
|
|
2413
|
+
...m,
|
|
2414
|
+
pending: false,
|
|
2415
|
+
error,
|
|
2416
|
+
updatedAt: chunk.receivedAt,
|
|
2417
|
+
thinkingSteps: finalizeOpenSteps(
|
|
2418
|
+
m.thinkingSteps,
|
|
2419
|
+
status === "canceled" ? "aborted" : "error",
|
|
2420
|
+
chunk.receivedAt
|
|
2421
|
+
)
|
|
2422
|
+
} : m
|
|
2423
|
+
);
|
|
2424
|
+
return {
|
|
2425
|
+
...next,
|
|
2426
|
+
status,
|
|
2427
|
+
error,
|
|
2428
|
+
messages,
|
|
2429
|
+
hitl: void 0
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
if (chunk.type === "thinking" || chunk.type === "responding") {
|
|
2433
|
+
const [stateWithMessage, streamingMessage] = ensureAssistantMessage(
|
|
2434
|
+
next,
|
|
2435
|
+
chunk.receivedAt
|
|
2436
|
+
);
|
|
2437
|
+
const updatedMessage = cloneMessage(streamingMessage);
|
|
2438
|
+
updatedMessage.thinkingSteps = mergeThinkingStep(
|
|
2439
|
+
updatedMessage.thinkingSteps,
|
|
2440
|
+
chunk
|
|
2441
|
+
);
|
|
2442
|
+
updatedMessage.updatedAt = chunk.receivedAt;
|
|
2443
|
+
if (chunk.type === "responding" && isErrorFallbackRespondingChunk(chunk)) {
|
|
2444
|
+
const error = getFallbackErrorMessage(chunk);
|
|
2445
|
+
updatedMessage.content = `${updatedMessage.content ?? ""}${chunk.content ?? ""}`;
|
|
2446
|
+
updatedMessage.pending = false;
|
|
2447
|
+
updatedMessage.updatedAt = chunk.receivedAt;
|
|
2448
|
+
updatedMessage.thinkingSteps = finalizeOpenSteps(
|
|
2449
|
+
updatedMessage.thinkingSteps,
|
|
2450
|
+
"error",
|
|
2451
|
+
chunk.receivedAt
|
|
2452
|
+
);
|
|
2453
|
+
const messages2 = stateWithMessage.messages.map(
|
|
2454
|
+
(m) => m.id === updatedMessage.id ? updatedMessage : m
|
|
2455
|
+
);
|
|
2456
|
+
return {
|
|
2457
|
+
...stateWithMessage,
|
|
2458
|
+
status: "error",
|
|
2459
|
+
error,
|
|
2460
|
+
followUpScratch: void 0,
|
|
2461
|
+
messages: messages2,
|
|
2462
|
+
activeMessageId: updatedMessage.id,
|
|
2463
|
+
hitl: void 0
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
let followUpScratch = stateWithMessage.followUpScratch;
|
|
2467
|
+
let status = stateWithMessage.status;
|
|
2468
|
+
const keepHitlWaiting = Boolean(stateWithMessage.hitl?.waiting);
|
|
2469
|
+
if (chunk.type === "responding" && STREAMING_NODES.has(chunk.node ?? "")) {
|
|
2470
|
+
updatedMessage.content = `${updatedMessage.content ?? ""}${chunk.content ?? ""}`;
|
|
2471
|
+
updatedMessage.pending = chunk.status !== "completed";
|
|
2472
|
+
if (chunk.status === "completed") {
|
|
2473
|
+
updatedMessage.thinkingSteps = finalizeOpenSteps(
|
|
2474
|
+
updatedMessage.thinkingSteps,
|
|
2475
|
+
"completed",
|
|
2476
|
+
chunk.receivedAt
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
status = chunk.status === "completed" ? "complete" : "streaming";
|
|
2480
|
+
} else if (chunk.type === "responding" && (chunk.node ?? "") === FOLLOW_UP_NODE) {
|
|
2481
|
+
followUpScratch = `${followUpScratch ?? ""}${chunk.content ?? ""}`;
|
|
2482
|
+
if (chunk.status === "completed") {
|
|
2483
|
+
updatedMessage.followUpQuestions = parseFollowUps(followUpScratch);
|
|
2484
|
+
followUpScratch = void 0;
|
|
2485
|
+
}
|
|
2486
|
+
status = "tool_running";
|
|
2487
|
+
} else {
|
|
2488
|
+
status = chunk.status === "completed" && chunk.node === "end" ? "complete" : stateWithMessage.status === "idle" || stateWithMessage.status === "connecting" ? "thinking" : stateWithMessage.status;
|
|
2489
|
+
updatedMessage.pending = chunk.status !== "completed" || updatedMessage.pending;
|
|
2490
|
+
if (chunk.status === "completed" && chunk.node === "end") {
|
|
2491
|
+
updatedMessage.thinkingSteps = finalizeOpenSteps(
|
|
2492
|
+
updatedMessage.thinkingSteps,
|
|
2493
|
+
"completed",
|
|
2494
|
+
chunk.receivedAt
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (keepHitlWaiting) {
|
|
2499
|
+
status = "hitl_waiting";
|
|
2500
|
+
updatedMessage.pending = true;
|
|
2501
|
+
}
|
|
2502
|
+
const messages = stateWithMessage.messages.map(
|
|
2503
|
+
(m) => m.id === updatedMessage.id ? updatedMessage : m
|
|
2504
|
+
);
|
|
2505
|
+
return {
|
|
2506
|
+
...stateWithMessage,
|
|
2507
|
+
status,
|
|
2508
|
+
followUpScratch,
|
|
2509
|
+
messages,
|
|
2510
|
+
activeMessageId: updatedMessage.id,
|
|
2511
|
+
hitl: keepHitlWaiting ? stateWithMessage.hitl : void 0
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
return next;
|
|
2515
|
+
};
|
|
2516
|
+
var appendQuery = (url, values) => {
|
|
2517
|
+
for (const [key, value] of Object.entries(values)) {
|
|
2518
|
+
if (value) {
|
|
2519
|
+
url.searchParams.set(key, value);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return url;
|
|
2523
|
+
};
|
|
2524
|
+
var compactErrorBody2 = async (response) => {
|
|
2525
|
+
const body = await response.text().catch(() => "");
|
|
2526
|
+
const trimmed = body.trim();
|
|
2527
|
+
if (!trimmed) {
|
|
2528
|
+
return void 0;
|
|
2529
|
+
}
|
|
2530
|
+
const redacted = redactSensitiveText(trimmed);
|
|
2531
|
+
return redacted.length > 1e3 ? `${redacted.slice(0, 1e3)}...` : redacted;
|
|
2532
|
+
};
|
|
2533
|
+
var isObject22 = (value) => typeof value === "object" && value !== null;
|
|
2534
|
+
var stringOr2 = (value, fallback) => typeof value === "string" && value.trim() ? value : fallback;
|
|
2535
|
+
var numberOr = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2536
|
+
var arrayOrEmpty = (value) => Array.isArray(value) ? value : [];
|
|
2537
|
+
var reportTypeToKind = (value) => value === "architecture" || value === "waf" ? "waf" : "cost";
|
|
2538
|
+
var backendReportType = (kind) => {
|
|
2539
|
+
if (!kind || kind === "all") return void 0;
|
|
2540
|
+
return kind === "waf" ? "architecture" : kind;
|
|
2541
|
+
};
|
|
2542
|
+
var requireProjectId = (projectId) => {
|
|
2543
|
+
if (!projectId) {
|
|
2544
|
+
throw new Error("Project ID is required for report requests.");
|
|
2545
|
+
}
|
|
2546
|
+
return projectId;
|
|
2547
|
+
};
|
|
2548
|
+
var requireUserId = (userId) => {
|
|
2549
|
+
if (!userId) {
|
|
2550
|
+
throw new Error("Authenticated user ID is required for report requests.");
|
|
2551
|
+
}
|
|
2552
|
+
return userId;
|
|
2553
|
+
};
|
|
2554
|
+
var normalizeCostParsed = (report, metrics = {}) => {
|
|
2555
|
+
const parsed = isObject22(report.parsed) ? report.parsed : void 0;
|
|
2556
|
+
if (parsed && isObject22(parsed.totalSpend) && isObject22(parsed.estimatedSavings)) {
|
|
2557
|
+
return parsed;
|
|
2558
|
+
}
|
|
2559
|
+
const processed = isObject22(report.processed) ? report.processed : {};
|
|
2560
|
+
const dashboard = isObject22(report.dashboard) ? report.dashboard : {};
|
|
2561
|
+
const source = { ...metrics, ...dashboard, ...processed };
|
|
2562
|
+
const currency = stringOr2(source.currency, "USD");
|
|
2563
|
+
const monthly = numberOr(
|
|
2564
|
+
source.total_monthly_cost ?? source.monthly_cost ?? source.monthly,
|
|
2565
|
+
0
|
|
2566
|
+
);
|
|
2567
|
+
const savings = numberOr(
|
|
2568
|
+
source.total_monthly_savings ?? (isObject22(source.opportunity_summary) ? source.opportunity_summary.total_monthly_savings : void 0) ?? source.monthly_savings,
|
|
2569
|
+
0
|
|
2570
|
+
);
|
|
2571
|
+
return {
|
|
2572
|
+
totalSpend: {
|
|
2573
|
+
amount: monthly,
|
|
2574
|
+
currency,
|
|
2575
|
+
changePercent: numberOr(source.change_percent, 0)
|
|
2576
|
+
},
|
|
2577
|
+
estimatedSavings: {
|
|
2578
|
+
amount: savings,
|
|
2579
|
+
currency,
|
|
2580
|
+
percentOfSpend: monthly > 0 ? savings / monthly * 100 : 0
|
|
2581
|
+
},
|
|
2582
|
+
serviceGroups: arrayOrEmpty(
|
|
2583
|
+
source.service_family_breakdown ?? source.serviceGroups
|
|
2584
|
+
).map((item, index) => {
|
|
2585
|
+
const row = isObject22(item) ? item : {};
|
|
2586
|
+
return {
|
|
2587
|
+
name: stringOr2(row.name ?? row.service ?? row.family, `Service ${index + 1}`),
|
|
2588
|
+
amount: numberOr(row.amount ?? row.monthly_cost ?? row.cost, 0),
|
|
2589
|
+
currency: stringOr2(row.currency, currency),
|
|
2590
|
+
changePercent: numberOr(row.change_percent ?? row.changePercent, 0)
|
|
2591
|
+
};
|
|
2592
|
+
}),
|
|
2593
|
+
recommendations: arrayOrEmpty(
|
|
2594
|
+
source.recommendations ?? source.top_opportunities ?? source.opportunities
|
|
2595
|
+
).map((item, index) => {
|
|
2596
|
+
const row = isObject22(item) ? item : {};
|
|
2597
|
+
return {
|
|
2598
|
+
id: stringOr2(row.id ?? row.resource_id, `recommendation-${index + 1}`),
|
|
2599
|
+
title: stringOr2(row.title ?? row.recommendation ?? row.description, "Cost recommendation"),
|
|
2600
|
+
monthlySavings: numberOr(row.monthlySavings ?? row.monthly_savings ?? row.savings, 0),
|
|
2601
|
+
currency: stringOr2(row.currency, currency),
|
|
2602
|
+
risk: stringOr2(row.risk, "medium")
|
|
2603
|
+
};
|
|
2604
|
+
}),
|
|
2605
|
+
anomalies: arrayOrEmpty(source.anomalies),
|
|
2606
|
+
budgets: arrayOrEmpty(source.budgets),
|
|
2607
|
+
trend: arrayOrEmpty(source.trend ?? source.time_series)
|
|
2608
|
+
};
|
|
2609
|
+
};
|
|
2610
|
+
var normalizeWafParsed = (report, metrics = {}) => {
|
|
2611
|
+
const parsed = isObject22(report.parsed) ? report.parsed : void 0;
|
|
2612
|
+
if (parsed && isObject22(parsed.score) && isObject22(parsed.counts)) {
|
|
2613
|
+
return parsed;
|
|
2614
|
+
}
|
|
2615
|
+
const processed = isObject22(report.processed) ? report.processed : {};
|
|
2616
|
+
const source = { ...metrics, ...processed };
|
|
2617
|
+
const pillarScores = isObject22(source.pillar_scores) ? source.pillar_scores : {};
|
|
2618
|
+
const rules = arrayOrEmpty(report.all_rules ?? source.rules).map((item, index) => {
|
|
2619
|
+
const row = isObject22(item) ? item : {};
|
|
2620
|
+
const outcome = stringOr2(row.status ?? row.outcome, "pass").toLowerCase();
|
|
2621
|
+
return {
|
|
2622
|
+
id: stringOr2(row.id ?? row.rule_id ?? row.name, `rule-${index + 1}`),
|
|
2623
|
+
pillar: stringOr2(row.pillar, "Uncategorized"),
|
|
2624
|
+
title: stringOr2(row.title ?? row.description, "Architecture rule"),
|
|
2625
|
+
status: outcome === "fail" || outcome === "error" ? "fail" : outcome === "warn" ? "warn" : "pass",
|
|
2626
|
+
severity: stringOr2(row.severity, "medium").toLowerCase(),
|
|
2627
|
+
resource: typeof row.resource === "string" ? row.resource : void 0,
|
|
2628
|
+
evidence: typeof row.evidence === "string" ? row.evidence : void 0,
|
|
2629
|
+
recommendation: typeof row.recommendation === "string" ? row.recommendation : void 0
|
|
2630
|
+
};
|
|
2631
|
+
});
|
|
2632
|
+
const failedRules = rules.filter((rule) => rule.status === "fail");
|
|
2633
|
+
const mediumRules = rules.filter((rule) => rule.severity === "medium");
|
|
2634
|
+
const highRules = rules.filter(
|
|
2635
|
+
(rule) => rule.severity === "high" || rule.severity === "critical"
|
|
2636
|
+
);
|
|
2637
|
+
return {
|
|
2638
|
+
score: {
|
|
2639
|
+
overall: numberOr(source.overall_score, 0),
|
|
2640
|
+
pillars: Object.entries(pillarScores).map(([id, score]) => ({
|
|
2641
|
+
id,
|
|
2642
|
+
label: id,
|
|
2643
|
+
score: numberOr(score, 0),
|
|
2644
|
+
passed: 0,
|
|
2645
|
+
warned: 0,
|
|
2646
|
+
failed: 0
|
|
2647
|
+
}))
|
|
2648
|
+
},
|
|
2649
|
+
counts: {
|
|
2650
|
+
passed: rules.length - failedRules.length,
|
|
2651
|
+
highRisk: numberOr(source.high_count, highRules.length),
|
|
2652
|
+
mediumRisk: numberOr(source.medium_count, mediumRules.length),
|
|
2653
|
+
evidenceCoveragePercent: numberOr(source.evidence_coverage_percent, 0)
|
|
2654
|
+
},
|
|
2655
|
+
rules
|
|
2656
|
+
};
|
|
2657
|
+
};
|
|
2658
|
+
var normalizeBackendReportDetail = (input, fallback) => {
|
|
2659
|
+
if (!isObject22(input)) {
|
|
2660
|
+
return normalizeReportEnvelope(input);
|
|
2661
|
+
}
|
|
2662
|
+
const report = isObject22(input.report) ? input.report : input;
|
|
2663
|
+
const reportType = input.report_type ?? report.kind ?? fallback.reportType;
|
|
2664
|
+
const kind = reportTypeToKind(reportType);
|
|
2665
|
+
const projectId = stringOr2(input.project_id ?? report.project_id, fallback.projectId);
|
|
2666
|
+
const generatedAt = stringOr2(
|
|
2667
|
+
input.timestamp ?? report.generated_at ?? (isObject22(report.metadata) ? report.metadata.generated_at : void 0),
|
|
2668
|
+
(/* @__PURE__ */ new Date(0)).toISOString()
|
|
2669
|
+
);
|
|
2670
|
+
const parsed = kind === "waf" ? normalizeWafParsed(report) : normalizeCostParsed(report);
|
|
2671
|
+
return {
|
|
2672
|
+
id: stringOr2(report.id, `${kind}:latest:${projectId}`),
|
|
2673
|
+
kind,
|
|
2674
|
+
projectId,
|
|
2675
|
+
generatedAt,
|
|
2676
|
+
source: { provider: "azure" },
|
|
2677
|
+
raw: report,
|
|
2678
|
+
parsed,
|
|
2679
|
+
formatted: isObject22(report.formatted) ? normalizeReportEnvelope({ ...report, kind, project_id: projectId }).formatted : void 0
|
|
2680
|
+
};
|
|
2681
|
+
};
|
|
2682
|
+
var normalizeBackendHistoryItem = (input) => {
|
|
2683
|
+
if (!isObject22(input)) {
|
|
2684
|
+
return normalizeReportEnvelope(input);
|
|
2685
|
+
}
|
|
2686
|
+
const kind = reportTypeToKind(input.report_type);
|
|
2687
|
+
const projectId = stringOr2(input.project_id, "unknown-project");
|
|
2688
|
+
const generatedAt = stringOr2(input.generated_at, (/* @__PURE__ */ new Date(0)).toISOString());
|
|
2689
|
+
const metrics = isObject22(input.metrics) ? input.metrics : {};
|
|
2690
|
+
return {
|
|
2691
|
+
id: stringOr2(input.report_id ?? input.id, `${kind}:${generatedAt}`),
|
|
2692
|
+
kind,
|
|
2693
|
+
projectId,
|
|
2694
|
+
generatedAt,
|
|
2695
|
+
source: { provider: "azure" },
|
|
2696
|
+
raw: input,
|
|
2697
|
+
parsed: kind === "waf" ? normalizeWafParsed({}, metrics) : normalizeCostParsed({}, metrics),
|
|
2698
|
+
formatted: {
|
|
2699
|
+
title: `${kind === "waf" ? "Well-Architected Framework" : "Cost"} report`,
|
|
2700
|
+
summary: stringOr2(input.status, "Report available"),
|
|
2701
|
+
sections: []
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
};
|
|
2705
|
+
var normalizeBackendHistory = (input) => {
|
|
2706
|
+
const items = isObject22(input) && Array.isArray(input.items) ? input.items : input;
|
|
2707
|
+
if (!Array.isArray(items)) {
|
|
2708
|
+
return normalizeReportList(input);
|
|
2709
|
+
}
|
|
2710
|
+
return items.map((item) => normalizeBackendHistoryItem(item));
|
|
2711
|
+
};
|
|
2712
|
+
var fetchJson = async (options, path2, query = {}) => {
|
|
2713
|
+
const apiBase = normalizeApiBase(options.baseUrl);
|
|
2714
|
+
const url = appendQuery(new URL(`${apiBase}${path2}`), query);
|
|
2715
|
+
const response = await fetch(url, {
|
|
2716
|
+
method: "GET",
|
|
2717
|
+
headers: getCLIHeaders(options.authToken)
|
|
2718
|
+
});
|
|
2719
|
+
if (!response.ok) {
|
|
2720
|
+
const body = await compactErrorBody2(response);
|
|
2721
|
+
throw new Error(
|
|
2722
|
+
`Report request failed with status ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
return response.json();
|
|
2726
|
+
};
|
|
2727
|
+
var postJson = async (options, path2, query = {}, body) => {
|
|
2728
|
+
const apiBase = normalizeApiBase(options.baseUrl);
|
|
2729
|
+
const url = appendQuery(new URL(`${apiBase}${path2}`), query);
|
|
2730
|
+
const response = await fetch(url, {
|
|
2731
|
+
method: "POST",
|
|
2732
|
+
headers: withIdempotencyHeader({
|
|
2733
|
+
...getCLIHeaders(options.authToken),
|
|
2734
|
+
"content-type": "application/json"
|
|
2735
|
+
}),
|
|
2736
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
2737
|
+
});
|
|
2738
|
+
if (!response.ok) {
|
|
2739
|
+
const bodyText = await compactErrorBody2(response);
|
|
2740
|
+
throw new Error(
|
|
2741
|
+
`Report request failed with status ${response.status} ${response.statusText}${bodyText ? `: ${bodyText}` : ""}`
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
return response.json();
|
|
2745
|
+
};
|
|
2746
|
+
var fetchReportResource = async (options, path2, query = {}) => fetchJson(options, path2, query);
|
|
2747
|
+
var listReports = async (options) => {
|
|
2748
|
+
const raw = await fetchJson(options, "/reports/history", {
|
|
2749
|
+
user_id: requireUserId(options.userId),
|
|
2750
|
+
project_ids: requireProjectId(options.projectId),
|
|
2751
|
+
report_type: backendReportType(options.kind)
|
|
2752
|
+
});
|
|
2753
|
+
return normalizeBackendHistory(raw).filter((report) => {
|
|
2754
|
+
if (!options.kind || options.kind === "all") return true;
|
|
2755
|
+
return report.kind === options.kind;
|
|
2756
|
+
});
|
|
2757
|
+
};
|
|
2758
|
+
var getReport = async (options) => {
|
|
2759
|
+
if (options.reportId.startsWith("latest:") || options.reportId.startsWith("history:")) {
|
|
2760
|
+
const reports = await listReports({
|
|
2761
|
+
baseUrl: options.baseUrl,
|
|
2762
|
+
authToken: options.authToken,
|
|
2763
|
+
projectId: options.projectId,
|
|
2764
|
+
userId: options.userId,
|
|
2765
|
+
kind: "all"
|
|
2766
|
+
});
|
|
2767
|
+
const found = reports.find((report) => report.id === options.reportId);
|
|
2768
|
+
if (found) {
|
|
2769
|
+
const reportType = found.kind === "waf" ? "architecture" : "cost";
|
|
2770
|
+
return normalizeBackendReportDetail(
|
|
2771
|
+
await fetchJson(
|
|
2772
|
+
options,
|
|
2773
|
+
`/reports/detail/${encodeURIComponent(found.projectId)}/${reportType}`,
|
|
2774
|
+
{
|
|
2775
|
+
user_id: requireUserId(options.userId),
|
|
2776
|
+
timestamp: found.raw && isObject22(found.raw) && !found.raw.is_latest ? found.generatedAt : void 0
|
|
2777
|
+
}
|
|
2778
|
+
),
|
|
2779
|
+
{ projectId: found.projectId, reportType }
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
const raw = await fetchJson(
|
|
2784
|
+
options,
|
|
2785
|
+
`/reports/${encodeURIComponent(options.reportId)}`,
|
|
2786
|
+
{ project_id: options.projectId, view: options.view }
|
|
2787
|
+
);
|
|
2788
|
+
return normalizeReportEnvelope(raw);
|
|
2789
|
+
};
|
|
2790
|
+
var getCostReport = async (options) => {
|
|
2791
|
+
const projectId = requireProjectId(options.projectId);
|
|
2792
|
+
const raw = await fetchJson(
|
|
2793
|
+
options,
|
|
2794
|
+
`/reports/detail/${encodeURIComponent(projectId)}/cost`,
|
|
2795
|
+
{ user_id: requireUserId(options.userId) }
|
|
2796
|
+
);
|
|
2797
|
+
return normalizeBackendReportDetail(raw, { projectId, reportType: "cost" });
|
|
2798
|
+
};
|
|
2799
|
+
var getWafReport = async (options) => {
|
|
2800
|
+
const projectId = requireProjectId(options.projectId);
|
|
2801
|
+
const raw = await fetchJson(
|
|
2802
|
+
options,
|
|
2803
|
+
`/reports/detail/${encodeURIComponent(projectId)}/architecture`,
|
|
2804
|
+
{ user_id: requireUserId(options.userId) }
|
|
2805
|
+
);
|
|
2806
|
+
const report = normalizeBackendReportDetail(raw, { projectId, reportType: "architecture" });
|
|
2807
|
+
if (options.severity && isObject22(report.parsed) && Array.isArray(report.parsed.rules)) {
|
|
2808
|
+
return {
|
|
2809
|
+
...report,
|
|
2810
|
+
parsed: {
|
|
2811
|
+
...report.parsed,
|
|
2812
|
+
rules: report.parsed.rules.filter(
|
|
2813
|
+
(rule) => isObject22(rule) && String(rule.severity).toLowerCase() === options.severity?.toLowerCase()
|
|
2814
|
+
)
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
return report;
|
|
2819
|
+
};
|
|
2820
|
+
var getReportDetail = async (options) => fetchJson(
|
|
2821
|
+
options,
|
|
2822
|
+
`/reports/detail/${encodeURIComponent(options.projectId)}/${encodeURIComponent(
|
|
2823
|
+
options.reportType
|
|
2824
|
+
)}`,
|
|
2825
|
+
{
|
|
2826
|
+
user_id: options.userId,
|
|
2827
|
+
timestamp: options.timestamp
|
|
2828
|
+
}
|
|
2829
|
+
);
|
|
2830
|
+
var getCostReportFull = async (options) => fetchJson(options, `/cost-reports/${encodeURIComponent(options.projectId)}/full`, {
|
|
2831
|
+
user_id: options.userId
|
|
2832
|
+
});
|
|
2833
|
+
var getWafReportFull = async (options) => fetchJson(
|
|
2834
|
+
options,
|
|
2835
|
+
`/well-architected-reports/${encodeURIComponent(options.projectId)}/full`,
|
|
2836
|
+
{
|
|
2837
|
+
user_id: options.userId
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2840
|
+
var getCostReportHistory = async (options) => options.timestamp ? fetchJson(
|
|
2841
|
+
options,
|
|
2842
|
+
`/cost-reports/${encodeURIComponent(options.projectId)}/historical/${encodeURIComponent(
|
|
2843
|
+
options.timestamp
|
|
2844
|
+
)}`,
|
|
2845
|
+
{ user_id: options.userId }
|
|
2846
|
+
) : fetchJson(options, `/cost-reports/${encodeURIComponent(options.projectId)}/historical`, {
|
|
2847
|
+
user_id: options.userId
|
|
2848
|
+
});
|
|
2849
|
+
var getWafReportHistory = async (options) => options.timestamp ? fetchJson(
|
|
2850
|
+
options,
|
|
2851
|
+
`/well-architected-reports/${encodeURIComponent(
|
|
2852
|
+
options.projectId
|
|
2853
|
+
)}/history/${encodeURIComponent(options.timestamp)}`,
|
|
2854
|
+
{ user_id: options.userId }
|
|
2855
|
+
) : fetchJson(
|
|
2856
|
+
options,
|
|
2857
|
+
`/well-architected-reports/${encodeURIComponent(options.projectId)}/history`,
|
|
2858
|
+
{ user_id: options.userId }
|
|
2859
|
+
);
|
|
2860
|
+
var reportRunTypes = (type) => {
|
|
2861
|
+
if (type === "all") return ["cost", "waf", "unit-tests"];
|
|
2862
|
+
if (type === "architecture") return ["waf"];
|
|
2863
|
+
return [type];
|
|
2864
|
+
};
|
|
2865
|
+
var boolQuery = (value) => value === void 0 ? void 0 : String(Boolean(value));
|
|
2866
|
+
var runReports = async (options) => {
|
|
2867
|
+
const projectId = requireProjectId(options.projectId);
|
|
2868
|
+
const userId = requireUserId(options.userId);
|
|
2869
|
+
const results = [];
|
|
2870
|
+
for (const type of reportRunTypes(options.type)) {
|
|
2871
|
+
if (type === "cost") {
|
|
2872
|
+
results.push(
|
|
2873
|
+
await postJson(options, `/cost-reports/${encodeURIComponent(projectId)}/regenerate`, {
|
|
2874
|
+
user_id: userId,
|
|
2875
|
+
region: options.region,
|
|
2876
|
+
currency: options.currency,
|
|
2877
|
+
include_time_series: boolQuery(options.includeTimeSeries),
|
|
2878
|
+
save_report: boolQuery(options.saveReport)
|
|
2879
|
+
})
|
|
2880
|
+
);
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
if (type === "waf") {
|
|
2884
|
+
results.push(
|
|
2885
|
+
await postJson(
|
|
2886
|
+
options,
|
|
2887
|
+
`/well-architected-reports/${encodeURIComponent(projectId)}/regenerate`,
|
|
2888
|
+
{
|
|
2889
|
+
user_id: userId,
|
|
2890
|
+
save_report: boolQuery(options.saveReport)
|
|
2891
|
+
}
|
|
2892
|
+
)
|
|
2893
|
+
);
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
results.push(
|
|
2897
|
+
await postJson(options, `/reports/${encodeURIComponent(projectId)}/unit-tests/regenerate`, {
|
|
2898
|
+
user_id: userId
|
|
2899
|
+
})
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
return results;
|
|
2903
|
+
};
|
|
2904
|
+
var getReportJobStatus = async (options) => fetchJson(options, `/jobs/${encodeURIComponent(options.jobId)}`, {
|
|
2905
|
+
user_id: requireUserId(options.userId)
|
|
2906
|
+
});
|
|
2907
|
+
var CREDIT_LOW_RATIO = 0.1;
|
|
2908
|
+
var CREDIT_WARNING_RATIO = 0.25;
|
|
2909
|
+
var DEFAULT_FREE_TRIAL_CREDITS_TOTAL = 1e3;
|
|
2910
|
+
var fetchBillingJson = async (options, path2, query = {}, request = {}) => {
|
|
2911
|
+
const url = new URL(`${normalizeApiBase(options.baseUrl)}${path2}`);
|
|
2912
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2913
|
+
if (value !== void 0 && value !== "") {
|
|
2914
|
+
url.searchParams.set(key, String(value));
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const headers = request.method === "POST" ? withIdempotencyHeader(getCLIHeaders(options.authToken)) : getCLIHeaders(options.authToken);
|
|
2918
|
+
if (request.body) {
|
|
2919
|
+
headers["content-type"] = "application/json";
|
|
2920
|
+
}
|
|
2921
|
+
const response = await fetch(url, {
|
|
2922
|
+
method: request.method ?? "GET",
|
|
2923
|
+
headers,
|
|
2924
|
+
...request.body ? { body: JSON.stringify(request.body) } : {}
|
|
2925
|
+
});
|
|
2926
|
+
if (!response.ok) {
|
|
2927
|
+
const body = await response.text().catch(() => "");
|
|
2928
|
+
const redactedBody = redactSensitiveText(body.trim());
|
|
2929
|
+
throw new Error(
|
|
2930
|
+
`Billing request failed with status ${response.status} ${response.statusText}${redactedBody ? `: ${redactedBody}` : ""}`
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
return await response.json();
|
|
2934
|
+
};
|
|
2935
|
+
var unwrapData = (value) => {
|
|
2936
|
+
if (value && typeof value === "object" && "data" in value && value.data) {
|
|
2937
|
+
return value.data;
|
|
2938
|
+
}
|
|
2939
|
+
return value;
|
|
2940
|
+
};
|
|
2941
|
+
var getBillingConfig = (options) => fetchBillingJson(options, "/billing/config");
|
|
2942
|
+
var getBillingEntitlement = async (options) => unwrapData(await fetchBillingJson(
|
|
2943
|
+
options,
|
|
2944
|
+
"/billing/entitlement"
|
|
2945
|
+
));
|
|
2946
|
+
var getSubscriptionStatus = (options) => fetchBillingJson(options, "/billing/subscription/status");
|
|
2947
|
+
var getSubscriptionBillingInfo = (options) => fetchBillingJson(options, "/billing/subscription/billing-info", {
|
|
2948
|
+
limit: Math.max(1, Math.min(50, Number(options.limit ?? 20)))
|
|
2949
|
+
});
|
|
2950
|
+
var getTopUpPacks = (options) => fetchBillingJson(options, "/billing/top-up/packs");
|
|
2951
|
+
var createTopUpCheckoutSession = (options) => fetchBillingJson(
|
|
2952
|
+
options,
|
|
2953
|
+
"/billing/checkout/session/top-up",
|
|
2954
|
+
{},
|
|
2955
|
+
{
|
|
2956
|
+
method: "POST",
|
|
2957
|
+
body: {
|
|
2958
|
+
pack_id: options.packId,
|
|
2959
|
+
...options.returnTo ? { return_to: options.returnTo } : {},
|
|
2960
|
+
...options.preferredCurrency ? { preferred_currency: options.preferredCurrency } : {},
|
|
2961
|
+
...options.countryCode ? { country_code: options.countryCode } : {},
|
|
2962
|
+
...options.contactEmail ? { contact_email: options.contactEmail } : {},
|
|
2963
|
+
...options.contactPhone ? { contact_phone: options.contactPhone } : {},
|
|
2964
|
+
...options.contactCountryCode ? { contact_country_code: options.contactCountryCode } : {}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
);
|
|
2968
|
+
var getBillingNotifications = (options) => fetchBillingJson(options, "/billing/notifications", {
|
|
2969
|
+
limit: Math.max(1, Math.min(100, Number(options.limit ?? 25)))
|
|
2970
|
+
});
|
|
2971
|
+
var getBillingUsageSummary = (options) => fetchBillingJson(options, "/billing/usage/summary", {
|
|
2972
|
+
start_at: options.startAt,
|
|
2973
|
+
end_at: options.endAt,
|
|
2974
|
+
granularity: options.granularity,
|
|
2975
|
+
action_type: options.actionType,
|
|
2976
|
+
model_name: options.modelName,
|
|
2977
|
+
outcome: options.outcome,
|
|
2978
|
+
trigger_mode: options.triggerMode,
|
|
2979
|
+
charge_status: options.chargeStatus
|
|
2980
|
+
});
|
|
2981
|
+
var getBillingUsageLedger = (options) => fetchBillingJson(options, "/billing/usage/ledger", {
|
|
2982
|
+
start_at: options.startAt,
|
|
2983
|
+
end_at: options.endAt,
|
|
2984
|
+
action_type: options.actionType,
|
|
2985
|
+
model_name: options.modelName,
|
|
2986
|
+
outcome: options.outcome,
|
|
2987
|
+
trigger_mode: options.triggerMode,
|
|
2988
|
+
charge_status: options.chargeStatus,
|
|
2989
|
+
limit: options.limit,
|
|
2990
|
+
cursor: options.cursor
|
|
2991
|
+
});
|
|
2992
|
+
var isFreePlan = (plan) => {
|
|
2993
|
+
if (!plan) {
|
|
2994
|
+
return false;
|
|
2995
|
+
}
|
|
2996
|
+
const id = String(plan.id || "").toLowerCase();
|
|
2997
|
+
const name = String(plan.name || "").toLowerCase();
|
|
2998
|
+
return id === "free" || name === "free" || Number(plan.price_usd ?? NaN) === 0;
|
|
2999
|
+
};
|
|
3000
|
+
var getCreditStatus = (summary, options) => {
|
|
3001
|
+
if (!summary) {
|
|
3002
|
+
return null;
|
|
3003
|
+
}
|
|
3004
|
+
const planName = summary.plan?.name || "Plan";
|
|
3005
|
+
const planId = String(summary.plan?.id || "").toLowerCase();
|
|
3006
|
+
const normalizedPlanName = String(summary.plan?.name || "").toLowerCase();
|
|
3007
|
+
const planSource = String(summary.plan_source || "").toLowerCase();
|
|
3008
|
+
const isFreeLikePlan = isFreePlan(summary.plan) || planSource === "trial" || planSource === "free" || planSource === "free_blocked";
|
|
3009
|
+
const explicitUnlimited = planId.includes("enterprise") || normalizedPlanName.includes("enterprise");
|
|
3010
|
+
const balance = summary.balance || {};
|
|
3011
|
+
const cycleTotal = Math.max(
|
|
3012
|
+
Number(balance.credits_total_cycle ?? balance.credits_total ?? 0),
|
|
3013
|
+
0
|
|
3014
|
+
);
|
|
3015
|
+
const cycleRemaining = Math.max(
|
|
3016
|
+
Number(balance.credits_remaining_cycle ?? balance.credits_remaining ?? 0),
|
|
3017
|
+
0
|
|
3018
|
+
);
|
|
3019
|
+
const cycleUsed = Math.max(
|
|
3020
|
+
Number.isFinite(Number(balance.credits_used_cycle ?? balance.credits_used)) ? Number(balance.credits_used_cycle ?? balance.credits_used) : cycleTotal - cycleRemaining,
|
|
3021
|
+
0
|
|
3022
|
+
);
|
|
3023
|
+
const topUpBalance = Math.max(
|
|
3024
|
+
Number(balance.top_up_credits_balance ?? summary.top_up_credits_balance ?? 0),
|
|
3025
|
+
0
|
|
3026
|
+
);
|
|
3027
|
+
const fallbackEffectiveTotal = cycleTotal + topUpBalance;
|
|
3028
|
+
const fallbackEffectiveRemaining = cycleRemaining + topUpBalance;
|
|
3029
|
+
const reportedEffectiveTotal = Number(balance.credits_total_effective ?? NaN);
|
|
3030
|
+
const reportedEffectiveRemaining = Number(
|
|
3031
|
+
balance.credits_remaining_effective ?? summary.credits_remaining_total ?? NaN
|
|
3032
|
+
);
|
|
3033
|
+
let effectiveTotal = Number.isFinite(reportedEffectiveTotal) ? Math.max(reportedEffectiveTotal, fallbackEffectiveTotal, 0) : fallbackEffectiveTotal;
|
|
3034
|
+
const effectiveRemaining = Number.isFinite(reportedEffectiveRemaining) ? Math.max(reportedEffectiveRemaining, fallbackEffectiveRemaining, 0) : fallbackEffectiveRemaining;
|
|
3035
|
+
if (effectiveTotal <= 0) {
|
|
3036
|
+
effectiveTotal = fallbackEffectiveTotal;
|
|
3037
|
+
}
|
|
3038
|
+
if (effectiveRemaining > effectiveTotal) {
|
|
3039
|
+
effectiveTotal = effectiveRemaining;
|
|
3040
|
+
}
|
|
3041
|
+
const trialTotal = Math.max(
|
|
3042
|
+
Number(
|
|
3043
|
+
summary.trial_state?.credits_total ?? summary.trial_state?.initial_credits ?? summary.plan?.features?.trial_credits_total ?? DEFAULT_FREE_TRIAL_CREDITS_TOTAL
|
|
3044
|
+
),
|
|
3045
|
+
0
|
|
3046
|
+
);
|
|
3047
|
+
const useTrialDisplayTotal = isFreeLikePlan && effectiveTotal <= 0 && trialTotal > 0;
|
|
3048
|
+
const displayTotal = useTrialDisplayTotal ? trialTotal : effectiveTotal;
|
|
3049
|
+
const remainingCandidate = useTrialDisplayTotal && (summary.trial_state?.consumed || summary.trial_state?.blocked) ? 0 : effectiveRemaining;
|
|
3050
|
+
const remaining = displayTotal > 0 ? Math.min(Math.max(remainingCandidate, 0), displayTotal) : 0;
|
|
3051
|
+
const used = displayTotal > 0 ? Math.max(displayTotal - remaining, 0) : 0;
|
|
3052
|
+
const remainingRatio = displayTotal > 0 ? remaining / displayTotal : explicitUnlimited ? 1 : 0;
|
|
3053
|
+
const creditsPerEvent = options?.creditsPerEvent;
|
|
3054
|
+
const messagesRemaining = explicitUnlimited || !creditsPerEvent || creditsPerEvent <= 0 ? null : Math.max(0, Math.floor(remaining / creditsPerEvent));
|
|
3055
|
+
const exhausted = summary.credits_exhausted === true || !explicitUnlimited && remaining <= 0;
|
|
3056
|
+
const low = !exhausted && (summary.credits_low === true || remainingRatio <= CREDIT_LOW_RATIO);
|
|
3057
|
+
const warning = !exhausted && !low && remainingRatio <= CREDIT_WARNING_RATIO;
|
|
3058
|
+
const tone = exhausted ? "exhausted" : low ? "low" : warning ? "warning" : "normal";
|
|
3059
|
+
return {
|
|
3060
|
+
planName,
|
|
3061
|
+
remaining,
|
|
3062
|
+
total: displayTotal,
|
|
3063
|
+
used,
|
|
3064
|
+
cycleTotal,
|
|
3065
|
+
cycleUsed,
|
|
3066
|
+
cycleRemaining,
|
|
3067
|
+
effectiveTotal,
|
|
3068
|
+
effectiveRemaining,
|
|
3069
|
+
topUpBalance,
|
|
3070
|
+
remainingRatio,
|
|
3071
|
+
isUnlimited: explicitUnlimited,
|
|
3072
|
+
tone,
|
|
3073
|
+
messagesRemaining
|
|
3074
|
+
};
|
|
3075
|
+
};
|
|
3076
|
+
var providerValues = /* @__PURE__ */ new Set(["azure", "aws", "gcp"]);
|
|
3077
|
+
var sanitizeNamePart = (value) => value.replace(/\.(json|yaml|yml|tf)$/i, "").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
3078
|
+
var inferCloudProvider = (owner, repo, filePath) => {
|
|
3079
|
+
const haystack = `${owner}/${repo}/${filePath}`.toLowerCase();
|
|
3080
|
+
if (haystack.includes("aws") || haystack.includes("cloudformation")) {
|
|
3081
|
+
return "aws";
|
|
3082
|
+
}
|
|
3083
|
+
if (haystack.includes("gcp") || haystack.includes("google")) {
|
|
3084
|
+
return "gcp";
|
|
3085
|
+
}
|
|
3086
|
+
return "azure";
|
|
3087
|
+
};
|
|
3088
|
+
var generateProjectName = (filePath, repo) => {
|
|
3089
|
+
const parts = filePath.split("/").filter(Boolean);
|
|
3090
|
+
const file = parts[parts.length - 1] || repo;
|
|
3091
|
+
const parent = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
3092
|
+
if (/^azuredeploy\.json$/i.test(file) && parent) {
|
|
3093
|
+
return sanitizeNamePart(parent);
|
|
3094
|
+
}
|
|
3095
|
+
return sanitizeNamePart(file || repo) || repo;
|
|
3096
|
+
};
|
|
3097
|
+
var parseTemplateUrl = (value) => {
|
|
3098
|
+
try {
|
|
3099
|
+
const url = new URL(value);
|
|
3100
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
3101
|
+
if (url.hostname === "raw.githubusercontent.com") {
|
|
3102
|
+
if (parts.length < 3) {
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
const [owner2, repo2, branch2, ...fileParts] = parts;
|
|
3106
|
+
const filePath2 = fileParts.join("/");
|
|
3107
|
+
return {
|
|
3108
|
+
normalizedUrl: value,
|
|
3109
|
+
githubUrl: `https://github.com/${owner2}/${repo2}/blob/${branch2}/${filePath2}`,
|
|
3110
|
+
cloudProvider: inferCloudProvider(owner2, repo2, filePath2),
|
|
3111
|
+
suggestedName: generateProjectName(filePath2, repo2),
|
|
3112
|
+
suggestedDescription: `Template from ${owner2}/${repo2}`,
|
|
3113
|
+
owner: owner2,
|
|
3114
|
+
repo: repo2,
|
|
3115
|
+
branch: branch2,
|
|
3116
|
+
filePath: filePath2
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
if (url.hostname !== "github.com" || parts.length < 4) {
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
const [owner, repo, type, branch, ...rest] = parts;
|
|
3123
|
+
if (type !== "blob" && type !== "tree") {
|
|
3124
|
+
return null;
|
|
3125
|
+
}
|
|
3126
|
+
const rawRest = rest.join("/");
|
|
3127
|
+
const filePath = rawRest && /\.(json|yaml|yml|tf)$/i.test(rawRest) ? rawRest : rawRest ? `${rawRest}/azuredeploy.json` : "azuredeploy.json";
|
|
3128
|
+
return {
|
|
3129
|
+
normalizedUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`,
|
|
3130
|
+
githubUrl: value,
|
|
3131
|
+
cloudProvider: inferCloudProvider(owner, repo, filePath),
|
|
3132
|
+
suggestedName: generateProjectName(filePath, repo),
|
|
3133
|
+
suggestedDescription: `Template from ${owner}/${repo}`,
|
|
3134
|
+
owner,
|
|
3135
|
+
repo,
|
|
3136
|
+
branch,
|
|
3137
|
+
filePath
|
|
3138
|
+
};
|
|
3139
|
+
} catch {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
var buildQuickProjectPayload = (input) => {
|
|
3144
|
+
const inferred = input.templateUrl ? parseTemplateUrl(input.templateUrl) : null;
|
|
3145
|
+
const normalizedTemplateUrl = inferred?.normalizedUrl ?? input.templateUrl;
|
|
3146
|
+
const provider = input.provider ?? inferred?.cloudProvider ?? "azure";
|
|
3147
|
+
if (!providerValues.has(provider)) {
|
|
3148
|
+
throw new Error(`Unsupported cloud provider '${provider}'.`);
|
|
3149
|
+
}
|
|
3150
|
+
const name = input.name?.trim() || inferred?.suggestedName || "Quick Project";
|
|
3151
|
+
const description = input.description?.trim() || inferred?.suggestedDescription || `Template project for ${name}`;
|
|
3152
|
+
const connection = {
|
|
3153
|
+
user_id: input.userId,
|
|
3154
|
+
name: `${name} Connection`,
|
|
3155
|
+
cloud_provider: provider,
|
|
3156
|
+
description,
|
|
3157
|
+
type: "template",
|
|
3158
|
+
...normalizedTemplateUrl ? { template_url: normalizedTemplateUrl } : {},
|
|
3159
|
+
...input.parametersUrl ? { parameters_file_url: input.parametersUrl } : {},
|
|
3160
|
+
auto_sync: true
|
|
3161
|
+
};
|
|
3162
|
+
const project = {
|
|
3163
|
+
user_id: input.userId,
|
|
3164
|
+
name,
|
|
3165
|
+
description,
|
|
3166
|
+
cloud_provider: provider,
|
|
3167
|
+
connection_ids: [],
|
|
3168
|
+
type: "template",
|
|
3169
|
+
report_config: {
|
|
3170
|
+
auto_generate_reports: true,
|
|
3171
|
+
include_cost_report: true,
|
|
3172
|
+
include_cost_forecast: true,
|
|
3173
|
+
region: "eastus",
|
|
3174
|
+
currency: "USD"
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
return {
|
|
3178
|
+
connection,
|
|
3179
|
+
project,
|
|
3180
|
+
normalizedTemplateUrl,
|
|
3181
|
+
inferred
|
|
3182
|
+
};
|
|
3183
|
+
};
|
|
3184
|
+
var responseJson = async (response, label) => {
|
|
3185
|
+
if (!response.ok) {
|
|
3186
|
+
const body = await response.text().catch(() => "");
|
|
3187
|
+
const redactedBody = redactSensitiveText(body.trim());
|
|
3188
|
+
throw new Error(
|
|
3189
|
+
`${label} failed with status ${response.status} ${response.statusText}${redactedBody ? `: ${redactedBody}` : ""}`
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
return await response.json();
|
|
3193
|
+
};
|
|
3194
|
+
var appendConnectionBody = (payload, input) => {
|
|
3195
|
+
if (!input.templateFile && !input.parametersFile) {
|
|
3196
|
+
return JSON.stringify(payload);
|
|
3197
|
+
}
|
|
3198
|
+
const formData = new FormData();
|
|
3199
|
+
formData.append("user_id", payload.user_id);
|
|
3200
|
+
formData.append("name", payload.name);
|
|
3201
|
+
formData.append("cloud_provider", payload.cloud_provider);
|
|
3202
|
+
formData.append("description", payload.description);
|
|
3203
|
+
formData.append("type", payload.type);
|
|
3204
|
+
formData.append("auto_sync", String(payload.auto_sync ?? true));
|
|
3205
|
+
if (payload.template_url) {
|
|
3206
|
+
formData.append("template_url", payload.template_url);
|
|
3207
|
+
}
|
|
3208
|
+
if (payload.parameters_file_url) {
|
|
3209
|
+
formData.append("parameters_file_url", payload.parameters_file_url);
|
|
3210
|
+
}
|
|
3211
|
+
if (input.templateFile) {
|
|
3212
|
+
formData.append("template_file", input.templateFile, input.templateFileName || "template.json");
|
|
3213
|
+
}
|
|
3214
|
+
if (input.parametersFile) {
|
|
3215
|
+
formData.append(
|
|
3216
|
+
"parameters_file",
|
|
3217
|
+
input.parametersFile,
|
|
3218
|
+
input.parametersFileName || "parameters.json"
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
return formData;
|
|
3222
|
+
};
|
|
3223
|
+
var headersForBody = (authToken, body) => {
|
|
3224
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) {
|
|
3225
|
+
const headers = getCLIHeaders(authToken);
|
|
3226
|
+
delete headers["Content-Type"];
|
|
3227
|
+
return headers;
|
|
3228
|
+
}
|
|
3229
|
+
return getCLIHeaders(authToken);
|
|
3230
|
+
};
|
|
3231
|
+
var createQuickProject = async (input) => {
|
|
3232
|
+
if (!input.templateUrl && !input.templateFile) {
|
|
3233
|
+
throw new Error("Provide --template-url or --template-file.");
|
|
3234
|
+
}
|
|
3235
|
+
const built = buildQuickProjectPayload(input);
|
|
3236
|
+
const apiBase = normalizeApiBase(input.baseUrl);
|
|
3237
|
+
const connectionBody = appendConnectionBody(built.connection, input);
|
|
3238
|
+
const connection = await responseJson(
|
|
3239
|
+
await fetch(`${apiBase}/connection/`, {
|
|
3240
|
+
method: "POST",
|
|
3241
|
+
headers: withIdempotencyHeader(headersForBody(input.authToken, connectionBody)),
|
|
3242
|
+
body: connectionBody
|
|
3243
|
+
}),
|
|
3244
|
+
"Connection creation"
|
|
3245
|
+
);
|
|
3246
|
+
const connectionId = String(connection.id || "");
|
|
3247
|
+
if (!connectionId) {
|
|
3248
|
+
throw new Error("Connection creation did not return a connection id.");
|
|
3249
|
+
}
|
|
3250
|
+
const projectPayload = {
|
|
3251
|
+
...built.project,
|
|
3252
|
+
connection_ids: [connectionId]
|
|
3253
|
+
};
|
|
3254
|
+
const project = await responseJson(
|
|
3255
|
+
await fetch(`${apiBase}/projects/`, {
|
|
3256
|
+
method: "POST",
|
|
3257
|
+
headers: withIdempotencyHeader(getCLIHeaders(input.authToken)),
|
|
3258
|
+
body: JSON.stringify(projectPayload)
|
|
3259
|
+
}),
|
|
3260
|
+
"Project creation"
|
|
3261
|
+
);
|
|
3262
|
+
return {
|
|
3263
|
+
project,
|
|
3264
|
+
connection,
|
|
3265
|
+
syncStatus: connection.sync_status ?? connection.sync_job ?? null,
|
|
3266
|
+
normalizedTemplateUrl: built.normalizedTemplateUrl,
|
|
3267
|
+
inferred: built.inferred
|
|
3268
|
+
};
|
|
3269
|
+
};
|
|
3270
|
+
var listConnections = async (options) => {
|
|
3271
|
+
const response = await fetch(
|
|
3272
|
+
`${normalizeApiBase(options.baseUrl)}/connection/user/${encodeURIComponent(options.userId)}`,
|
|
3273
|
+
{ method: "GET", headers: getCLIHeaders(options.authToken) }
|
|
3274
|
+
);
|
|
3275
|
+
return responseJson(response, "List connections");
|
|
3276
|
+
};
|
|
3277
|
+
var getConnection = async (options) => {
|
|
3278
|
+
const connections = await listConnections(options);
|
|
3279
|
+
return connections.find((connection) => String(connection.id) === options.connectionId) ?? null;
|
|
3280
|
+
};
|
|
3281
|
+
var compactErrorBody3 = async (response) => {
|
|
3282
|
+
const body = await response.text().catch(() => "");
|
|
3283
|
+
const trimmed = body.trim();
|
|
3284
|
+
return trimmed ? redactSensitiveText(trimmed).slice(0, 1e3) : void 0;
|
|
3285
|
+
};
|
|
3286
|
+
var fetchCredentialJson = async (options, path2, request = {}) => {
|
|
3287
|
+
const url = new URL(`${normalizeApiBase(options.baseUrl)}${path2}`);
|
|
3288
|
+
for (const [key, value] of Object.entries(request.query ?? {})) {
|
|
3289
|
+
if (value) {
|
|
3290
|
+
url.searchParams.set(key, value);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
const headers = request.method === "POST" ? withIdempotencyHeader(getCLIHeaders(options.authToken), request.idempotencyKey) : getCLIHeaders(options.authToken);
|
|
3294
|
+
const response = await fetch(url, {
|
|
3295
|
+
method: request.method ?? "GET",
|
|
3296
|
+
headers,
|
|
3297
|
+
...request.body ? { body: JSON.stringify(request.body) } : {}
|
|
3298
|
+
});
|
|
3299
|
+
if (!response.ok) {
|
|
3300
|
+
const body = await compactErrorBody3(response);
|
|
3301
|
+
throw new Error(
|
|
3302
|
+
`Credential request failed with status ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
return await response.json();
|
|
3306
|
+
};
|
|
3307
|
+
var getCredentialTemplates = (options) => fetchCredentialJson(options, "/credential-templates");
|
|
3308
|
+
var listCredentials = (options) => fetchCredentialJson(options, "/credentials", {
|
|
3309
|
+
query: { project_id: options.projectId }
|
|
3310
|
+
});
|
|
3311
|
+
var createCredential = (options) => fetchCredentialJson(options, "/credentials", {
|
|
3312
|
+
method: "POST",
|
|
3313
|
+
idempotencyKey: options.idempotencyKey,
|
|
3314
|
+
body: {
|
|
3315
|
+
template: options.template,
|
|
3316
|
+
name: options.name,
|
|
3317
|
+
...options.projectId ? { project_id: options.projectId } : {},
|
|
3318
|
+
...options.expires ? { expires: options.expires } : {},
|
|
3319
|
+
...options.capabilities?.length ? { capabilities: options.capabilities } : {}
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
var getCredential = (options) => fetchCredentialJson(
|
|
3323
|
+
options,
|
|
3324
|
+
`/credentials/${encodeURIComponent(options.credentialId)}`
|
|
3325
|
+
);
|
|
3326
|
+
var revokeCredential = (options) => fetchCredentialJson(
|
|
3327
|
+
options,
|
|
3328
|
+
`/credentials/${encodeURIComponent(options.credentialId)}/revoke`,
|
|
3329
|
+
{
|
|
3330
|
+
method: "POST",
|
|
3331
|
+
idempotencyKey: options.idempotencyKey,
|
|
3332
|
+
body: options.reason ? { reason: options.reason } : {}
|
|
3333
|
+
}
|
|
3334
|
+
);
|
|
3335
|
+
var getIdentity = (options) => fetchCredentialJson(options, "/identity");
|
|
3336
|
+
var getCapabilities = (options) => fetchCredentialJson(options, "/capabilities");
|
|
3337
|
+
var AgentProfileRequestError = class extends Error {
|
|
3338
|
+
status;
|
|
3339
|
+
statusText;
|
|
3340
|
+
body;
|
|
3341
|
+
code;
|
|
3342
|
+
requiresAuth;
|
|
3343
|
+
constructor(input) {
|
|
3344
|
+
super(
|
|
3345
|
+
`Agent Profile request failed with status ${input.status} ${input.statusText}${input.body.trim() ? `: ${input.body.trim().slice(0, 1e3)}` : ""}`
|
|
3346
|
+
);
|
|
3347
|
+
this.name = "AgentProfileRequestError";
|
|
3348
|
+
this.status = input.status;
|
|
3349
|
+
this.statusText = input.statusText;
|
|
3350
|
+
this.body = input.body;
|
|
3351
|
+
this.code = input.code;
|
|
3352
|
+
this.requiresAuth = input.requiresAuth;
|
|
3353
|
+
}
|
|
3354
|
+
};
|
|
3355
|
+
var parseErrorBody = (body) => {
|
|
3356
|
+
try {
|
|
3357
|
+
const parsed = JSON.parse(body);
|
|
3358
|
+
return {
|
|
3359
|
+
code: typeof parsed.code === "string" ? parsed.code : void 0,
|
|
3360
|
+
requiresAuth: typeof parsed.requiresAuth === "boolean" ? parsed.requiresAuth : void 0
|
|
3361
|
+
};
|
|
3362
|
+
} catch {
|
|
3363
|
+
return {};
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
var isAgentProfileAuthRequiredError = (error) => {
|
|
3367
|
+
if (!(error instanceof AgentProfileRequestError)) {
|
|
3368
|
+
return false;
|
|
3369
|
+
}
|
|
3370
|
+
return error.requiresAuth === true || error.code === "AUTH_REQUIRED_PUBLIC" || error.status === 401 || error.status === 403;
|
|
3371
|
+
};
|
|
3372
|
+
var isAgentProfileDiscoveryFallbackError = (error) => {
|
|
3373
|
+
if (isAgentProfileAuthRequiredError(error)) {
|
|
3374
|
+
return true;
|
|
3375
|
+
}
|
|
3376
|
+
return error instanceof AgentProfileRequestError && error.status === 404;
|
|
3377
|
+
};
|
|
3378
|
+
var fetchAgentProfileJson = async (options, path2) => {
|
|
3379
|
+
const response = await fetch(`${normalizeApiBase(options.baseUrl)}${path2}`, {
|
|
3380
|
+
headers: getCLIHeaders(options.authToken)
|
|
3381
|
+
});
|
|
3382
|
+
if (!response.ok) {
|
|
3383
|
+
const body = await response.text().catch(() => "");
|
|
3384
|
+
const parsed = parseErrorBody(body);
|
|
3385
|
+
throw new AgentProfileRequestError({
|
|
3386
|
+
status: response.status,
|
|
3387
|
+
statusText: response.statusText,
|
|
3388
|
+
body,
|
|
3389
|
+
code: parsed.code,
|
|
3390
|
+
requiresAuth: parsed.requiresAuth
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
return await response.json();
|
|
3394
|
+
};
|
|
3395
|
+
var listAgentProfiles = (options) => fetchAgentProfileJson(options, "/agent-profiles");
|
|
3396
|
+
var getAgentProfile = (options) => fetchAgentProfileJson(
|
|
3397
|
+
options,
|
|
3398
|
+
`/agent-profiles/${encodeURIComponent(options.profileId)}`
|
|
3399
|
+
);
|
|
3400
|
+
|
|
3401
|
+
export {
|
|
3402
|
+
normalizeReportEnvelope,
|
|
3403
|
+
normalizeReportList,
|
|
3404
|
+
SECRET_REDACTION,
|
|
3405
|
+
isSensitiveSecretKey,
|
|
3406
|
+
redactSensitiveText,
|
|
3407
|
+
redactSensitiveSecrets,
|
|
3408
|
+
BUNDLED_AGENT_PROFILES,
|
|
3409
|
+
getBundledAgentProfiles,
|
|
3410
|
+
getBundledAgentProfile,
|
|
3411
|
+
assertSecureBaseUrl,
|
|
3412
|
+
normalizeApiBase,
|
|
3413
|
+
clearAuthSession,
|
|
3414
|
+
getCLIHeaders,
|
|
3415
|
+
getProjects,
|
|
3416
|
+
getAccessibleProjects,
|
|
3417
|
+
ensurePlaygroundProject,
|
|
3418
|
+
ensureDefaultProject,
|
|
3419
|
+
loginWithDeviceCode,
|
|
3420
|
+
login,
|
|
3421
|
+
logout,
|
|
3422
|
+
getAuthStatus,
|
|
3423
|
+
extractEmailFromToken,
|
|
3424
|
+
isAuthLookupFailure,
|
|
3425
|
+
checkUserStatus,
|
|
3426
|
+
completeOnboarding,
|
|
3427
|
+
getAuthToken,
|
|
3428
|
+
getAuthHeader,
|
|
3429
|
+
streamChat,
|
|
3430
|
+
initialChatState,
|
|
3431
|
+
completeActiveAssistantMessage,
|
|
3432
|
+
reduceChunk,
|
|
3433
|
+
fetchReportResource,
|
|
3434
|
+
listReports,
|
|
3435
|
+
getReport,
|
|
3436
|
+
getCostReport,
|
|
3437
|
+
getWafReport,
|
|
3438
|
+
getReportDetail,
|
|
3439
|
+
getCostReportFull,
|
|
3440
|
+
getWafReportFull,
|
|
3441
|
+
getCostReportHistory,
|
|
3442
|
+
getWafReportHistory,
|
|
3443
|
+
runReports,
|
|
3444
|
+
getReportJobStatus,
|
|
3445
|
+
getBillingConfig,
|
|
3446
|
+
getBillingEntitlement,
|
|
3447
|
+
getSubscriptionStatus,
|
|
3448
|
+
getSubscriptionBillingInfo,
|
|
3449
|
+
getTopUpPacks,
|
|
3450
|
+
createTopUpCheckoutSession,
|
|
3451
|
+
getBillingNotifications,
|
|
3452
|
+
getBillingUsageSummary,
|
|
3453
|
+
getBillingUsageLedger,
|
|
3454
|
+
getCreditStatus,
|
|
3455
|
+
parseTemplateUrl,
|
|
3456
|
+
buildQuickProjectPayload,
|
|
3457
|
+
createQuickProject,
|
|
3458
|
+
listConnections,
|
|
3459
|
+
getConnection,
|
|
3460
|
+
getCredentialTemplates,
|
|
3461
|
+
listCredentials,
|
|
3462
|
+
createCredential,
|
|
3463
|
+
getCredential,
|
|
3464
|
+
revokeCredential,
|
|
3465
|
+
getIdentity,
|
|
3466
|
+
getCapabilities,
|
|
3467
|
+
AgentProfileRequestError,
|
|
3468
|
+
isAgentProfileAuthRequiredError,
|
|
3469
|
+
isAgentProfileDiscoveryFallbackError,
|
|
3470
|
+
listAgentProfiles,
|
|
3471
|
+
getAgentProfile
|
|
3472
|
+
};
|