@luckypiggy/zhihu-ai-cli 0.1.0-test.1
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/README.md +92 -0
- package/assets/codex-skills/zhihu-aiops/SKILL.md +39 -0
- package/assets/codex-skills/zhihu-aiops/agents/openai.yaml +4 -0
- package/assets/codex-skills/zhihu-aiops/references/commands.md +33 -0
- package/assets/codex-skills/zhihu-aiops/references/errors.md +14 -0
- package/assets/codex-skills/zhihu-aiops/references/safety.md +9 -0
- package/bin/zhihu.js +5 -0
- package/dist/auth-service.js +204 -0
- package/dist/browser.js +49 -0
- package/dist/cli.js +1092 -0
- package/dist/codex-skill.js +573 -0
- package/dist/config.js +350 -0
- package/dist/contracts.js +15 -0
- package/dist/credentials.js +198 -0
- package/dist/http.js +1875 -0
- package/dist/install.js +435 -0
- package/dist/logger.js +28 -0
- package/dist/output.js +48 -0
- package/dist/pkce.js +15 -0
- package/dist/profile-lock.js +126 -0
- package/dist/redaction.js +42 -0
- package/dist/token-manager.js +320 -0
- package/package.json +50 -0
package/dist/http.js
ADDED
|
@@ -0,0 +1,1875 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { request as httpRequest } from "node:http";
|
|
4
|
+
import { request as httpsRequest } from "node:https";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
import { CLI_VERSION, P1_CAPABILITY_SCOPES } from "./contracts.js";
|
|
7
|
+
const INTEGRATION_PATH = "/api/zhihu/integrations/v1";
|
|
8
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_REQUEST_BYTES = 128 * 1024;
|
|
10
|
+
const MAX_CA_BUNDLE_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
const CLI_USER_AGENT = `zhihu-ai-cli/${CLI_VERSION}`;
|
|
12
|
+
const INFO_CAPABILITIES = [
|
|
13
|
+
"info",
|
|
14
|
+
"capabilities",
|
|
15
|
+
"employees",
|
|
16
|
+
"assets",
|
|
17
|
+
"incidents",
|
|
18
|
+
"inspections",
|
|
19
|
+
"metrics",
|
|
20
|
+
"alarms",
|
|
21
|
+
];
|
|
22
|
+
function semver(value) {
|
|
23
|
+
if (typeof value !== "string" || !/^\d+\.\d+\.\d+$/.test(value))
|
|
24
|
+
return null;
|
|
25
|
+
const parts = value.split(".").map(Number);
|
|
26
|
+
if (parts.some((part) => !Number.isSafeInteger(part) || part < 0))
|
|
27
|
+
return null;
|
|
28
|
+
return parts;
|
|
29
|
+
}
|
|
30
|
+
function compareSemver(left, right) {
|
|
31
|
+
for (let index = 0; index < 3; index += 1) {
|
|
32
|
+
if (left[index] !== right[index])
|
|
33
|
+
return left[index] - right[index];
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
/** Validate the exact, unauthenticated integration discovery contract. */
|
|
38
|
+
export function validateIntegrationInfo(value, requireReady = false) {
|
|
39
|
+
const object = validateResponseObject(value, "Info");
|
|
40
|
+
const expectedKeys = new Set(["service", "protocol_version", "minimum_cli_version", "status", "capabilities", "auth"]);
|
|
41
|
+
if (Object.keys(object).length !== expectedKeys.size || Object.keys(object).some((key) => !expectedKeys.has(key))) {
|
|
42
|
+
throw new HttpError("PROTOCOL_ERROR", "Info response is invalid");
|
|
43
|
+
}
|
|
44
|
+
if (object.service !== "zhihu-pilot" || object.protocol_version !== "1.0") {
|
|
45
|
+
throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot does not support this CLI version");
|
|
46
|
+
}
|
|
47
|
+
const minimum = semver(object.minimum_cli_version);
|
|
48
|
+
const current = semver(CLI_VERSION);
|
|
49
|
+
if (!minimum || !current || compareSemver(minimum, current) > 0) {
|
|
50
|
+
throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot requires a newer CLI version");
|
|
51
|
+
}
|
|
52
|
+
if (object.status !== "ready" && object.status !== "configuration_required") {
|
|
53
|
+
throw new HttpError("PROTOCOL_ERROR", "Info response is invalid");
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(object.capabilities) || object.capabilities.length === 0) {
|
|
56
|
+
throw new HttpError("PROTOCOL_ERROR", "Info capabilities are invalid");
|
|
57
|
+
}
|
|
58
|
+
const capabilities = object.capabilities;
|
|
59
|
+
let previous = -1;
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
for (const capability of capabilities) {
|
|
62
|
+
if (typeof capability !== "string" || !INFO_CAPABILITIES.includes(capability)) {
|
|
63
|
+
throw new HttpError("PROTOCOL_ERROR", "Info capabilities are invalid");
|
|
64
|
+
}
|
|
65
|
+
const index = INFO_CAPABILITIES.indexOf(capability);
|
|
66
|
+
if (index <= previous || seen.has(capability))
|
|
67
|
+
throw new HttpError("PROTOCOL_ERROR", "Info capabilities are invalid");
|
|
68
|
+
seen.add(capability);
|
|
69
|
+
previous = index;
|
|
70
|
+
}
|
|
71
|
+
if (!seen.has("info"))
|
|
72
|
+
throw new HttpError("PROTOCOL_ERROR", "Info capabilities are invalid");
|
|
73
|
+
const auth = validateResponseObject(object.auth, "Info auth");
|
|
74
|
+
const authKeys = new Set(["mode", "configured", "pkce_methods"]);
|
|
75
|
+
if (Object.keys(auth).length !== authKeys.size || Object.keys(auth).some((key) => !authKeys.has(key))) {
|
|
76
|
+
throw new HttpError("PROTOCOL_ERROR", "Info auth is invalid");
|
|
77
|
+
}
|
|
78
|
+
if (auth.mode !== "pilot_oauth_bff" || typeof auth.configured !== "boolean"
|
|
79
|
+
|| !Array.isArray(auth.pkce_methods) || auth.pkce_methods.length !== 1 || auth.pkce_methods[0] !== "S256") {
|
|
80
|
+
throw new HttpError("PROTOCOL_ERROR", "Info auth is invalid");
|
|
81
|
+
}
|
|
82
|
+
const baseCapabilities = INFO_CAPABILITIES.slice(0, 4);
|
|
83
|
+
const baseMatch = capabilities.length >= baseCapabilities.length
|
|
84
|
+
&& baseCapabilities.every((capability, index) => capabilities[index] === capability);
|
|
85
|
+
const hasMetrics = seen.has("metrics");
|
|
86
|
+
const hasAlarms = seen.has("alarms");
|
|
87
|
+
const hasIncidents = seen.has("incidents");
|
|
88
|
+
const hasInspections = seen.has("inspections");
|
|
89
|
+
// The backend advertises a monotonic capability set. The base four are
|
|
90
|
+
// always present; metrics and alarms may be enabled independently. The
|
|
91
|
+
// incident/inspection pair is available only when both data sources exist,
|
|
92
|
+
// and the pair itself is all-or-nothing.
|
|
93
|
+
const optionalCombinationValid = baseMatch && (capabilities.length === 4
|
|
94
|
+
|| (capabilities.length === 5 && hasMetrics && !hasAlarms && !hasIncidents && !hasInspections)
|
|
95
|
+
|| (capabilities.length === 5 && hasAlarms && !hasMetrics && !hasIncidents && !hasInspections)
|
|
96
|
+
|| (capabilities.length === 8 && hasMetrics && hasAlarms && hasIncidents && hasInspections));
|
|
97
|
+
if (object.status === "ready" && (auth.configured !== true || !optionalCombinationValid)) {
|
|
98
|
+
throw new HttpError("PROTOCOL_ERROR", "Info readiness is inconsistent");
|
|
99
|
+
}
|
|
100
|
+
if (object.status === "configuration_required" && (auth.configured !== false
|
|
101
|
+
|| capabilities.length !== 1 || capabilities[0] !== "info")) {
|
|
102
|
+
throw new HttpError("PROTOCOL_ERROR", "Info readiness is inconsistent");
|
|
103
|
+
}
|
|
104
|
+
if (requireReady && (object.status !== "ready" || auth.configured !== true || !optionalCombinationValid)) {
|
|
105
|
+
if (object.status === "configuration_required" && auth.configured === false) {
|
|
106
|
+
throw new HttpError("SERVICE_CONFIGURATION_REQUIRED", "Pilot is not configured for CLI OAuth yet. Ask a Pilot administrator to complete the server-side CLI OAuth configuration before installing.");
|
|
107
|
+
}
|
|
108
|
+
throw new HttpError("VERSION_INCOMPATIBLE", "The configured Pilot is not ready for CLI installation");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
service: object.service,
|
|
112
|
+
protocol_version: object.protocol_version,
|
|
113
|
+
minimum_cli_version: object.minimum_cli_version,
|
|
114
|
+
status: object.status,
|
|
115
|
+
capabilities: capabilities,
|
|
116
|
+
auth: {
|
|
117
|
+
mode: auth.mode,
|
|
118
|
+
configured: auth.configured,
|
|
119
|
+
pkce_methods: ["S256"],
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export class HttpError extends Error {
|
|
124
|
+
code;
|
|
125
|
+
status;
|
|
126
|
+
retryable;
|
|
127
|
+
constructor(code, message, options = {}) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "HttpError";
|
|
130
|
+
this.code = code;
|
|
131
|
+
this.status = options.status;
|
|
132
|
+
this.retryable = options.retryable ?? false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Retryability is a client-side contract derived from the stable error code,
|
|
136
|
+
// never from a provider/server message or a caller-controlled envelope field.
|
|
137
|
+
// Keep this list aligned with the public integration error contract.
|
|
138
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
139
|
+
"RATE_LIMITED",
|
|
140
|
+
"RATE_LIMITER_UNAVAILABLE",
|
|
141
|
+
"CONVERSATION_BUSY",
|
|
142
|
+
"CONVERSATION_CAPACITY_REACHED",
|
|
143
|
+
"DATA_SOURCE_UNAVAILABLE",
|
|
144
|
+
"TIMEOUT",
|
|
145
|
+
]);
|
|
146
|
+
const NON_RETRYABLE_ERROR_CODES = new Set([
|
|
147
|
+
"AUTH_RELOGIN_REQUIRED",
|
|
148
|
+
"CAPABILITY_SCOPE_REQUIRED",
|
|
149
|
+
"CAPABILITY_NOT_AVAILABLE",
|
|
150
|
+
"FORBIDDEN",
|
|
151
|
+
"INVALID_ARGUMENT",
|
|
152
|
+
"EMPLOYEE_NOT_FOUND",
|
|
153
|
+
"EMPLOYEE_NOT_CHATABLE",
|
|
154
|
+
"CONVERSATION_NOT_FOUND",
|
|
155
|
+
"MODEL_NOT_FOUND",
|
|
156
|
+
"ASSET_NOT_FOUND",
|
|
157
|
+
"ALARM_NOT_FOUND",
|
|
158
|
+
"INCIDENT_ASSET_IDENTITY_UNAVAILABLE",
|
|
159
|
+
"UPSTREAM_PROTOCOL_ERROR",
|
|
160
|
+
"UPSTREAM_RESPONSE_TOO_LARGE",
|
|
161
|
+
"USER_CANCELLED",
|
|
162
|
+
"PROTOCOL_ERROR",
|
|
163
|
+
]);
|
|
164
|
+
function retryableForCode(code, fallback) {
|
|
165
|
+
if (RETRYABLE_ERROR_CODES.has(code))
|
|
166
|
+
return true;
|
|
167
|
+
if (NON_RETRYABLE_ERROR_CODES.has(code))
|
|
168
|
+
return false;
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
function stableCode(status) {
|
|
172
|
+
if (status === 401)
|
|
173
|
+
return { code: "AUTH_RELOGIN_REQUIRED", retryable: false };
|
|
174
|
+
if (status === 403)
|
|
175
|
+
return { code: "FORBIDDEN", retryable: false };
|
|
176
|
+
if (status === 429)
|
|
177
|
+
return { code: "RATE_LIMITED", retryable: true };
|
|
178
|
+
if (status >= 500)
|
|
179
|
+
return { code: "SERVER_ERROR", retryable: true };
|
|
180
|
+
return { code: "PROTOCOL_ERROR", retryable: false };
|
|
181
|
+
}
|
|
182
|
+
function responseError(status, body) {
|
|
183
|
+
const fallback = stableCode(status);
|
|
184
|
+
const messages = {
|
|
185
|
+
AUTH_RELOGIN_REQUIRED: "Your Zhihu session has expired. Run `zhihu auth login`.",
|
|
186
|
+
CAPABILITY_NOT_AVAILABLE: "This capability is not available on the configured Pilot.",
|
|
187
|
+
CAPABILITY_SCOPE_REQUIRED: "This CLI session is not allowed to use this capability.",
|
|
188
|
+
FORBIDDEN: "The server denied this operation.",
|
|
189
|
+
RATE_LIMITED: "The server is rate limiting requests; try again later.",
|
|
190
|
+
RATE_LIMITER_UNAVAILABLE: "The capability service is temporarily unavailable; try again later.",
|
|
191
|
+
SERVER_ERROR: "The server could not complete the request.",
|
|
192
|
+
PROTOCOL_ERROR: "The server returned an invalid response.",
|
|
193
|
+
EMPLOYEE_NOT_FOUND: "The requested digital employee was not found.",
|
|
194
|
+
EMPLOYEE_NOT_CHATABLE: "This digital employee is not available for read-only chat.",
|
|
195
|
+
ASSET_NOT_FOUND: "The requested asset was not found.",
|
|
196
|
+
ALARM_NOT_FOUND: "The requested alarm was not found.",
|
|
197
|
+
INCIDENT_ASSET_IDENTITY_UNAVAILABLE: "The alarm and asset identity could not be verified.",
|
|
198
|
+
CONVERSATION_NOT_FOUND: "The conversation is unavailable for this session.",
|
|
199
|
+
CONVERSATION_BUSY: "This conversation already has an active request.",
|
|
200
|
+
CONVERSATION_CAPACITY_REACHED: "Too many active conversations; try again later.",
|
|
201
|
+
DATA_SOURCE_UNAVAILABLE: "The requested data source is unavailable.",
|
|
202
|
+
INVALID_ARGUMENT: "The request arguments are invalid.",
|
|
203
|
+
MODEL_NOT_FOUND: "The requested asset model was not found.",
|
|
204
|
+
UPSTREAM_PROTOCOL_ERROR: "The data source returned an invalid response.",
|
|
205
|
+
UPSTREAM_RESPONSE_TOO_LARGE: "The data source returned too much data.",
|
|
206
|
+
METRIC_DATA_NOT_FOUND: "No metric data was found for the requested asset and window.",
|
|
207
|
+
ASSET_METRIC_IDENTITY_UNAVAILABLE: "The asset has no monitorable IP address for metric lookup.",
|
|
208
|
+
TIMEOUT: "The request timed out.",
|
|
209
|
+
USER_CANCELLED: "The request was cancelled.",
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(body);
|
|
213
|
+
const error = parsed.error && typeof parsed.error === "object" ? parsed.error : undefined;
|
|
214
|
+
const detailCode = typeof parsed.detail === "string" && /^[A-Z][A-Z0-9_]{2,63}$/.test(parsed.detail)
|
|
215
|
+
? parsed.detail
|
|
216
|
+
: undefined;
|
|
217
|
+
const code = typeof error?.code === "string" && /^[A-Z][A-Z0-9_]{2,63}$/.test(error.code)
|
|
218
|
+
? error.code
|
|
219
|
+
: typeof parsed.error_code === "string" && /^[A-Z][A-Z0-9_]{2,63}$/.test(parsed.error_code)
|
|
220
|
+
? parsed.error_code
|
|
221
|
+
: detailCode ?? fallback.code;
|
|
222
|
+
// Never reflect provider/server detail: it may contain a token, cookie,
|
|
223
|
+
// stack trace or customer topology. The stable code is enough for CLI UX.
|
|
224
|
+
const message = messages[code] ?? "The server rejected the request.";
|
|
225
|
+
const retryable = retryableForCode(code, fallback.retryable);
|
|
226
|
+
return new HttpError(code, message, { status, retryable });
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return new HttpError(fallback.code, "Request failed", { status, retryable: fallback.retryable });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function validateResponseObject(value, label) {
|
|
233
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
234
|
+
throw new HttpError("PROTOCOL_ERROR", `${label} response is invalid`);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
function validateCredentialField(value, field) {
|
|
239
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) {
|
|
240
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
function validateStringList(value, field) {
|
|
245
|
+
if (!Array.isArray(value) || value.length > P1_CAPABILITY_SCOPES.length || value.some((item) => (typeof item !== "string" || item.length === 0 || item.length > 128))) {
|
|
246
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
247
|
+
}
|
|
248
|
+
const items = [...value];
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
let previousIndex = -1;
|
|
251
|
+
for (const item of items) {
|
|
252
|
+
const index = P1_CAPABILITY_SCOPES.indexOf(item);
|
|
253
|
+
if (index < 0 || seen.has(item) || index <= previousIndex) {
|
|
254
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
255
|
+
}
|
|
256
|
+
seen.add(item);
|
|
257
|
+
previousIndex = index;
|
|
258
|
+
}
|
|
259
|
+
return items;
|
|
260
|
+
}
|
|
261
|
+
function validateIdentityId(value, field, allowEmpty = false) {
|
|
262
|
+
if (typeof value !== "string") {
|
|
263
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
264
|
+
}
|
|
265
|
+
if (allowEmpty && value === "")
|
|
266
|
+
return value;
|
|
267
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
268
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
function validateGeneratedAt(value) {
|
|
273
|
+
if (typeof value !== "string"
|
|
274
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/.test(value)
|
|
275
|
+
|| !Number.isFinite(Date.parse(value))) {
|
|
276
|
+
throw new HttpError("PROTOCOL_ERROR", "Capabilities metadata is invalid");
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function validateEmployeeText(value, field) {
|
|
281
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) {
|
|
282
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
function validateEmployeeList(value) {
|
|
287
|
+
if (!Array.isArray(value) || value.length > 64) {
|
|
288
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees response is invalid");
|
|
289
|
+
}
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
return value.map((raw, index) => {
|
|
292
|
+
const item = validateResponseObject(raw, `Employee ${index}`);
|
|
293
|
+
const expected = new Set([
|
|
294
|
+
"id",
|
|
295
|
+
"name",
|
|
296
|
+
"name_en",
|
|
297
|
+
"description",
|
|
298
|
+
"description_en",
|
|
299
|
+
"readonly",
|
|
300
|
+
"chat_available",
|
|
301
|
+
]);
|
|
302
|
+
if (Object.keys(item).some((key) => !expected.has(key)) || expected.size !== Object.keys(item).length) {
|
|
303
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees response is invalid");
|
|
304
|
+
}
|
|
305
|
+
const id = validateIdentityId(item.id, "employee id");
|
|
306
|
+
if (seen.has(id))
|
|
307
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees response is invalid");
|
|
308
|
+
seen.add(id);
|
|
309
|
+
if (typeof item.readonly !== "boolean" || typeof item.chat_available !== "boolean") {
|
|
310
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees response is invalid");
|
|
311
|
+
}
|
|
312
|
+
if (item.chat_available && !item.readonly) {
|
|
313
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees response is invalid");
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
id,
|
|
317
|
+
name: validateEmployeeText(item.name, "employee name"),
|
|
318
|
+
name_en: validateEmployeeText(item.name_en, "employee name_en"),
|
|
319
|
+
description: validateEmployeeText(item.description, "employee description"),
|
|
320
|
+
description_en: validateEmployeeText(item.description_en, "employee description_en"),
|
|
321
|
+
readonly: item.readonly,
|
|
322
|
+
chat_available: item.chat_available,
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const ASSET_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
327
|
+
const ASSET_IP_PATTERN = /^[0-9a-fA-F:.]{1,128}$/;
|
|
328
|
+
function validateNullableAssetText(value, field) {
|
|
329
|
+
if (value === null)
|
|
330
|
+
return null;
|
|
331
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024 || /[\u0000-\u001f]/.test(value)) {
|
|
332
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
333
|
+
}
|
|
334
|
+
if (/^(?:https?|tcp|udp):\/\//i.test(value)) {
|
|
335
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
function validateNullableAssetId(value, field) {
|
|
340
|
+
if (value === null)
|
|
341
|
+
return null;
|
|
342
|
+
if (typeof value !== "string" || !ASSET_ID_PATTERN.test(value)) {
|
|
343
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
344
|
+
}
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
function validateAssetId(value) {
|
|
348
|
+
if (typeof value !== "string" || !ASSET_ID_PATTERN.test(value)) {
|
|
349
|
+
throw new HttpError("PROTOCOL_ERROR", "asset id response is invalid");
|
|
350
|
+
}
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
function validateAssetRequestId(value) {
|
|
354
|
+
if (typeof value !== "string" || !ASSET_ID_PATTERN.test(value)) {
|
|
355
|
+
throw new HttpError("PROTOCOL_ERROR", "asset id is invalid");
|
|
356
|
+
}
|
|
357
|
+
if (/^\d+$/.test(value)) {
|
|
358
|
+
const normalized = value.replace(/^0+/, "") || "0";
|
|
359
|
+
let numeric;
|
|
360
|
+
try {
|
|
361
|
+
numeric = BigInt(normalized);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
throw new HttpError("PROTOCOL_ERROR", "asset id is invalid");
|
|
365
|
+
}
|
|
366
|
+
if (numeric < 1n || numeric > 9223372036854775807n) {
|
|
367
|
+
throw new HttpError("PROTOCOL_ERROR", "asset id is invalid");
|
|
368
|
+
}
|
|
369
|
+
return numeric.toString();
|
|
370
|
+
}
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
function validateNullableAssetIp(value) {
|
|
374
|
+
if (value === null)
|
|
375
|
+
return null;
|
|
376
|
+
if (typeof value !== "string" || !ASSET_IP_PATTERN.test(value)) {
|
|
377
|
+
throw new HttpError("PROTOCOL_ERROR", "asset ip response is invalid");
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function validateMetricAsset(value) {
|
|
382
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 128 || /[\u0000-\u001f]/.test(value)) {
|
|
383
|
+
throw new HttpError("PROTOCOL_ERROR", "metric asset is invalid");
|
|
384
|
+
}
|
|
385
|
+
if (isIP(value) > 0)
|
|
386
|
+
return value;
|
|
387
|
+
return validateAssetRequestId(value);
|
|
388
|
+
}
|
|
389
|
+
function canonicalIp(value) {
|
|
390
|
+
if (typeof value !== "string")
|
|
391
|
+
return null;
|
|
392
|
+
const kind = isIP(value);
|
|
393
|
+
if (kind === 4)
|
|
394
|
+
return value;
|
|
395
|
+
if (kind !== 6 || value.includes("%"))
|
|
396
|
+
return null;
|
|
397
|
+
let input = value.toLowerCase();
|
|
398
|
+
if (input.includes(".")) {
|
|
399
|
+
const separator = input.lastIndexOf(":");
|
|
400
|
+
if (separator < 0)
|
|
401
|
+
return null;
|
|
402
|
+
const octets = input.slice(separator + 1).split(".");
|
|
403
|
+
if (octets.length !== 4 || octets.some((octet) => !/^\d+$/.test(octet) || Number(octet) > 255))
|
|
404
|
+
return null;
|
|
405
|
+
const high = ((Number(octets[0]) << 8) | Number(octets[1])).toString(16);
|
|
406
|
+
const low = ((Number(octets[2]) << 8) | Number(octets[3])).toString(16);
|
|
407
|
+
input = `${input.slice(0, separator)}:${high}:${low}`;
|
|
408
|
+
}
|
|
409
|
+
const sections = input.split("::");
|
|
410
|
+
if (sections.length > 2)
|
|
411
|
+
return null;
|
|
412
|
+
const left = sections[0] ? sections[0].split(":") : [];
|
|
413
|
+
const right = sections.length === 2 && sections[1] ? sections[1].split(":") : [];
|
|
414
|
+
const validSection = (section) => /^[0-9a-f]{1,4}$/.test(section);
|
|
415
|
+
if (left.some((section) => !validSection(section)) || right.some((section) => !validSection(section)))
|
|
416
|
+
return null;
|
|
417
|
+
const missing = 8 - left.length - right.length;
|
|
418
|
+
if ((sections.length === 1 && missing !== 0) || (sections.length === 2 && missing < 1))
|
|
419
|
+
return null;
|
|
420
|
+
const full = [...left, ...Array.from({ length: Math.max(0, missing) }, () => "0"), ...right]
|
|
421
|
+
.map((section) => section.replace(/^0+(?=[0-9a-f])/, ""));
|
|
422
|
+
if (full.length !== 8)
|
|
423
|
+
return null;
|
|
424
|
+
let bestStart = -1;
|
|
425
|
+
let bestLength = 0;
|
|
426
|
+
for (let index = 0; index < full.length;) {
|
|
427
|
+
if (full[index] !== "0") {
|
|
428
|
+
index += 1;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
let end = index;
|
|
432
|
+
while (end < full.length && full[end] === "0")
|
|
433
|
+
end += 1;
|
|
434
|
+
if (end - index > bestLength && end - index >= 2) {
|
|
435
|
+
bestStart = index;
|
|
436
|
+
bestLength = end - index;
|
|
437
|
+
}
|
|
438
|
+
index = end;
|
|
439
|
+
}
|
|
440
|
+
if (bestStart < 0)
|
|
441
|
+
return full.join(":");
|
|
442
|
+
const before = full.slice(0, bestStart).join(":");
|
|
443
|
+
const after = full.slice(bestStart + bestLength).join(":");
|
|
444
|
+
return `${before}::${after}`;
|
|
445
|
+
}
|
|
446
|
+
const METRIC_NAMES = new Set(["online", "cpu", "memory", "load", "disk", "io", "snmp"]);
|
|
447
|
+
const METRIC_LABELS = new Set(["host_ip", "instance", "monitorIp", "ip", "agent_hostname"]);
|
|
448
|
+
function validateMetricSeries(raw, index, maxPoints) {
|
|
449
|
+
const item = validateResponseObject(raw, `Metric series ${index}`);
|
|
450
|
+
const expected = new Set(["dimension", "unit", "current", "min", "max", "average", "sample_count", "points"]);
|
|
451
|
+
if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
|
|
452
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics response is invalid");
|
|
453
|
+
}
|
|
454
|
+
const text = (value, field, max = 256, allowEmpty = false) => {
|
|
455
|
+
if (typeof value !== "string" || (!allowEmpty && value.length < 1) || value.length > max || /[\u0000-\u001f]/.test(value)) {
|
|
456
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
457
|
+
}
|
|
458
|
+
return value;
|
|
459
|
+
};
|
|
460
|
+
const number = (value, field) => {
|
|
461
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
462
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
463
|
+
return value;
|
|
464
|
+
};
|
|
465
|
+
if (!Number.isSafeInteger(item.sample_count) || item.sample_count < 1 || item.sample_count > maxPoints) {
|
|
466
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric sample count is invalid");
|
|
467
|
+
}
|
|
468
|
+
if (!Array.isArray(item.points) || item.points.length !== item.sample_count || item.points.length > maxPoints) {
|
|
469
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric points are invalid");
|
|
470
|
+
}
|
|
471
|
+
const points = item.points.map((point, pointIndex) => {
|
|
472
|
+
const record = validateResponseObject(point, `Metric point ${pointIndex}`);
|
|
473
|
+
if (Object.keys(record).length !== 2 || !Object.hasOwn(record, "timestamp") || !Object.hasOwn(record, "value")) {
|
|
474
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric point response is invalid");
|
|
475
|
+
}
|
|
476
|
+
const timestamp = number(record.timestamp, "Metric timestamp");
|
|
477
|
+
if (timestamp < 0)
|
|
478
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric timestamp is invalid");
|
|
479
|
+
return { timestamp, value: number(record.value, "Metric value") };
|
|
480
|
+
});
|
|
481
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
482
|
+
if (points[index].timestamp <= points[index - 1].timestamp) {
|
|
483
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric timestamps are not strictly increasing");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const current = number(item.current, "Metric current");
|
|
487
|
+
const min = number(item.min, "Metric min");
|
|
488
|
+
const max = number(item.max, "Metric max");
|
|
489
|
+
const average = number(item.average, "Metric average");
|
|
490
|
+
if (min > max || average < min || average > max || current < min || current > max || current !== points[points.length - 1].value) {
|
|
491
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric summary is inconsistent");
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
dimension: text(item.dimension, "Metric dimension"),
|
|
495
|
+
unit: text(item.unit, "Metric unit", 32, true),
|
|
496
|
+
current,
|
|
497
|
+
min,
|
|
498
|
+
max,
|
|
499
|
+
average,
|
|
500
|
+
sample_count: item.sample_count,
|
|
501
|
+
points,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function validateAssetRecord(raw, index, seen = new Set()) {
|
|
505
|
+
const item = validateResponseObject(raw, `Asset ${index}`);
|
|
506
|
+
const expected = new Set([
|
|
507
|
+
"id", "name", "ip", "model_id", "model_name", "model_code", "managed", "monitorable",
|
|
508
|
+
]);
|
|
509
|
+
if (Object.keys(item).some((key) => !expected.has(key)) || Object.keys(item).length !== expected.size) {
|
|
510
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets response is invalid");
|
|
511
|
+
}
|
|
512
|
+
const id = validateAssetId(item.id);
|
|
513
|
+
if (seen.has(id))
|
|
514
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets response is invalid");
|
|
515
|
+
seen.add(id);
|
|
516
|
+
if ((item.managed !== null && typeof item.managed !== "boolean")
|
|
517
|
+
|| (item.monitorable !== null && typeof item.monitorable !== "boolean")) {
|
|
518
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets response is invalid");
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
id,
|
|
522
|
+
name: validateNullableAssetText(item.name, "asset name"),
|
|
523
|
+
ip: validateNullableAssetIp(item.ip),
|
|
524
|
+
model_id: validateNullableAssetId(item.model_id, "asset model id"),
|
|
525
|
+
model_name: validateNullableAssetText(item.model_name, "asset model name"),
|
|
526
|
+
model_code: validateNullableAssetText(item.model_code, "asset model code"),
|
|
527
|
+
managed: item.managed,
|
|
528
|
+
monitorable: item.monitorable,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function validateAssetItems(value, pageSize) {
|
|
532
|
+
if (!Array.isArray(value) || value.length > pageSize) {
|
|
533
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets response is invalid");
|
|
534
|
+
}
|
|
535
|
+
const seen = new Set();
|
|
536
|
+
return value.map((raw, index) => validateAssetRecord(raw, index, seen));
|
|
537
|
+
}
|
|
538
|
+
const ALARM_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
539
|
+
const ALARM_QUERY_STATUSES = new Set(["all", "firing", "resolved"]);
|
|
540
|
+
const ALARM_STATUSES = new Set(["firing", "resolved", "unknown"]);
|
|
541
|
+
const ALARM_SEVERITIES = new Set(["early_warning", "normal", "severe", "critical", "unknown"]);
|
|
542
|
+
const ALARM_SEVERITY_BY_CODE = new Map([
|
|
543
|
+
[1, "early_warning"],
|
|
544
|
+
[2, "normal"],
|
|
545
|
+
[3, "severe"],
|
|
546
|
+
[4, "critical"],
|
|
547
|
+
]);
|
|
548
|
+
function validateAlarmRequestId(value) {
|
|
549
|
+
if (typeof value !== "string" || !ALARM_ID_PATTERN.test(value)) {
|
|
550
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm id is invalid");
|
|
551
|
+
}
|
|
552
|
+
if (!/^\d+$/.test(value))
|
|
553
|
+
return value;
|
|
554
|
+
const normalized = value.replace(/^0+/, "") || "0";
|
|
555
|
+
let numeric;
|
|
556
|
+
try {
|
|
557
|
+
numeric = BigInt(normalized);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm id is invalid");
|
|
561
|
+
}
|
|
562
|
+
if (numeric < 1n || numeric > 9223372036854775807n) {
|
|
563
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm id is invalid");
|
|
564
|
+
}
|
|
565
|
+
return numeric.toString();
|
|
566
|
+
}
|
|
567
|
+
function validateAlarmNullableText(value, field, maxLength = 1024) {
|
|
568
|
+
if (value === null)
|
|
569
|
+
return null;
|
|
570
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength || /[\u0000-\u001f]/.test(value)) {
|
|
571
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
572
|
+
}
|
|
573
|
+
if (/^(?:https?|tcp|udp):\/\//i.test(value))
|
|
574
|
+
return null;
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
function validateAlarmIp(value) {
|
|
578
|
+
if (value === null)
|
|
579
|
+
return null;
|
|
580
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128) {
|
|
581
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm ip response is invalid");
|
|
582
|
+
}
|
|
583
|
+
const canonical = canonicalIp(value);
|
|
584
|
+
if (canonical === null)
|
|
585
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm ip response is invalid");
|
|
586
|
+
return canonical;
|
|
587
|
+
}
|
|
588
|
+
function validateAlarmDuration(value) {
|
|
589
|
+
if (value === null)
|
|
590
|
+
return null;
|
|
591
|
+
if (typeof value === "number") {
|
|
592
|
+
if (!Number.isFinite(value) || value < 0 || value > 365 * 24 * 60 * 60) {
|
|
593
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm duration response is invalid");
|
|
594
|
+
}
|
|
595
|
+
return value;
|
|
596
|
+
}
|
|
597
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128 || /[\u0000-\u001f]/.test(value)) {
|
|
598
|
+
throw new HttpError("PROTOCOL_ERROR", "alarm duration response is invalid");
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function validateAlarmContent(value, field) {
|
|
603
|
+
if (value === null)
|
|
604
|
+
return null;
|
|
605
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 16 * 1024) {
|
|
606
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
607
|
+
}
|
|
608
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value)) {
|
|
609
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} response is invalid`);
|
|
610
|
+
}
|
|
611
|
+
if (/(?:https?:\/\/|tcp:\/\/|udp:\/\/|authorization|bearer\s|token\s*=|password\s*=|secret\s*=|stack\s*trace|traceback|\/(?:api|var|data|etc)\/)/i.test(value)) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
return value;
|
|
615
|
+
}
|
|
616
|
+
function validateAlarmRecord(raw, index, detail, seen) {
|
|
617
|
+
const item = validateResponseObject(raw, `Alarm ${index}`);
|
|
618
|
+
const expected = new Set([
|
|
619
|
+
"id", "title", "status", "severity", "severity_code", "asset_id", "asset_name", "asset_ip",
|
|
620
|
+
"model_name", "started_at", "duration", "count", "source",
|
|
621
|
+
...(detail ? ["content", "description"] : []),
|
|
622
|
+
]);
|
|
623
|
+
if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
|
|
624
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms response is invalid");
|
|
625
|
+
}
|
|
626
|
+
const id = validateAlarmRequestId(item.id);
|
|
627
|
+
if (seen?.has(id))
|
|
628
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms response is invalid");
|
|
629
|
+
seen?.add(id);
|
|
630
|
+
if (typeof item.status !== "string" || !ALARM_STATUSES.has(item.status)) {
|
|
631
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm status response is invalid");
|
|
632
|
+
}
|
|
633
|
+
if (typeof item.severity !== "string" || !ALARM_SEVERITIES.has(item.severity)) {
|
|
634
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm severity response is invalid");
|
|
635
|
+
}
|
|
636
|
+
const severityCode = item.severity_code;
|
|
637
|
+
if (severityCode !== null && (typeof severityCode !== "number" || !Number.isSafeInteger(severityCode) || severityCode < 0 || severityCode > 100)) {
|
|
638
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm severity code response is invalid");
|
|
639
|
+
}
|
|
640
|
+
const expectedSeverity = severityCode === null ? "unknown" : (ALARM_SEVERITY_BY_CODE.get(severityCode) ?? "unknown");
|
|
641
|
+
if (item.severity !== expectedSeverity) {
|
|
642
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm severity response is inconsistent");
|
|
643
|
+
}
|
|
644
|
+
const count = item.count;
|
|
645
|
+
if (count !== null && (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > 1_000_000_000)) {
|
|
646
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm count response is invalid");
|
|
647
|
+
}
|
|
648
|
+
const output = {
|
|
649
|
+
id,
|
|
650
|
+
title: validateAlarmNullableText(item.title, "alarm title"),
|
|
651
|
+
status: item.status,
|
|
652
|
+
severity: item.severity,
|
|
653
|
+
severity_code: severityCode,
|
|
654
|
+
asset_id: item.asset_id === null ? null : validateAlarmRequestId(item.asset_id),
|
|
655
|
+
asset_name: validateAlarmNullableText(item.asset_name, "alarm asset name"),
|
|
656
|
+
asset_ip: validateAlarmIp(item.asset_ip),
|
|
657
|
+
model_name: validateAlarmNullableText(item.model_name, "alarm model name"),
|
|
658
|
+
started_at: validateAlarmNullableText(item.started_at, "alarm start time", 128),
|
|
659
|
+
duration: validateAlarmDuration(item.duration),
|
|
660
|
+
count: count,
|
|
661
|
+
source: validateAlarmNullableText(item.source, "alarm source", 256),
|
|
662
|
+
};
|
|
663
|
+
if (!detail)
|
|
664
|
+
return output;
|
|
665
|
+
return {
|
|
666
|
+
...output,
|
|
667
|
+
content: validateAlarmContent(item.content, "alarm content"),
|
|
668
|
+
description: validateAlarmContent(item.description, "alarm description"),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const INCIDENT_CATEGORIES = new Set(["cpu", "memory", "load", "disk", "io", "online", "unknown"]);
|
|
672
|
+
const INCIDENT_CONCLUSIONS = new Set(["active_anomaly", "recovered", "not_reproduced", "insufficient_evidence", "unsupported_alarm"]);
|
|
673
|
+
const INCIDENT_CONFIDENCES = new Set(["high", "medium", "low"]);
|
|
674
|
+
const INCIDENT_ASSOCIATIONS = new Set(["exact_asset_id", "exact_alarm_ip", "unavailable"]);
|
|
675
|
+
const INCIDENT_WINDOWS = new Set(["event", "recent"]);
|
|
676
|
+
const INCIDENT_METRICS = new Set(["cpu", "memory", "load", "disk", "io", "online"]);
|
|
677
|
+
const INCIDENT_FINDINGS = new Set([
|
|
678
|
+
"ALARM_CATEGORY_UNKNOWN",
|
|
679
|
+
"TARGET_UNAVAILABLE",
|
|
680
|
+
"ASSET_IP_UNAVAILABLE",
|
|
681
|
+
"EVENT_TIME_UNAVAILABLE",
|
|
682
|
+
"RECENT_METRIC_ANOMALY",
|
|
683
|
+
"HISTORICAL_METRIC_ANOMALY",
|
|
684
|
+
"NO_RECENT_DATA",
|
|
685
|
+
"NO_EVENT_DATA",
|
|
686
|
+
"ALARM_RESOLVED",
|
|
687
|
+
]);
|
|
688
|
+
const INCIDENT_ACTIONS = new Set([
|
|
689
|
+
"VERIFY_ALARM_RULE",
|
|
690
|
+
"REVIEW_ASSET_MONITORING",
|
|
691
|
+
"RECHECK_METRICS",
|
|
692
|
+
"CONTINUE_OBSERVATION",
|
|
693
|
+
"NO_ACTION",
|
|
694
|
+
]);
|
|
695
|
+
function validateIncidentTarget(value, alarm) {
|
|
696
|
+
const item = validateResponseObject(value, "Incident target");
|
|
697
|
+
const expected = new Set(["association", "asset_id", "name", "ip"]);
|
|
698
|
+
if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
|
|
699
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident target is invalid");
|
|
700
|
+
}
|
|
701
|
+
if (typeof item.association !== "string" || !INCIDENT_ASSOCIATIONS.has(item.association)) {
|
|
702
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident target association is invalid");
|
|
703
|
+
}
|
|
704
|
+
const association = item.association;
|
|
705
|
+
const assetId = item.asset_id === null ? null : validateAlarmRequestId(item.asset_id);
|
|
706
|
+
const name = item.name === null ? null : validateAlarmNullableText(item.name, "incident target name");
|
|
707
|
+
const ip = item.ip === null ? null : validateAlarmIp(item.ip);
|
|
708
|
+
if (association === "exact_asset_id") {
|
|
709
|
+
if (assetId === null || alarm.asset_id !== assetId)
|
|
710
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident target identity is invalid");
|
|
711
|
+
if (ip !== null && alarm.asset_ip !== null && ip !== alarm.asset_ip)
|
|
712
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident target identity is invalid");
|
|
713
|
+
}
|
|
714
|
+
else if (association === "exact_alarm_ip") {
|
|
715
|
+
if (assetId !== null || ip === null || alarm.asset_id !== null || alarm.asset_ip !== ip)
|
|
716
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident target identity is invalid");
|
|
717
|
+
}
|
|
718
|
+
else if (assetId !== null || name !== null || ip !== null || alarm.asset_id !== null || alarm.asset_ip !== null) {
|
|
719
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident target identity is invalid");
|
|
720
|
+
}
|
|
721
|
+
return { association, asset_id: assetId, name, ip };
|
|
722
|
+
}
|
|
723
|
+
function validateIncidentEvidence(value, index) {
|
|
724
|
+
const item = validateResponseObject(value, `Incident evidence ${index}`);
|
|
725
|
+
const expected = new Set(["window", "metric", "status", "series", "truncated"]);
|
|
726
|
+
if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
|
|
727
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence is invalid");
|
|
728
|
+
}
|
|
729
|
+
if (typeof item.window !== "string" || !INCIDENT_WINDOWS.has(item.window)) {
|
|
730
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence window is invalid");
|
|
731
|
+
}
|
|
732
|
+
if (typeof item.metric !== "string" || !INCIDENT_METRICS.has(item.metric)) {
|
|
733
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence metric is invalid");
|
|
734
|
+
}
|
|
735
|
+
if (typeof item.status !== "string" || !["available", "no_data"].includes(item.status)) {
|
|
736
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence status is invalid");
|
|
737
|
+
}
|
|
738
|
+
if (typeof item.truncated !== "boolean" || !Array.isArray(item.series) || item.series.length > 20) {
|
|
739
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence is invalid");
|
|
740
|
+
}
|
|
741
|
+
let pointCount = 0;
|
|
742
|
+
const series = item.series.map((raw, seriesIndex) => {
|
|
743
|
+
const parsed = validateMetricSeries(raw, seriesIndex, 60);
|
|
744
|
+
pointCount += parsed.points.length;
|
|
745
|
+
if (pointCount > 60)
|
|
746
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence points are invalid");
|
|
747
|
+
return parsed;
|
|
748
|
+
});
|
|
749
|
+
if (item.status === "available" && series.length === 0)
|
|
750
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence is invalid");
|
|
751
|
+
if (item.status === "no_data" && series.length !== 0)
|
|
752
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence is invalid");
|
|
753
|
+
return {
|
|
754
|
+
window: item.window,
|
|
755
|
+
metric: item.metric,
|
|
756
|
+
status: item.status,
|
|
757
|
+
series,
|
|
758
|
+
truncated: item.truncated,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function validateIncidentData(value, requestedAlarmId) {
|
|
762
|
+
const data = validateResponseObject(value, "Incident data");
|
|
763
|
+
const expected = new Set(["alarm", "target", "category", "conclusion", "confidence", "evidence", "findings", "actions"]);
|
|
764
|
+
if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
|
|
765
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident response is invalid");
|
|
766
|
+
}
|
|
767
|
+
const alarm = validateAlarmRecord(data.alarm, 0, true);
|
|
768
|
+
if (alarm.id !== requestedAlarmId)
|
|
769
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident alarm identity is invalid");
|
|
770
|
+
const target = validateIncidentTarget(data.target, alarm);
|
|
771
|
+
if (typeof data.category !== "string" || !INCIDENT_CATEGORIES.has(data.category)) {
|
|
772
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident category is invalid");
|
|
773
|
+
}
|
|
774
|
+
const category = data.category;
|
|
775
|
+
if (typeof data.conclusion !== "string" || !INCIDENT_CONCLUSIONS.has(data.conclusion)) {
|
|
776
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident conclusion is invalid");
|
|
777
|
+
}
|
|
778
|
+
if (typeof data.confidence !== "string" || !INCIDENT_CONFIDENCES.has(data.confidence)) {
|
|
779
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident confidence is invalid");
|
|
780
|
+
}
|
|
781
|
+
if (!Array.isArray(data.evidence) || data.evidence.length > 2)
|
|
782
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence is invalid");
|
|
783
|
+
const evidence = data.evidence.map((raw, index) => validateIncidentEvidence(raw, index));
|
|
784
|
+
if (new Set(evidence.map((item) => item.window)).size !== evidence.length)
|
|
785
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence is invalid");
|
|
786
|
+
if ((category === "unknown" && evidence.length > 0)
|
|
787
|
+
|| (category !== "unknown" && evidence.some((item) => item.metric !== category))) {
|
|
788
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident evidence identity is invalid");
|
|
789
|
+
}
|
|
790
|
+
if (!Array.isArray(data.findings) || data.findings.length > 8 || data.findings.some((item) => typeof item !== "string" || !INCIDENT_FINDINGS.has(item))) {
|
|
791
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident findings are invalid");
|
|
792
|
+
}
|
|
793
|
+
if (new Set(data.findings).size !== data.findings.length)
|
|
794
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident findings are invalid");
|
|
795
|
+
if (!Array.isArray(data.actions) || data.actions.length > 8 || data.actions.some((item) => typeof item !== "string" || !INCIDENT_ACTIONS.has(item))) {
|
|
796
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident actions are invalid");
|
|
797
|
+
}
|
|
798
|
+
if (new Set(data.actions).size !== data.actions.length)
|
|
799
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident actions are invalid");
|
|
800
|
+
return {
|
|
801
|
+
alarm,
|
|
802
|
+
target,
|
|
803
|
+
category,
|
|
804
|
+
conclusion: data.conclusion,
|
|
805
|
+
confidence: data.confidence,
|
|
806
|
+
evidence,
|
|
807
|
+
findings: [...data.findings],
|
|
808
|
+
actions: [...data.actions],
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const INSPECTION_GRADES = new Set(["healthy", "warning", "critical", "insufficient"]);
|
|
812
|
+
const INSPECTION_MONITORING = new Set(["covered", "missing", "unknown"]);
|
|
813
|
+
const INSPECTION_FINDINGS = new Set([
|
|
814
|
+
"NO_ASSETS",
|
|
815
|
+
"MONITORING_MISSING",
|
|
816
|
+
"IDENTITY_UNKNOWN",
|
|
817
|
+
"ACTIVE_ALARMS",
|
|
818
|
+
"CRITICAL_ALARM",
|
|
819
|
+
"UNMATCHED_ALARMS",
|
|
820
|
+
"RESULT_TRUNCATED",
|
|
821
|
+
"MONITORING_UNAVAILABLE",
|
|
822
|
+
]);
|
|
823
|
+
const INSPECTION_ACTIONS = new Set([
|
|
824
|
+
"REVIEW_ACTIVE_ALARMS",
|
|
825
|
+
"REVIEW_ASSET_MONITORING",
|
|
826
|
+
"REVIEW_ASSET_IDENTITY",
|
|
827
|
+
"RECHECK_INSPECTION",
|
|
828
|
+
"INVESTIGATE_CRITICAL_ALARMS",
|
|
829
|
+
"NO_ACTION",
|
|
830
|
+
]);
|
|
831
|
+
function inspectionSafeCount(value, field) {
|
|
832
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 10_000_000) {
|
|
833
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} is invalid`);
|
|
834
|
+
}
|
|
835
|
+
return value;
|
|
836
|
+
}
|
|
837
|
+
function validateInspectionScope(value) {
|
|
838
|
+
const item = validateResponseObject(value, "Inspection scope");
|
|
839
|
+
const expected = new Set(["model", "window_minutes", "asset_limit", "alarm_limit"]);
|
|
840
|
+
if (Object.keys(item).length !== expected.size || Object.keys(item).some((key) => !expected.has(key))) {
|
|
841
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection scope is invalid");
|
|
842
|
+
}
|
|
843
|
+
if (item.model !== null && (typeof item.model !== "string" || item.model.length < 1 || item.model.length > 128 || /[\u0000-\u001f]/.test(item.model) || item.model.trim() !== item.model)) {
|
|
844
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection scope model is invalid");
|
|
845
|
+
}
|
|
846
|
+
if (typeof item.window_minutes !== "number" || !Number.isSafeInteger(item.window_minutes) || item.window_minutes < 5 || item.window_minutes > 1440) {
|
|
847
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection scope window is invalid");
|
|
848
|
+
}
|
|
849
|
+
if (item.asset_limit !== 100 || item.alarm_limit !== 100) {
|
|
850
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection scope limits are invalid");
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
model: item.model,
|
|
854
|
+
window_minutes: item.window_minutes,
|
|
855
|
+
asset_limit: item.asset_limit,
|
|
856
|
+
alarm_limit: item.alarm_limit,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function validateInspectionData(value) {
|
|
860
|
+
const data = validateResponseObject(value, "Inspection data");
|
|
861
|
+
const expected = new Set(["scope", "summary", "grade", "items", "unmatched_alarm_count", "truncated", "findings", "actions"]);
|
|
862
|
+
if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
|
|
863
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection response is invalid");
|
|
864
|
+
}
|
|
865
|
+
const scope = validateInspectionScope(data.scope);
|
|
866
|
+
const summaryRaw = validateResponseObject(data.summary, "Inspection summary");
|
|
867
|
+
const summaryKeys = new Set([
|
|
868
|
+
"assets_total",
|
|
869
|
+
"assets_examined",
|
|
870
|
+
"monitoring_covered",
|
|
871
|
+
"monitoring_missing",
|
|
872
|
+
"identity_unknown",
|
|
873
|
+
"active_alarms",
|
|
874
|
+
"affected_assets",
|
|
875
|
+
]);
|
|
876
|
+
if (Object.keys(summaryRaw).length !== summaryKeys.size || Object.keys(summaryRaw).some((key) => !summaryKeys.has(key))) {
|
|
877
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection summary is invalid");
|
|
878
|
+
}
|
|
879
|
+
const summary = {
|
|
880
|
+
assets_total: inspectionSafeCount(summaryRaw.assets_total, "Inspection assets total"),
|
|
881
|
+
assets_examined: inspectionSafeCount(summaryRaw.assets_examined, "Inspection assets examined"),
|
|
882
|
+
monitoring_covered: inspectionSafeCount(summaryRaw.monitoring_covered, "Inspection covered count"),
|
|
883
|
+
monitoring_missing: inspectionSafeCount(summaryRaw.monitoring_missing, "Inspection missing count"),
|
|
884
|
+
identity_unknown: inspectionSafeCount(summaryRaw.identity_unknown, "Inspection unknown count"),
|
|
885
|
+
active_alarms: inspectionSafeCount(summaryRaw.active_alarms, "Inspection alarm count"),
|
|
886
|
+
affected_assets: inspectionSafeCount(summaryRaw.affected_assets, "Inspection affected count"),
|
|
887
|
+
};
|
|
888
|
+
if (summary.assets_examined > summary.assets_total
|
|
889
|
+
|| summary.assets_examined > scope.asset_limit
|
|
890
|
+
|| summary.monitoring_covered + summary.monitoring_missing + summary.identity_unknown !== summary.assets_examined
|
|
891
|
+
|| summary.affected_assets > summary.assets_examined
|
|
892
|
+
|| summary.active_alarms + (typeof data.unmatched_alarm_count === "number" && Number.isSafeInteger(data.unmatched_alarm_count) && data.unmatched_alarm_count >= 0 ? data.unmatched_alarm_count : 10_000_001) > scope.alarm_limit) {
|
|
893
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection summary counts are inconsistent");
|
|
894
|
+
}
|
|
895
|
+
if (typeof data.grade !== "string" || !INSPECTION_GRADES.has(data.grade)) {
|
|
896
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection grade is invalid");
|
|
897
|
+
}
|
|
898
|
+
if (!Array.isArray(data.items) || data.items.length > 100) {
|
|
899
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection items are invalid");
|
|
900
|
+
}
|
|
901
|
+
const items = [];
|
|
902
|
+
const seenIds = new Set();
|
|
903
|
+
const seenIps = new Set();
|
|
904
|
+
let itemAlarmCount = 0;
|
|
905
|
+
let itemAffected = 0;
|
|
906
|
+
for (let index = 0; index < data.items.length; index += 1) {
|
|
907
|
+
const raw = validateResponseObject(data.items[index], `Inspection item ${index}`);
|
|
908
|
+
const itemKeys = new Set(["asset", "monitoring", "active_alarm_count", "max_severity"]);
|
|
909
|
+
if (Object.keys(raw).length !== itemKeys.size || Object.keys(raw).some((key) => !itemKeys.has(key))) {
|
|
910
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection item is invalid");
|
|
911
|
+
}
|
|
912
|
+
const asset = validateAssetRecord(raw.asset, index);
|
|
913
|
+
if (seenIds.has(asset.id))
|
|
914
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection item identity is invalid");
|
|
915
|
+
seenIds.add(asset.id);
|
|
916
|
+
if (asset.ip !== null) {
|
|
917
|
+
const canonical = canonicalIp(asset.ip);
|
|
918
|
+
if (canonical === null || seenIps.has(canonical))
|
|
919
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection item identity is invalid");
|
|
920
|
+
seenIps.add(canonical);
|
|
921
|
+
asset.ip = canonical;
|
|
922
|
+
}
|
|
923
|
+
if (typeof raw.monitoring !== "string" || !INSPECTION_MONITORING.has(raw.monitoring)) {
|
|
924
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection monitoring status is invalid");
|
|
925
|
+
}
|
|
926
|
+
const activeAlarmCount = inspectionSafeCount(raw.active_alarm_count, "Inspection item alarm count");
|
|
927
|
+
if (typeof raw.max_severity !== "string" || !ALARM_SEVERITIES.has(raw.max_severity)) {
|
|
928
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection item severity is invalid");
|
|
929
|
+
}
|
|
930
|
+
if (activeAlarmCount === 0 && raw.max_severity !== "unknown") {
|
|
931
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection item severity is inconsistent");
|
|
932
|
+
}
|
|
933
|
+
if (raw.monitoring === "covered" && activeAlarmCount === 0) {
|
|
934
|
+
throw new HttpError("PROTOCOL_ERROR", "Normal inspection items must be omitted");
|
|
935
|
+
}
|
|
936
|
+
if ((raw.monitoring === "covered" || raw.monitoring === "missing") && asset.ip === null) {
|
|
937
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection monitoring identity is invalid");
|
|
938
|
+
}
|
|
939
|
+
if (raw.monitoring === "unknown" && asset.ip !== null) {
|
|
940
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection monitoring identity is invalid");
|
|
941
|
+
}
|
|
942
|
+
itemAlarmCount += activeAlarmCount;
|
|
943
|
+
if (activeAlarmCount > 0)
|
|
944
|
+
itemAffected += 1;
|
|
945
|
+
items.push({
|
|
946
|
+
asset,
|
|
947
|
+
monitoring: raw.monitoring,
|
|
948
|
+
active_alarm_count: activeAlarmCount,
|
|
949
|
+
max_severity: raw.max_severity,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
const truncated = data.truncated;
|
|
953
|
+
if (typeof truncated !== "boolean")
|
|
954
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection truncation is invalid");
|
|
955
|
+
const itemMissing = items.filter((item) => item.monitoring === "missing").length;
|
|
956
|
+
const itemUnknown = items.filter((item) => item.monitoring === "unknown").length;
|
|
957
|
+
if (itemAlarmCount !== summary.active_alarms
|
|
958
|
+
|| itemAffected !== summary.affected_assets
|
|
959
|
+
|| itemMissing !== summary.monitoring_missing
|
|
960
|
+
|| itemUnknown !== summary.identity_unknown) {
|
|
961
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection item counts are inconsistent");
|
|
962
|
+
}
|
|
963
|
+
const unmatched = inspectionSafeCount(data.unmatched_alarm_count, "Inspection unmatched alarm count");
|
|
964
|
+
if (summary.active_alarms + unmatched > scope.alarm_limit) {
|
|
965
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection alarm counts are inconsistent");
|
|
966
|
+
}
|
|
967
|
+
if (summary.assets_total > summary.assets_examined && !truncated) {
|
|
968
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection truncation is inconsistent");
|
|
969
|
+
}
|
|
970
|
+
const parseCodes = (value, field, allowed) => {
|
|
971
|
+
if (!Array.isArray(value) || value.length > 8 || value.some((entry) => typeof entry !== "string" || !allowed.has(entry))) {
|
|
972
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} are invalid`);
|
|
973
|
+
}
|
|
974
|
+
const values = [...value];
|
|
975
|
+
if (new Set(values).size !== values.length)
|
|
976
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} are invalid`);
|
|
977
|
+
return values;
|
|
978
|
+
};
|
|
979
|
+
const findings = parseCodes(data.findings, "Inspection findings", INSPECTION_FINDINGS);
|
|
980
|
+
const actions = parseCodes(data.actions, "Inspection actions", INSPECTION_ACTIONS);
|
|
981
|
+
const hasCriticalItem = items.some((item) => item.max_severity === "critical");
|
|
982
|
+
if ((data.grade === "healthy" && (truncated || summary.assets_examined < 1 || summary.monitoring_covered < 1 || summary.monitoring_missing > 0 || summary.identity_unknown > 0 || summary.active_alarms > 0 || summary.assets_total !== summary.assets_examined))
|
|
983
|
+
|| (data.grade === "critical" && !hasCriticalItem)
|
|
984
|
+
|| (hasCriticalItem && data.grade !== "critical")) {
|
|
985
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection grade is inconsistent");
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
scope,
|
|
989
|
+
summary,
|
|
990
|
+
grade: data.grade,
|
|
991
|
+
items,
|
|
992
|
+
unmatched_alarm_count: unmatched,
|
|
993
|
+
truncated,
|
|
994
|
+
findings,
|
|
995
|
+
actions,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const CHAT_ID_PATTERN = /^[A-Za-z0-9_-]{1,96}$/;
|
|
999
|
+
const EMPLOYEE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
1000
|
+
const CHAT_ERROR_CODES = new Set([
|
|
1001
|
+
"AUTH_RELOGIN_REQUIRED",
|
|
1002
|
+
"CAPABILITY_SCOPE_REQUIRED",
|
|
1003
|
+
"CAPABILITY_NOT_AVAILABLE",
|
|
1004
|
+
"EMPLOYEE_NOT_FOUND",
|
|
1005
|
+
"EMPLOYEE_NOT_CHATABLE",
|
|
1006
|
+
"CONVERSATION_NOT_FOUND",
|
|
1007
|
+
"CONVERSATION_BUSY",
|
|
1008
|
+
"CONVERSATION_CAPACITY_REACHED",
|
|
1009
|
+
"DATA_SOURCE_UNAVAILABLE",
|
|
1010
|
+
"TIMEOUT",
|
|
1011
|
+
"USER_CANCELLED",
|
|
1012
|
+
"PROTOCOL_ERROR",
|
|
1013
|
+
]);
|
|
1014
|
+
function validateChatId(value, field) {
|
|
1015
|
+
if (typeof value !== "string" || !CHAT_ID_PATTERN.test(value)) {
|
|
1016
|
+
throw new HttpError("PROTOCOL_ERROR", `${field} is invalid`);
|
|
1017
|
+
}
|
|
1018
|
+
return value;
|
|
1019
|
+
}
|
|
1020
|
+
function validateEmployeeId(value) {
|
|
1021
|
+
if (typeof value !== "string" || !EMPLOYEE_ID_PATTERN.test(value)) {
|
|
1022
|
+
throw new HttpError("PROTOCOL_ERROR", "employee id is invalid");
|
|
1023
|
+
}
|
|
1024
|
+
return value;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Incremental SSE parser for the public employee-chat protocol.
|
|
1028
|
+
*
|
|
1029
|
+
* It deliberately returns a tiny allowlisted event shape. Provider payloads,
|
|
1030
|
+
* internal IDs, tool calls and error text never become part of a thrown error
|
|
1031
|
+
* or callback value.
|
|
1032
|
+
*/
|
|
1033
|
+
export class EmployeeChatSseParser {
|
|
1034
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1035
|
+
buffer = "";
|
|
1036
|
+
totalBytes = 0;
|
|
1037
|
+
terminal = false;
|
|
1038
|
+
terminalErrorCode;
|
|
1039
|
+
conversationId;
|
|
1040
|
+
text = "";
|
|
1041
|
+
maxTotalBytes;
|
|
1042
|
+
maxEventBytes;
|
|
1043
|
+
constructor(initialConversationId, options = {}) {
|
|
1044
|
+
if (initialConversationId !== undefined)
|
|
1045
|
+
validateChatId(initialConversationId, "conversation id");
|
|
1046
|
+
this.conversationId = initialConversationId;
|
|
1047
|
+
this.maxTotalBytes = options.maxTotalBytes ?? 4 * 1024 * 1024;
|
|
1048
|
+
this.maxEventBytes = options.maxEventBytes ?? 64 * 1024;
|
|
1049
|
+
if (!Number.isSafeInteger(this.maxTotalBytes) || this.maxTotalBytes < 1) {
|
|
1050
|
+
throw new HttpError("PROTOCOL_ERROR", "SSE limits are invalid");
|
|
1051
|
+
}
|
|
1052
|
+
if (!Number.isSafeInteger(this.maxEventBytes) || this.maxEventBytes < 1 || this.maxEventBytes > this.maxTotalBytes) {
|
|
1053
|
+
throw new HttpError("PROTOCOL_ERROR", "SSE limits are invalid");
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
push(chunk) {
|
|
1057
|
+
const bytes = typeof chunk === "string" ? Buffer.byteLength(chunk, "utf8") : chunk.byteLength;
|
|
1058
|
+
if (this.terminal && bytes > 0)
|
|
1059
|
+
throw new HttpError("PROTOCOL_ERROR", "SSE stream has already terminated");
|
|
1060
|
+
this.totalBytes += bytes;
|
|
1061
|
+
if (this.totalBytes > this.maxTotalBytes) {
|
|
1062
|
+
throw new HttpError("RESPONSE_TOO_LARGE", "Chat stream is too large");
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
this.buffer += typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream encoding is invalid");
|
|
1069
|
+
}
|
|
1070
|
+
const events = [];
|
|
1071
|
+
for (;;) {
|
|
1072
|
+
const boundary = this.findBoundary(this.buffer);
|
|
1073
|
+
if (!boundary) {
|
|
1074
|
+
if (Buffer.byteLength(this.buffer, "utf8") > this.maxEventBytes) {
|
|
1075
|
+
throw new HttpError("RESPONSE_TOO_LARGE", "Chat event is too large");
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
const frame = this.buffer.slice(0, boundary.index);
|
|
1080
|
+
this.buffer = this.buffer.slice(boundary.index + boundary.length);
|
|
1081
|
+
if (Buffer.byteLength(frame, "utf8") > this.maxEventBytes) {
|
|
1082
|
+
throw new HttpError("RESPONSE_TOO_LARGE", "Chat event is too large");
|
|
1083
|
+
}
|
|
1084
|
+
const event = this.decodeFrame(frame);
|
|
1085
|
+
if (event) {
|
|
1086
|
+
events.push(event);
|
|
1087
|
+
if (this.terminal) {
|
|
1088
|
+
if (this.buffer.length > 0)
|
|
1089
|
+
throw new HttpError("PROTOCOL_ERROR", "SSE data followed a terminal event");
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return events;
|
|
1095
|
+
}
|
|
1096
|
+
finish() {
|
|
1097
|
+
try {
|
|
1098
|
+
this.buffer += this.decoder.decode();
|
|
1099
|
+
}
|
|
1100
|
+
catch {
|
|
1101
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream encoding is invalid");
|
|
1102
|
+
}
|
|
1103
|
+
if (this.buffer !== "") {
|
|
1104
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream did not terminate an event");
|
|
1105
|
+
}
|
|
1106
|
+
if (!this.terminal || !this.conversationId) {
|
|
1107
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream did not terminate");
|
|
1108
|
+
}
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
result() {
|
|
1112
|
+
if (this.terminalErrorCode) {
|
|
1113
|
+
throw new HttpError(this.terminalErrorCode, "The server rejected the chat request.", {
|
|
1114
|
+
status: 200,
|
|
1115
|
+
retryable: retryableForCode(this.terminalErrorCode, false),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
if (!this.terminal || !this.conversationId) {
|
|
1119
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream did not terminate");
|
|
1120
|
+
}
|
|
1121
|
+
return { conversation_id: this.conversationId, text: this.text };
|
|
1122
|
+
}
|
|
1123
|
+
findBoundary(value) {
|
|
1124
|
+
const candidates = ["\r\n\r\n", "\n\n", "\r\r"]
|
|
1125
|
+
.map((separator) => ({ index: value.indexOf(separator), length: separator.length }))
|
|
1126
|
+
.filter((item) => item.index >= 0);
|
|
1127
|
+
return candidates.length ? candidates.sort((left, right) => left.index - right.index)[0] : undefined;
|
|
1128
|
+
}
|
|
1129
|
+
decodeFrame(frame) {
|
|
1130
|
+
let eventName = "message";
|
|
1131
|
+
const dataLines = [];
|
|
1132
|
+
for (const rawLine of frame.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n")) {
|
|
1133
|
+
if (!rawLine || rawLine.startsWith(":"))
|
|
1134
|
+
continue;
|
|
1135
|
+
if (rawLine.startsWith("event:"))
|
|
1136
|
+
eventName = rawLine.slice(6).trim();
|
|
1137
|
+
else if (rawLine.startsWith("data:"))
|
|
1138
|
+
dataLines.push(rawLine.slice(5).replace(/^ /, ""));
|
|
1139
|
+
}
|
|
1140
|
+
if (!dataLines.length)
|
|
1141
|
+
return undefined;
|
|
1142
|
+
const rawData = dataLines.join("\n");
|
|
1143
|
+
if (rawData.trim() === "[DONE]") {
|
|
1144
|
+
this.terminal = true;
|
|
1145
|
+
if (!this.conversationId)
|
|
1146
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat conversation is missing");
|
|
1147
|
+
return { type: "done", conversation_id: this.conversationId };
|
|
1148
|
+
}
|
|
1149
|
+
let payload;
|
|
1150
|
+
try {
|
|
1151
|
+
payload = JSON.parse(rawData);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream payload is invalid");
|
|
1155
|
+
}
|
|
1156
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1157
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat stream payload is invalid");
|
|
1158
|
+
}
|
|
1159
|
+
const record = payload;
|
|
1160
|
+
if (!["conversation", "delta", "done", "error", "message", ""].includes(eventName))
|
|
1161
|
+
return undefined;
|
|
1162
|
+
if (eventName === "conversation") {
|
|
1163
|
+
const conversationId = validateChatId(record.conversation_id, "conversation id");
|
|
1164
|
+
if (this.conversationId && this.conversationId !== conversationId) {
|
|
1165
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat conversation changed");
|
|
1166
|
+
}
|
|
1167
|
+
this.conversationId = conversationId;
|
|
1168
|
+
return { type: "conversation", conversation_id: conversationId };
|
|
1169
|
+
}
|
|
1170
|
+
if (eventName === "delta") {
|
|
1171
|
+
if (typeof record.text !== "string" || record.text.length === 0) {
|
|
1172
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat delta is invalid");
|
|
1173
|
+
}
|
|
1174
|
+
this.text += record.text;
|
|
1175
|
+
if (Buffer.byteLength(this.text, "utf8") > this.maxTotalBytes) {
|
|
1176
|
+
throw new HttpError("RESPONSE_TOO_LARGE", "Chat text is too large");
|
|
1177
|
+
}
|
|
1178
|
+
return { type: "delta", text: record.text };
|
|
1179
|
+
}
|
|
1180
|
+
if (eventName === "error") {
|
|
1181
|
+
const code = typeof record.code === "string" && /^[A-Z][A-Z0-9_]{2,63}$/.test(record.code)
|
|
1182
|
+
? record.code
|
|
1183
|
+
: "PROTOCOL_ERROR";
|
|
1184
|
+
this.terminal = true;
|
|
1185
|
+
this.terminalErrorCode = CHAT_ERROR_CODES.has(code) ? code : "PROTOCOL_ERROR";
|
|
1186
|
+
return { type: "error", code: this.terminalErrorCode };
|
|
1187
|
+
}
|
|
1188
|
+
if (eventName === "done") {
|
|
1189
|
+
const conversationId = record.conversation_id === undefined
|
|
1190
|
+
? this.conversationId
|
|
1191
|
+
: validateChatId(record.conversation_id, "conversation id");
|
|
1192
|
+
if (!conversationId || (this.conversationId && this.conversationId !== conversationId)) {
|
|
1193
|
+
throw new HttpError("PROTOCOL_ERROR", "Chat conversation is invalid");
|
|
1194
|
+
}
|
|
1195
|
+
this.conversationId = conversationId;
|
|
1196
|
+
this.terminal = true;
|
|
1197
|
+
return { type: "done", conversation_id: conversationId };
|
|
1198
|
+
}
|
|
1199
|
+
// A message/unknown payload is intentionally ignored; only the public
|
|
1200
|
+
// event names above can affect the result.
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function validateExpires(value) {
|
|
1205
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 365 * 24 * 3600) {
|
|
1206
|
+
throw new HttpError("PROTOCOL_ERROR", "Token expiry response is invalid");
|
|
1207
|
+
}
|
|
1208
|
+
return value;
|
|
1209
|
+
}
|
|
1210
|
+
function joinPath(basePath, suffix) {
|
|
1211
|
+
const normalized = basePath.replace(/\/+$/, "");
|
|
1212
|
+
return `${normalized}${INTEGRATION_PATH}${suffix}` || "/";
|
|
1213
|
+
}
|
|
1214
|
+
function isLoopbackHostname(hostname) {
|
|
1215
|
+
const value = hostname.toLowerCase();
|
|
1216
|
+
return value === "localhost" || value === "127.0.0.1" || value === "[::1]" || value === "::1";
|
|
1217
|
+
}
|
|
1218
|
+
export function integrationUrl(baseUrl, suffix) {
|
|
1219
|
+
let parsed;
|
|
1220
|
+
try {
|
|
1221
|
+
parsed = new URL(baseUrl);
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
throw new HttpError("CONFIG_INVALID", "Profile base_url is invalid");
|
|
1225
|
+
}
|
|
1226
|
+
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
1227
|
+
throw new HttpError("CONFIG_INVALID", "Profile base_url must be an HTTP(S) URL without credentials");
|
|
1228
|
+
}
|
|
1229
|
+
if (parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) {
|
|
1230
|
+
throw new HttpError("CONFIG_INVALID", "Profile base_url must use HTTPS");
|
|
1231
|
+
}
|
|
1232
|
+
if (!/^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(suffix) || suffix.includes("//")) {
|
|
1233
|
+
throw new HttpError("PROTOCOL_ERROR", "Integration path is invalid");
|
|
1234
|
+
}
|
|
1235
|
+
parsed.pathname = joinPath(parsed.pathname, suffix);
|
|
1236
|
+
parsed.search = "";
|
|
1237
|
+
parsed.hash = "";
|
|
1238
|
+
return parsed;
|
|
1239
|
+
}
|
|
1240
|
+
function requestOptions(url, profile, headers) {
|
|
1241
|
+
const options = {
|
|
1242
|
+
protocol: url.protocol,
|
|
1243
|
+
hostname: url.hostname,
|
|
1244
|
+
port: url.port || undefined,
|
|
1245
|
+
path: `${url.pathname}${url.search}`,
|
|
1246
|
+
method: headers["content-length"] ? "POST" : "GET",
|
|
1247
|
+
headers,
|
|
1248
|
+
};
|
|
1249
|
+
if (url.protocol === "https:") {
|
|
1250
|
+
options.rejectUnauthorized = true;
|
|
1251
|
+
if (profile.ca_bundle) {
|
|
1252
|
+
try {
|
|
1253
|
+
if (!isAbsolute(profile.ca_bundle))
|
|
1254
|
+
throw new Error("CA path must be absolute");
|
|
1255
|
+
const stat = lstatSync(profile.ca_bundle);
|
|
1256
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
1257
|
+
throw new Error("CA bundle must be a regular file");
|
|
1258
|
+
const size = stat.size;
|
|
1259
|
+
if (!Number.isSafeInteger(size) || size <= 0 || size > MAX_CA_BUNDLE_BYTES)
|
|
1260
|
+
throw new Error("CA bundle too large");
|
|
1261
|
+
options.ca = readFileSync(profile.ca_bundle);
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
throw new HttpError("CONFIG_INVALID", "Profile CA bundle cannot be read");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return options;
|
|
1269
|
+
}
|
|
1270
|
+
function collectResponse(response, maxBytes) {
|
|
1271
|
+
return new Promise((resolve, reject) => {
|
|
1272
|
+
let total = 0;
|
|
1273
|
+
const chunks = [];
|
|
1274
|
+
let settled = false;
|
|
1275
|
+
const fail = (error) => {
|
|
1276
|
+
if (settled)
|
|
1277
|
+
return;
|
|
1278
|
+
settled = true;
|
|
1279
|
+
response.destroy();
|
|
1280
|
+
reject(error);
|
|
1281
|
+
};
|
|
1282
|
+
response.on("data", (chunk) => {
|
|
1283
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1284
|
+
total += buffer.length;
|
|
1285
|
+
if (total > maxBytes) {
|
|
1286
|
+
fail(new HttpError("RESPONSE_TOO_LARGE", "Server response is too large"));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
chunks.push(buffer);
|
|
1290
|
+
});
|
|
1291
|
+
response.on("end", () => {
|
|
1292
|
+
if (settled)
|
|
1293
|
+
return;
|
|
1294
|
+
settled = true;
|
|
1295
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
1296
|
+
});
|
|
1297
|
+
response.on("error", (error) => fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true })));
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
export class IntegrationHttpClient {
|
|
1301
|
+
baseUrl;
|
|
1302
|
+
profile;
|
|
1303
|
+
maxResponseBytes;
|
|
1304
|
+
constructor(profile, options = {}) {
|
|
1305
|
+
if (!profile.base_url)
|
|
1306
|
+
throw new HttpError("CONFIG_INVALID", "Profile base_url is required");
|
|
1307
|
+
// Validate once at construction so every route remains fixed to this origin.
|
|
1308
|
+
integrationUrl(profile.base_url, "/info");
|
|
1309
|
+
this.baseUrl = profile.base_url;
|
|
1310
|
+
this.profile = profile;
|
|
1311
|
+
this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
1312
|
+
}
|
|
1313
|
+
async requestJson(method, suffix, body, accessToken, options = {}) {
|
|
1314
|
+
const url = integrationUrl(this.baseUrl, suffix);
|
|
1315
|
+
const encoded = body === undefined ? "" : JSON.stringify(body);
|
|
1316
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_REQUEST_BYTES) {
|
|
1317
|
+
throw new HttpError("PROTOCOL_ERROR", "Request is too large");
|
|
1318
|
+
}
|
|
1319
|
+
const headers = {
|
|
1320
|
+
accept: "application/json",
|
|
1321
|
+
"cache-control": "no-store",
|
|
1322
|
+
pragma: "no-cache",
|
|
1323
|
+
connection: "close",
|
|
1324
|
+
"user-agent": CLI_USER_AGENT,
|
|
1325
|
+
"x-zhihu-cli-version": CLI_VERSION,
|
|
1326
|
+
};
|
|
1327
|
+
if (body !== undefined) {
|
|
1328
|
+
headers["content-type"] = "application/json";
|
|
1329
|
+
headers["content-length"] = String(Buffer.byteLength(encoded, "utf8"));
|
|
1330
|
+
}
|
|
1331
|
+
if (accessToken)
|
|
1332
|
+
headers.authorization = `Bearer ${accessToken}`;
|
|
1333
|
+
const requestConfig = requestOptions(url, this.profile, headers);
|
|
1334
|
+
requestConfig.method = method;
|
|
1335
|
+
const raw = await this.send(requestConfig, encoded);
|
|
1336
|
+
if (raw.status >= 300 && raw.status < 400) {
|
|
1337
|
+
throw new HttpError("REDIRECT_NOT_ALLOWED", "Server redirects are not allowed", { status: raw.status });
|
|
1338
|
+
}
|
|
1339
|
+
if (raw.status < 200 || raw.status >= 300)
|
|
1340
|
+
throw responseError(raw.status, raw.body);
|
|
1341
|
+
if (!raw.body.trim() && options.allowEmpty)
|
|
1342
|
+
return undefined;
|
|
1343
|
+
try {
|
|
1344
|
+
return JSON.parse(raw.body);
|
|
1345
|
+
}
|
|
1346
|
+
catch {
|
|
1347
|
+
throw new HttpError("PROTOCOL_ERROR", "Server response is not valid JSON", { status: raw.status });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
send(options, body) {
|
|
1351
|
+
return new Promise((resolve, reject) => {
|
|
1352
|
+
const protocol = options.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1353
|
+
let request;
|
|
1354
|
+
try {
|
|
1355
|
+
request = protocol(options, async (response) => {
|
|
1356
|
+
try {
|
|
1357
|
+
const responseBody = await collectResponse(response, this.maxResponseBytes);
|
|
1358
|
+
resolve({ status: response.statusCode ?? 0, headers: response.headers, body: responseBody });
|
|
1359
|
+
}
|
|
1360
|
+
catch (error) {
|
|
1361
|
+
reject(error instanceof HttpError ? error : new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
catch {
|
|
1366
|
+
reject(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const timeout = Math.max(1, this.profile.timeout_seconds) * 1000;
|
|
1370
|
+
request.setTimeout(timeout, () => {
|
|
1371
|
+
request.destroy();
|
|
1372
|
+
reject(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
|
|
1373
|
+
});
|
|
1374
|
+
request.once("error", (error) => {
|
|
1375
|
+
if (error.code === "ETIMEDOUT")
|
|
1376
|
+
reject(new HttpError("TIMEOUT", "Network request timed out", { retryable: true }));
|
|
1377
|
+
else
|
|
1378
|
+
reject(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1379
|
+
});
|
|
1380
|
+
if (body)
|
|
1381
|
+
request.write(body);
|
|
1382
|
+
request.end();
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
async info(options = {}) {
|
|
1386
|
+
const value = await this.requestJson("GET", "/info");
|
|
1387
|
+
if (options.requireReady)
|
|
1388
|
+
return validateIntegrationInfo(value, true);
|
|
1389
|
+
// Keep the low-level discovery call backwards compatible for existing
|
|
1390
|
+
// clients/tests; installation and doctor opt into the strict contract.
|
|
1391
|
+
return validateResponseObject(value, "Info");
|
|
1392
|
+
}
|
|
1393
|
+
async capabilities(accessToken) {
|
|
1394
|
+
const envelope = validateResponseObject(await this.requestJson("GET", "/capabilities", undefined, accessToken), "Capabilities");
|
|
1395
|
+
if (envelope.ok !== true) {
|
|
1396
|
+
throw new HttpError("PROTOCOL_ERROR", "Capabilities response is invalid");
|
|
1397
|
+
}
|
|
1398
|
+
const data = validateResponseObject(envelope.data, "Capabilities data");
|
|
1399
|
+
const userId = validateIdentityId(data.user_id, "user_id");
|
|
1400
|
+
const tenantId = validateIdentityId(data.tenant_id, "tenant_id", true);
|
|
1401
|
+
const scopes = validateStringList(data.scopes, "scopes");
|
|
1402
|
+
const capabilities = validateStringList(data.capabilities, "capabilities");
|
|
1403
|
+
if (scopes.length !== capabilities.length || scopes.some((scope, index) => capabilities[index] !== scope)) {
|
|
1404
|
+
throw new HttpError("PROTOCOL_ERROR", "Capabilities response is invalid");
|
|
1405
|
+
}
|
|
1406
|
+
const meta = validateResponseObject(envelope.meta, "Capabilities metadata");
|
|
1407
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1408
|
+
|| meta.capability !== "capabilities.list"
|
|
1409
|
+
|| meta.schema_version !== "1.0"
|
|
1410
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1411
|
+
throw new HttpError("PROTOCOL_ERROR", "Capabilities metadata is invalid");
|
|
1412
|
+
}
|
|
1413
|
+
return { user_id: userId, tenant_id: tenantId, scopes, capabilities };
|
|
1414
|
+
}
|
|
1415
|
+
async employees(accessToken) {
|
|
1416
|
+
const envelope = validateResponseObject(await this.requestJson("GET", "/employees", undefined, accessToken), "Employees");
|
|
1417
|
+
if (envelope.ok !== true) {
|
|
1418
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees response is invalid");
|
|
1419
|
+
}
|
|
1420
|
+
const data = validateResponseObject(envelope.data, "Employees data");
|
|
1421
|
+
const employees = validateEmployeeList(data.employees);
|
|
1422
|
+
const meta = validateResponseObject(envelope.meta, "Employees metadata");
|
|
1423
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1424
|
+
|| meta.capability !== "employees.list"
|
|
1425
|
+
|| meta.schema_version !== "1.0"
|
|
1426
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1427
|
+
throw new HttpError("PROTOCOL_ERROR", "Employees metadata is invalid");
|
|
1428
|
+
}
|
|
1429
|
+
return { employees };
|
|
1430
|
+
}
|
|
1431
|
+
async assetsSearch(input, accessToken) {
|
|
1432
|
+
if (!Number.isSafeInteger(input.page) || input.page < 1 || input.page > 10000
|
|
1433
|
+
|| !Number.isSafeInteger(input.page_size) || input.page_size < 1 || input.page_size > 100) {
|
|
1434
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets request pagination is invalid");
|
|
1435
|
+
}
|
|
1436
|
+
for (const value of [input.keyword, input.model]) {
|
|
1437
|
+
if (value !== undefined && (typeof value !== "string" || value.length > 256 || /[\u0000-\u001f]/.test(value))) {
|
|
1438
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets request is invalid");
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (input.model !== undefined && input.model.length > 128) {
|
|
1442
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets model is invalid");
|
|
1443
|
+
}
|
|
1444
|
+
const envelope = validateResponseObject(await this.requestJson("POST", "/assets/search", {
|
|
1445
|
+
...(input.keyword !== undefined ? { keyword: input.keyword } : {}),
|
|
1446
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
1447
|
+
page: input.page,
|
|
1448
|
+
page_size: input.page_size,
|
|
1449
|
+
}, accessToken), "Assets");
|
|
1450
|
+
if (envelope.ok !== true)
|
|
1451
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets response is invalid");
|
|
1452
|
+
const data = validateResponseObject(envelope.data, "Assets data");
|
|
1453
|
+
const page = data.page;
|
|
1454
|
+
const pageSize = data.page_size;
|
|
1455
|
+
const total = data.total;
|
|
1456
|
+
if (page !== input.page || pageSize !== input.page_size
|
|
1457
|
+
|| !Number.isSafeInteger(page) || !Number.isSafeInteger(pageSize)
|
|
1458
|
+
|| !Number.isSafeInteger(total) || page < 1 || page > 10000
|
|
1459
|
+
|| pageSize < 1 || pageSize > 100 || total < 0) {
|
|
1460
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets pagination is invalid");
|
|
1461
|
+
}
|
|
1462
|
+
const items = validateAssetItems(data.items, pageSize);
|
|
1463
|
+
if (total < items.length)
|
|
1464
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets pagination is invalid");
|
|
1465
|
+
const meta = validateResponseObject(envelope.meta, "Assets metadata");
|
|
1466
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1467
|
+
|| meta.capability !== "assets.search"
|
|
1468
|
+
|| meta.schema_version !== "1.0"
|
|
1469
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1470
|
+
throw new HttpError("PROTOCOL_ERROR", "Assets metadata is invalid");
|
|
1471
|
+
}
|
|
1472
|
+
return { page, page_size: pageSize, total, items };
|
|
1473
|
+
}
|
|
1474
|
+
async assetGet(assetId, accessToken) {
|
|
1475
|
+
const normalizedId = validateAssetRequestId(assetId);
|
|
1476
|
+
const envelope = validateResponseObject(await this.requestJson("GET", `/assets/${assetId}`, undefined, accessToken), "Asset");
|
|
1477
|
+
if (envelope.ok !== true)
|
|
1478
|
+
throw new HttpError("PROTOCOL_ERROR", "Asset response is invalid");
|
|
1479
|
+
const data = validateResponseObject(envelope.data, "Asset data");
|
|
1480
|
+
if (Object.keys(data).length !== 1 || !Object.hasOwn(data, "asset")) {
|
|
1481
|
+
throw new HttpError("PROTOCOL_ERROR", "Asset data response is invalid");
|
|
1482
|
+
}
|
|
1483
|
+
const asset = validateAssetRecord(data.asset, 0);
|
|
1484
|
+
if (asset.id !== normalizedId) {
|
|
1485
|
+
throw new HttpError("PROTOCOL_ERROR", "Asset response identity is invalid");
|
|
1486
|
+
}
|
|
1487
|
+
const meta = validateResponseObject(envelope.meta, "Asset metadata");
|
|
1488
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1489
|
+
|| meta.capability !== "assets.get"
|
|
1490
|
+
|| meta.schema_version !== "1.0"
|
|
1491
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1492
|
+
throw new HttpError("PROTOCOL_ERROR", "Asset metadata is invalid");
|
|
1493
|
+
}
|
|
1494
|
+
return { asset };
|
|
1495
|
+
}
|
|
1496
|
+
async alarmsSearch(input, accessToken) {
|
|
1497
|
+
if (!ALARM_QUERY_STATUSES.has(input.status)
|
|
1498
|
+
|| !Number.isSafeInteger(input.page) || input.page < 1 || input.page > 10000
|
|
1499
|
+
|| !Number.isSafeInteger(input.page_size) || input.page_size < 1 || input.page_size > 100) {
|
|
1500
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms request is invalid");
|
|
1501
|
+
}
|
|
1502
|
+
const envelope = validateResponseObject(await this.requestJson("POST", "/alarms/search", {
|
|
1503
|
+
status: input.status,
|
|
1504
|
+
page: input.page,
|
|
1505
|
+
page_size: input.page_size,
|
|
1506
|
+
}, accessToken), "Alarms");
|
|
1507
|
+
if (envelope.ok !== true)
|
|
1508
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms response is invalid");
|
|
1509
|
+
const data = validateResponseObject(envelope.data, "Alarms data");
|
|
1510
|
+
const expected = new Set(["page", "page_size", "total", "items"]);
|
|
1511
|
+
if (Object.keys(data).length !== expected.size || Object.keys(data).some((key) => !expected.has(key))) {
|
|
1512
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms data response is invalid");
|
|
1513
|
+
}
|
|
1514
|
+
const responsePage = data.page;
|
|
1515
|
+
const responsePageSize = data.page_size;
|
|
1516
|
+
const responseTotal = data.total;
|
|
1517
|
+
if (typeof responsePage !== "number" || typeof responsePageSize !== "number" || typeof responseTotal !== "number"
|
|
1518
|
+
|| responsePage !== input.page || responsePageSize !== input.page_size
|
|
1519
|
+
|| !Number.isSafeInteger(responsePage) || !Number.isSafeInteger(responsePageSize)
|
|
1520
|
+
|| !Number.isSafeInteger(responseTotal) || responsePage < 1 || responsePage > 10000
|
|
1521
|
+
|| responsePageSize < 1 || responsePageSize > 100 || responseTotal < 0 || responseTotal > 10_000_000) {
|
|
1522
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms pagination is invalid");
|
|
1523
|
+
}
|
|
1524
|
+
if (!Array.isArray(data.items) || data.items.length > responsePageSize || data.items.length > 100) {
|
|
1525
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms response is invalid");
|
|
1526
|
+
}
|
|
1527
|
+
const seen = new Set();
|
|
1528
|
+
const items = data.items.map((item, index) => validateAlarmRecord(item, index, false, seen));
|
|
1529
|
+
if (responseTotal < items.length)
|
|
1530
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms pagination is invalid");
|
|
1531
|
+
if (input.status !== "all" && items.some((item) => item.status !== input.status)) {
|
|
1532
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms status filter response is invalid");
|
|
1533
|
+
}
|
|
1534
|
+
const meta = validateResponseObject(envelope.meta, "Alarms metadata");
|
|
1535
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1536
|
+
|| meta.capability !== "alarms.search"
|
|
1537
|
+
|| meta.schema_version !== "1.0"
|
|
1538
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1539
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarms metadata is invalid");
|
|
1540
|
+
}
|
|
1541
|
+
return { page: responsePage, page_size: responsePageSize, total: responseTotal, items };
|
|
1542
|
+
}
|
|
1543
|
+
async alarmGet(alarmId, accessToken) {
|
|
1544
|
+
const normalizedId = validateAlarmRequestId(alarmId);
|
|
1545
|
+
const envelope = validateResponseObject(await this.requestJson("GET", `/alarms/${normalizedId}`, undefined, accessToken), "Alarm");
|
|
1546
|
+
if (envelope.ok !== true)
|
|
1547
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm response is invalid");
|
|
1548
|
+
const data = validateResponseObject(envelope.data, "Alarm data");
|
|
1549
|
+
if (Object.keys(data).length !== 1 || !Object.hasOwn(data, "alarm")) {
|
|
1550
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm data response is invalid");
|
|
1551
|
+
}
|
|
1552
|
+
const alarm = validateAlarmRecord(data.alarm, 0, true);
|
|
1553
|
+
if (alarm.id !== normalizedId)
|
|
1554
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm response identity is invalid");
|
|
1555
|
+
const meta = validateResponseObject(envelope.meta, "Alarm metadata");
|
|
1556
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1557
|
+
|| meta.capability !== "alarms.get"
|
|
1558
|
+
|| meta.schema_version !== "1.0"
|
|
1559
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1560
|
+
throw new HttpError("PROTOCOL_ERROR", "Alarm metadata is invalid");
|
|
1561
|
+
}
|
|
1562
|
+
return { alarm };
|
|
1563
|
+
}
|
|
1564
|
+
async alarmsGet(alarmId, accessToken) {
|
|
1565
|
+
return this.alarmGet(alarmId, accessToken);
|
|
1566
|
+
}
|
|
1567
|
+
async incidentsDiagnose(alarmId, accessToken) {
|
|
1568
|
+
const normalizedId = validateAlarmRequestId(alarmId);
|
|
1569
|
+
const envelope = validateResponseObject(await this.requestJson("POST", "/incidents/diagnose", { alarm_id: normalizedId }, accessToken), "Incident");
|
|
1570
|
+
const envelopeKeys = new Set(["ok", "data", "meta"]);
|
|
1571
|
+
if (Object.keys(envelope).length !== envelopeKeys.size || Object.keys(envelope).some((key) => !envelopeKeys.has(key))) {
|
|
1572
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident response is invalid");
|
|
1573
|
+
}
|
|
1574
|
+
if (envelope.ok !== true)
|
|
1575
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident response is invalid");
|
|
1576
|
+
const data = validateIncidentData(envelope.data, normalizedId);
|
|
1577
|
+
const meta = validateResponseObject(envelope.meta, "Incident metadata");
|
|
1578
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1579
|
+
|| meta.capability !== "incidents.diagnose"
|
|
1580
|
+
|| meta.schema_version !== "1.0"
|
|
1581
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1582
|
+
throw new HttpError("PROTOCOL_ERROR", "Incident metadata is invalid");
|
|
1583
|
+
}
|
|
1584
|
+
return data;
|
|
1585
|
+
}
|
|
1586
|
+
async inspectionsRun(input, accessToken) {
|
|
1587
|
+
if (!Number.isSafeInteger(input.window_minutes) || input.window_minutes < 5 || input.window_minutes > 1440) {
|
|
1588
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection window is invalid");
|
|
1589
|
+
}
|
|
1590
|
+
if (input.model !== undefined && input.model !== null
|
|
1591
|
+
&& (typeof input.model !== "string" || input.model.length < 1 || input.model.length > 128
|
|
1592
|
+
|| input.model.trim() !== input.model || /[\u0000-\u001f]/.test(input.model))) {
|
|
1593
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection model is invalid");
|
|
1594
|
+
}
|
|
1595
|
+
const envelope = validateResponseObject(await this.requestJson("POST", "/inspections/run", {
|
|
1596
|
+
...(input.model !== undefined && input.model !== null ? { model: input.model } : {}),
|
|
1597
|
+
window_minutes: input.window_minutes,
|
|
1598
|
+
}, accessToken), "Inspection");
|
|
1599
|
+
const envelopeKeys = new Set(["ok", "data", "meta"]);
|
|
1600
|
+
if (Object.keys(envelope).length !== envelopeKeys.size || Object.keys(envelope).some((key) => !envelopeKeys.has(key))) {
|
|
1601
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection response is invalid");
|
|
1602
|
+
}
|
|
1603
|
+
if (envelope.ok !== true)
|
|
1604
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection response is invalid");
|
|
1605
|
+
const data = validateInspectionData(envelope.data);
|
|
1606
|
+
if (data.scope.window_minutes !== input.window_minutes
|
|
1607
|
+
|| (input.model ?? null) !== data.scope.model) {
|
|
1608
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection scope identity is invalid");
|
|
1609
|
+
}
|
|
1610
|
+
const meta = validateResponseObject(envelope.meta, "Inspection metadata");
|
|
1611
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1612
|
+
|| meta.capability !== "inspections.run"
|
|
1613
|
+
|| meta.schema_version !== "1.0"
|
|
1614
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1615
|
+
throw new HttpError("PROTOCOL_ERROR", "Inspection metadata is invalid");
|
|
1616
|
+
}
|
|
1617
|
+
return data;
|
|
1618
|
+
}
|
|
1619
|
+
async inspectionRun(input, accessToken) {
|
|
1620
|
+
return this.inspectionsRun(input, accessToken);
|
|
1621
|
+
}
|
|
1622
|
+
async metricsQuery(input, accessToken) {
|
|
1623
|
+
const asset = validateMetricAsset(input.asset);
|
|
1624
|
+
if (!METRIC_NAMES.has(input.metric)
|
|
1625
|
+
|| !Number.isSafeInteger(input.window_minutes) || input.window_minutes < 5 || input.window_minutes > 1440
|
|
1626
|
+
|| !Number.isSafeInteger(input.max_points) || input.max_points < 1 || input.max_points > 120) {
|
|
1627
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics request is invalid");
|
|
1628
|
+
}
|
|
1629
|
+
const envelope = validateResponseObject(await this.requestJson("POST", "/metrics/query", {
|
|
1630
|
+
asset,
|
|
1631
|
+
metric: input.metric,
|
|
1632
|
+
window_minutes: input.window_minutes,
|
|
1633
|
+
max_points: input.max_points,
|
|
1634
|
+
}, accessToken), "Metrics");
|
|
1635
|
+
if (envelope.ok !== true)
|
|
1636
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics response is invalid");
|
|
1637
|
+
const data = validateResponseObject(envelope.data, "Metrics data");
|
|
1638
|
+
const expectedData = new Set(["target", "metric", "window_minutes", "matched_label", "series", "truncated"]);
|
|
1639
|
+
if (Object.keys(data).length !== expectedData.size || Object.keys(data).some((key) => !expectedData.has(key))) {
|
|
1640
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics data response is invalid");
|
|
1641
|
+
}
|
|
1642
|
+
const target = validateResponseObject(data.target, "Metrics target");
|
|
1643
|
+
if (Object.keys(target).length !== 3 || !Object.hasOwn(target, "asset_id") || !Object.hasOwn(target, "name") || !Object.hasOwn(target, "ip")) {
|
|
1644
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics target is invalid");
|
|
1645
|
+
}
|
|
1646
|
+
if (typeof target.asset_id !== "string" || target.asset_id.length < 1 || target.asset_id.length > 128) {
|
|
1647
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics target id is invalid");
|
|
1648
|
+
}
|
|
1649
|
+
const targetId = isIP(input.asset) > 0
|
|
1650
|
+
? input.asset
|
|
1651
|
+
: validateAssetRequestId(input.asset);
|
|
1652
|
+
if (target.asset_id !== targetId) {
|
|
1653
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics response identity is invalid");
|
|
1654
|
+
}
|
|
1655
|
+
const targetName = target.name === null ? null : validateNullableAssetText(target.name, "metric asset name");
|
|
1656
|
+
const responseIp = canonicalIp(target.ip);
|
|
1657
|
+
if (responseIp === null)
|
|
1658
|
+
throw new HttpError("PROTOCOL_ERROR", "Metric asset ip is invalid");
|
|
1659
|
+
const requestedIp = canonicalIp(input.asset);
|
|
1660
|
+
if (requestedIp !== null && responseIp !== requestedIp) {
|
|
1661
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics response target ip is invalid");
|
|
1662
|
+
}
|
|
1663
|
+
if (data.metric !== input.metric || data.window_minutes !== input.window_minutes || typeof data.truncated !== "boolean") {
|
|
1664
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics response identity is invalid");
|
|
1665
|
+
}
|
|
1666
|
+
const matchedLabel = data.matched_label === null ? null : data.matched_label;
|
|
1667
|
+
if (matchedLabel !== null && (typeof matchedLabel !== "string" || !METRIC_LABELS.has(matchedLabel))) {
|
|
1668
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics matched label is invalid");
|
|
1669
|
+
}
|
|
1670
|
+
if (!Array.isArray(data.series) || data.series.length < 1 || data.series.length > 20) {
|
|
1671
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics series are invalid");
|
|
1672
|
+
}
|
|
1673
|
+
const series = data.series.map((item, index) => validateMetricSeries(item, index, input.max_points));
|
|
1674
|
+
const meta = validateResponseObject(envelope.meta, "Metrics metadata");
|
|
1675
|
+
if (typeof meta.request_id !== "string" || !/^req_[A-Za-z0-9_-]{8,128}$/.test(meta.request_id)
|
|
1676
|
+
|| meta.capability !== "metrics.query"
|
|
1677
|
+
|| meta.schema_version !== "1.0"
|
|
1678
|
+
|| validateGeneratedAt(meta.generated_at) !== meta.generated_at) {
|
|
1679
|
+
throw new HttpError("PROTOCOL_ERROR", "Metrics metadata is invalid");
|
|
1680
|
+
}
|
|
1681
|
+
return {
|
|
1682
|
+
target: { asset_id: targetId, name: targetName, ip: responseIp },
|
|
1683
|
+
metric: input.metric,
|
|
1684
|
+
window_minutes: input.window_minutes,
|
|
1685
|
+
matched_label: matchedLabel,
|
|
1686
|
+
series,
|
|
1687
|
+
truncated: data.truncated,
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
async employeeChat(employeeId, message, conversationId, accessToken, options = {}) {
|
|
1691
|
+
validateEmployeeId(employeeId);
|
|
1692
|
+
if (typeof message !== "string" || message.trim().length === 0 || message.length > 10000 || message.includes("\u0000")) {
|
|
1693
|
+
throw new HttpError("PROTOCOL_ERROR", "message is invalid");
|
|
1694
|
+
}
|
|
1695
|
+
if (conversationId !== undefined)
|
|
1696
|
+
validateChatId(conversationId, "conversation id");
|
|
1697
|
+
const body = JSON.stringify({ message: message.trim(), ...(conversationId ? { conversation_id: conversationId } : {}) });
|
|
1698
|
+
if (Buffer.byteLength(body, "utf8") > MAX_REQUEST_BYTES) {
|
|
1699
|
+
throw new HttpError("PROTOCOL_ERROR", "Request is too large");
|
|
1700
|
+
}
|
|
1701
|
+
const url = integrationUrl(this.baseUrl, `/employees/${employeeId}/chat`);
|
|
1702
|
+
const headers = {
|
|
1703
|
+
accept: "text/event-stream",
|
|
1704
|
+
"cache-control": "no-store",
|
|
1705
|
+
pragma: "no-cache",
|
|
1706
|
+
connection: "close",
|
|
1707
|
+
"content-type": "application/json",
|
|
1708
|
+
"content-length": String(Buffer.byteLength(body, "utf8")),
|
|
1709
|
+
"user-agent": CLI_USER_AGENT,
|
|
1710
|
+
"x-zhihu-cli-version": CLI_VERSION,
|
|
1711
|
+
authorization: `Bearer ${accessToken}`,
|
|
1712
|
+
};
|
|
1713
|
+
const requestConfig = requestOptions(url, this.profile, headers);
|
|
1714
|
+
requestConfig.method = "POST";
|
|
1715
|
+
const parser = new EmployeeChatSseParser(conversationId, {
|
|
1716
|
+
maxTotalBytes: this.maxResponseBytes,
|
|
1717
|
+
maxEventBytes: Math.min(options.maxEventBytes ?? 64 * 1024, this.maxResponseBytes),
|
|
1718
|
+
});
|
|
1719
|
+
return new Promise((resolve, reject) => {
|
|
1720
|
+
let request;
|
|
1721
|
+
let settled = false;
|
|
1722
|
+
let abortListener;
|
|
1723
|
+
const cleanup = () => {
|
|
1724
|
+
if (abortListener && options.signal)
|
|
1725
|
+
options.signal.removeEventListener("abort", abortListener);
|
|
1726
|
+
abortListener = undefined;
|
|
1727
|
+
};
|
|
1728
|
+
const fail = (error) => {
|
|
1729
|
+
if (settled)
|
|
1730
|
+
return;
|
|
1731
|
+
settled = true;
|
|
1732
|
+
cleanup();
|
|
1733
|
+
if (request)
|
|
1734
|
+
request.destroy();
|
|
1735
|
+
reject(error instanceof HttpError ? error : new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1736
|
+
};
|
|
1737
|
+
const emit = (event) => {
|
|
1738
|
+
try {
|
|
1739
|
+
options.onEvent?.(event);
|
|
1740
|
+
}
|
|
1741
|
+
catch {
|
|
1742
|
+
fail(new HttpError("PROTOCOL_ERROR", "Unable to consume chat stream"));
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (event.type === "error") {
|
|
1746
|
+
const message = event.code === "PROTOCOL_ERROR" ? "The server returned an invalid chat stream." : "The server rejected the chat request.";
|
|
1747
|
+
fail(new HttpError(event.code, message, {
|
|
1748
|
+
status: 200,
|
|
1749
|
+
retryable: retryableForCode(event.code, false),
|
|
1750
|
+
}));
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
const handleChunk = (chunk) => {
|
|
1754
|
+
if (settled)
|
|
1755
|
+
return;
|
|
1756
|
+
try {
|
|
1757
|
+
for (const event of parser.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
|
|
1758
|
+
emit(event);
|
|
1759
|
+
}
|
|
1760
|
+
catch (error) {
|
|
1761
|
+
fail(error);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
const protocol = requestConfig.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1765
|
+
try {
|
|
1766
|
+
request = protocol(requestConfig, (response) => {
|
|
1767
|
+
if (settled)
|
|
1768
|
+
return;
|
|
1769
|
+
const statusCode = response.statusCode ?? 0;
|
|
1770
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
1771
|
+
collectResponse(response, this.maxResponseBytes).then((responseBody) => fail(responseError(statusCode, responseBody)), (error) => fail(error));
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const contentType = String(response.headers["content-type"] ?? "").toLowerCase();
|
|
1775
|
+
if (!contentType.includes("text/event-stream")) {
|
|
1776
|
+
fail(new HttpError("PROTOCOL_ERROR", "Chat response is not an SSE stream", { status: statusCode }));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
response.on("data", handleChunk);
|
|
1780
|
+
response.on("end", () => {
|
|
1781
|
+
if (settled)
|
|
1782
|
+
return;
|
|
1783
|
+
try {
|
|
1784
|
+
for (const event of parser.finish())
|
|
1785
|
+
emit(event);
|
|
1786
|
+
if (!settled) {
|
|
1787
|
+
const result = parser.result();
|
|
1788
|
+
settled = true;
|
|
1789
|
+
cleanup();
|
|
1790
|
+
resolve(result);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
catch (error) {
|
|
1794
|
+
fail(error);
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
response.on("error", () => fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true })));
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
catch {
|
|
1801
|
+
fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const timeout = Math.max(1, this.profile.timeout_seconds) * 1000;
|
|
1805
|
+
request.setTimeout(timeout, () => {
|
|
1806
|
+
fail(new HttpError("TIMEOUT", "The request timed out.", { retryable: true }));
|
|
1807
|
+
});
|
|
1808
|
+
request.once("error", (error) => {
|
|
1809
|
+
if (settled)
|
|
1810
|
+
return;
|
|
1811
|
+
if (error.code === "ETIMEDOUT")
|
|
1812
|
+
fail(new HttpError("TIMEOUT", "The request timed out.", { retryable: true }));
|
|
1813
|
+
else
|
|
1814
|
+
fail(new HttpError("NETWORK_ERROR", "Network request failed", { retryable: true }));
|
|
1815
|
+
});
|
|
1816
|
+
abortListener = () => {
|
|
1817
|
+
fail(new HttpError("USER_CANCELLED", "The request was cancelled."));
|
|
1818
|
+
};
|
|
1819
|
+
if (options.signal?.aborted) {
|
|
1820
|
+
abortListener();
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
1824
|
+
request.write(body);
|
|
1825
|
+
request.end();
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
async chat(employeeId, message, conversationId, accessToken, options = {}) {
|
|
1829
|
+
return this.employeeChat(employeeId, message, conversationId, accessToken, options);
|
|
1830
|
+
}
|
|
1831
|
+
async start(codeChallenge) {
|
|
1832
|
+
const result = validateResponseObject(await this.requestJson("POST", "/auth/start", {
|
|
1833
|
+
code_challenge: codeChallenge,
|
|
1834
|
+
code_challenge_method: "S256",
|
|
1835
|
+
}), "Auth start");
|
|
1836
|
+
return {
|
|
1837
|
+
transaction_id: validateCredentialField(result.transaction_id, "transaction_id"),
|
|
1838
|
+
polling_secret: validateCredentialField(result.polling_secret, "polling_secret"),
|
|
1839
|
+
authorization_url: validateCredentialField(result.authorization_url, "authorization_url"),
|
|
1840
|
+
expires_in: validateExpires(result.expires_in),
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
async poll(transactionId, pollingSecret, codeVerifier) {
|
|
1844
|
+
const result = validateResponseObject(await this.requestJson("POST", "/auth/poll", {
|
|
1845
|
+
transaction_id: transactionId,
|
|
1846
|
+
polling_secret: pollingSecret,
|
|
1847
|
+
code_verifier: codeVerifier,
|
|
1848
|
+
}), "Auth poll");
|
|
1849
|
+
const status = validateCredentialField(result.status, "status");
|
|
1850
|
+
const output = { status };
|
|
1851
|
+
if (typeof result.error_code === "string")
|
|
1852
|
+
output.error_code = result.error_code;
|
|
1853
|
+
if (result.access_token !== undefined)
|
|
1854
|
+
output.access_token = validateCredentialField(result.access_token, "access_token");
|
|
1855
|
+
if (result.refresh_credential !== undefined)
|
|
1856
|
+
output.refresh_credential = validateCredentialField(result.refresh_credential, "refresh_credential");
|
|
1857
|
+
if (result.expires_in !== undefined)
|
|
1858
|
+
output.expires_in = validateExpires(result.expires_in);
|
|
1859
|
+
return output;
|
|
1860
|
+
}
|
|
1861
|
+
async refresh(refreshCredential) {
|
|
1862
|
+
const result = validateResponseObject(await this.requestJson("POST", "/auth/refresh", {
|
|
1863
|
+
refresh_credential: refreshCredential,
|
|
1864
|
+
}), "Auth refresh");
|
|
1865
|
+
return {
|
|
1866
|
+
access_token: validateCredentialField(result.access_token, "access_token"),
|
|
1867
|
+
refresh_credential: validateCredentialField(result.refresh_credential, "refresh_credential"),
|
|
1868
|
+
expires_in: validateExpires(result.expires_in),
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
async logout(refreshCredential) {
|
|
1872
|
+
await this.requestJson("POST", "/auth/logout", { refresh_credential: refreshCredential }, undefined, { allowEmpty: true });
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
export { IntegrationHttpClient as HttpClient };
|