@loadstrike/loadstrike-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/cjs/cluster.js +410 -0
- package/dist/cjs/contracts.js +2 -0
- package/dist/cjs/correlation.js +1009 -0
- package/dist/cjs/index.js +71 -0
- package/dist/cjs/local.js +1884 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/reporting.js +1250 -0
- package/dist/cjs/runtime.js +7013 -0
- package/dist/cjs/sinks.js +2675 -0
- package/dist/cjs/transports.js +3695 -0
- package/dist/esm/cluster.js +403 -0
- package/dist/esm/contracts.js +1 -0
- package/dist/esm/correlation.js +999 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/local.js +1844 -0
- package/dist/esm/reporting.js +1241 -0
- package/dist/esm/runtime.js +6992 -0
- package/dist/esm/sinks.js +2657 -0
- package/dist/esm/transports.js +3658 -0
- package/dist/types/cluster.d.ts +112 -0
- package/dist/types/contracts.d.ts +439 -0
- package/dist/types/correlation.d.ts +234 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/local.d.ts +30 -0
- package/dist/types/reporting.d.ts +6 -0
- package/dist/types/runtime.d.ts +1052 -0
- package/dist/types/sinks.d.ts +497 -0
- package/dist/types/transports.d.ts +745 -0
- package/package.json +110 -0
|
@@ -0,0 +1,1884 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.LoadStrikeLocalClient = exports.DEFAULT_LICENSING_API_BASE_URL = void 0;
|
|
40
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
43
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
|
+
const correlation_js_1 = require("./correlation.js");
|
|
45
|
+
const transports_js_1 = require("./transports.js");
|
|
46
|
+
exports.DEFAULT_LICENSING_API_BASE_URL = "https://licensing.loadstrike.com";
|
|
47
|
+
const BUILT_IN_WORKER_PLUGIN_NAMES = new Set([
|
|
48
|
+
"loadstrike failed responses",
|
|
49
|
+
"loadstrike correlation"
|
|
50
|
+
]);
|
|
51
|
+
const SINK_FEATURE_BY_KIND = {
|
|
52
|
+
influxdb: "extensions.reporting_sinks.influxdb",
|
|
53
|
+
timescaledb: "extensions.reporting_sinks.timescaledb",
|
|
54
|
+
grafanaloki: "extensions.reporting_sinks.grafana_loki",
|
|
55
|
+
datadog: "extensions.reporting_sinks.datadog",
|
|
56
|
+
splunk: "extensions.reporting_sinks.splunk",
|
|
57
|
+
otelcollector: "extensions.reporting_sinks.otel_collector"
|
|
58
|
+
};
|
|
59
|
+
const TRACKING_FEATURE_BY_KIND = {
|
|
60
|
+
http: "endpoint.http",
|
|
61
|
+
kafka: "endpoint.kafka",
|
|
62
|
+
rabbitmq: "endpoint.rabbitmq",
|
|
63
|
+
azureeventhubs: "endpoint.azure_event_hubs",
|
|
64
|
+
pushdiffusion: "endpoint.push_diffusion",
|
|
65
|
+
delegatestream: "endpoint.delegate_stream",
|
|
66
|
+
nats: "endpoint.nats",
|
|
67
|
+
redisstreams: "endpoint.redis_streams"
|
|
68
|
+
};
|
|
69
|
+
const CI_ENVIRONMENT_VARIABLES = [
|
|
70
|
+
"GITHUB_ACTIONS",
|
|
71
|
+
"GITLAB_CI",
|
|
72
|
+
"TF_BUILD",
|
|
73
|
+
"BUILD_BUILDID",
|
|
74
|
+
"JENKINS_URL",
|
|
75
|
+
"TEAMCITY_VERSION",
|
|
76
|
+
"CIRCLECI",
|
|
77
|
+
"BUILDKITE",
|
|
78
|
+
"TRAVIS",
|
|
79
|
+
"APPVEYOR",
|
|
80
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
81
|
+
"CODEBUILD_BUILD_ID",
|
|
82
|
+
"DRONE",
|
|
83
|
+
"SEMAPHORE",
|
|
84
|
+
"HEROKU_TEST_RUN_ID"
|
|
85
|
+
];
|
|
86
|
+
class LoadStrikeLocalClient {
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
this.signingKeyCache = new Map();
|
|
89
|
+
assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
|
|
90
|
+
this.licensingApiBaseUrl = normalizeLicensingApiBaseUrl(options.licensingApiBaseUrl);
|
|
91
|
+
this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
|
|
92
|
+
}
|
|
93
|
+
async run(request) {
|
|
94
|
+
const sanitized = sanitizeRequest(request);
|
|
95
|
+
const licenseSession = await this.acquireLicenseLease(sanitized);
|
|
96
|
+
try {
|
|
97
|
+
const createdUtc = new Date();
|
|
98
|
+
const context = asRecord(sanitized.Context);
|
|
99
|
+
const scenarios = Array.isArray(sanitized.Scenarios) ? sanitized.Scenarios : [];
|
|
100
|
+
let allRequestCount = 0;
|
|
101
|
+
let allOkCount = 0;
|
|
102
|
+
let allFailCount = 0;
|
|
103
|
+
const thresholdResults = [];
|
|
104
|
+
const scenarioRows = [];
|
|
105
|
+
for (const [index, scenarioValue] of scenarios.entries()) {
|
|
106
|
+
const scenario = asRecord(scenarioValue);
|
|
107
|
+
const name = stringOrDefault(scenario.Name, "unknown");
|
|
108
|
+
const requestCount = computeScenarioRequestCount(scenario);
|
|
109
|
+
const startedMs = Date.now();
|
|
110
|
+
const scenarioOutcome = await evaluateScenarioOutcome(scenario, requestCount, context);
|
|
111
|
+
const durationMs = Math.max(Date.now() - startedMs, 0);
|
|
112
|
+
const okCount = scenarioOutcome.okCount;
|
|
113
|
+
const failCount = scenarioOutcome.failCount;
|
|
114
|
+
const actualRequestCount = scenarioOutcome.requestCount;
|
|
115
|
+
allRequestCount += actualRequestCount;
|
|
116
|
+
allOkCount += okCount;
|
|
117
|
+
allFailCount += failCount;
|
|
118
|
+
thresholdResults.push(...evaluateScenarioThresholds(scenario, {
|
|
119
|
+
scenarioName: name,
|
|
120
|
+
requestCount: actualRequestCount,
|
|
121
|
+
okCount,
|
|
122
|
+
failCount,
|
|
123
|
+
durationMs
|
|
124
|
+
}));
|
|
125
|
+
scenarioRows.push(buildScenarioStatsContractRow(name, index, actualRequestCount, okCount, failCount, durationMs));
|
|
126
|
+
}
|
|
127
|
+
const completedUtc = new Date();
|
|
128
|
+
return {
|
|
129
|
+
CompletedUtc: completedUtc,
|
|
130
|
+
Stats: buildNodeStatsContract(context, createdUtc, completedUtc, {
|
|
131
|
+
allRequestCount,
|
|
132
|
+
allOkCount,
|
|
133
|
+
allFailCount,
|
|
134
|
+
scenarios: scenarioRows,
|
|
135
|
+
thresholds: thresholdResults
|
|
136
|
+
})
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await this.releaseLicenseLease(licenseSession, sanitized);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async acquireLicenseLease(request) {
|
|
144
|
+
return this.validateLicenseIfRequired(request);
|
|
145
|
+
}
|
|
146
|
+
async releaseLicenseLease(session, request) {
|
|
147
|
+
await this.stopLicenseLeaseIfRequired(session, request);
|
|
148
|
+
}
|
|
149
|
+
async validateLicenseIfRequired(request) {
|
|
150
|
+
const context = asRecord(request.Context);
|
|
151
|
+
const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
|
|
152
|
+
if (nodeType === "agent") {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
const runnerKey = stringOrDefault(context.RunnerKey, "").trim();
|
|
156
|
+
if (!runnerKey) {
|
|
157
|
+
throw new Error("Runner key is required. Call WithRunnerKey(...) before Run().");
|
|
158
|
+
}
|
|
159
|
+
const sessionId = stringOrDefault(context.SessionId, "").trim() || generateSessionId();
|
|
160
|
+
context.SessionId = sessionId;
|
|
161
|
+
const machineName = node_os_1.default.hostname();
|
|
162
|
+
const environmentClassification = detectEnvironmentClassification(nodeType);
|
|
163
|
+
const computedDeviceHash = deviceHash();
|
|
164
|
+
const requestedFeatures = collectRequestedFeatures(request);
|
|
165
|
+
const payload = {
|
|
166
|
+
RunnerKey: runnerKey,
|
|
167
|
+
RequestedFeatures: requestedFeatures,
|
|
168
|
+
SessionId: sessionId,
|
|
169
|
+
TestSuite: stringOrDefault(context.TestSuite, ""),
|
|
170
|
+
TestName: stringOrDefault(context.TestName, ""),
|
|
171
|
+
NodeType: stringOrDefault(context.NodeType, nodeType),
|
|
172
|
+
MachineName: machineName,
|
|
173
|
+
EnvironmentClassification: environmentClassification,
|
|
174
|
+
DeviceHash: computedDeviceHash
|
|
175
|
+
};
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
const timer = setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs);
|
|
178
|
+
try {
|
|
179
|
+
const { response, json } = await this.postLicensingRequest("/api/v1/licenses/validate", payload, controller.signal);
|
|
180
|
+
if (!response.ok || json.IsValid !== true) {
|
|
181
|
+
const denialCode = stringOrDefault(json.DenialCode, "unknown_denial");
|
|
182
|
+
const details = stringOrDefault(json.Message, "No additional details provided.");
|
|
183
|
+
throw new Error(`Runner key validation denied for '${runnerKey}'. Reason: ${denialCode}. ${details}`);
|
|
184
|
+
}
|
|
185
|
+
const runToken = stringOrDefault(pickValue(json, "RunToken", "SignedRunToken", "Token", "runToken"), "").trim();
|
|
186
|
+
if (!runToken) {
|
|
187
|
+
throw new Error("Runner key validation failed: run token is missing.");
|
|
188
|
+
}
|
|
189
|
+
await this.verifySignedRunToken(runToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
|
|
190
|
+
const heartbeatIntervalSeconds = Math.max(asInt(pickValue(json, "HeartbeatIntervalSeconds", "heartbeatIntervalSeconds")), 1);
|
|
191
|
+
const heartbeatTimer = setInterval(() => {
|
|
192
|
+
void this.sendRunTokenHeartbeat({
|
|
193
|
+
runToken,
|
|
194
|
+
sessionId,
|
|
195
|
+
deviceHash: computedDeviceHash,
|
|
196
|
+
machineName,
|
|
197
|
+
environmentClassification
|
|
198
|
+
}).catch(() => {
|
|
199
|
+
// Best-effort heartbeat: server-side lease expiration is authoritative.
|
|
200
|
+
});
|
|
201
|
+
}, heartbeatIntervalSeconds * 1000);
|
|
202
|
+
if (typeof heartbeatTimer.unref === "function") {
|
|
203
|
+
heartbeatTimer.unref();
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
runToken,
|
|
207
|
+
sessionId,
|
|
208
|
+
deviceHash: computedDeviceHash,
|
|
209
|
+
machineName,
|
|
210
|
+
environmentClassification,
|
|
211
|
+
heartbeatTimer
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async verifySignedRunToken(runToken, request, requestedFeatures, runnerKey, sessionId, deviceHashValue) {
|
|
219
|
+
const parsed = parseJwt(runToken);
|
|
220
|
+
const keyId = stringOrDefault(parsed.header.kid, "default");
|
|
221
|
+
const algorithm = stringOrDefault(parsed.header.alg, "RS256").toUpperCase();
|
|
222
|
+
if (algorithm !== "RS256") {
|
|
223
|
+
throw new Error("Runner key validation denied. DenialCode=invalid_run_token_signature, Message=Invalid run token signature.");
|
|
224
|
+
}
|
|
225
|
+
const keyRecord = await this.getSigningKey(keyId, algorithm);
|
|
226
|
+
if (!verifyJwtSignature(runToken, keyRecord, algorithm)) {
|
|
227
|
+
throw new Error("Runner key validation denied. DenialCode=invalid_run_token_signature, Message=Invalid run token signature.");
|
|
228
|
+
}
|
|
229
|
+
enforceJwtLifetime(parsed.payload);
|
|
230
|
+
const tokenIssuer = stringOrDefault(parsed.payload.iss, "").trim();
|
|
231
|
+
if (!keyRecord.issuer || tokenIssuer !== keyRecord.issuer) {
|
|
232
|
+
throw new Error("Runner key validation failed: run token issuer does not match.");
|
|
233
|
+
}
|
|
234
|
+
const rawAudience = parsed.payload.aud;
|
|
235
|
+
const tokenAudiences = Array.isArray(rawAudience)
|
|
236
|
+
? rawAudience.map((entry) => stringOrDefault(entry, "").trim()).filter((entry) => entry.length > 0)
|
|
237
|
+
: stringOrDefault(rawAudience, "").trim()
|
|
238
|
+
? [stringOrDefault(rawAudience, "").trim()]
|
|
239
|
+
: [];
|
|
240
|
+
if (!keyRecord.audience || !tokenAudiences.includes(keyRecord.audience)) {
|
|
241
|
+
throw new Error("Runner key validation failed: run token audience does not match.");
|
|
242
|
+
}
|
|
243
|
+
const tokenRunnerKey = stringOrDefault(parsed.payload.runner_key, "").trim();
|
|
244
|
+
if (tokenRunnerKey !== runnerKey) {
|
|
245
|
+
throw new Error("Runner key validation failed: run token runner key does not match.");
|
|
246
|
+
}
|
|
247
|
+
const tokenSessionId = stringOrDefault(parsed.payload.session_id, "").trim();
|
|
248
|
+
if (tokenSessionId !== sessionId) {
|
|
249
|
+
throw new Error("Runner key validation failed: run token session id does not match.");
|
|
250
|
+
}
|
|
251
|
+
const tokenDeviceHash = stringOrDefault(parsed.payload.device_hash, "").trim();
|
|
252
|
+
if (tokenDeviceHash !== deviceHashValue) {
|
|
253
|
+
throw new Error("Runner key validation failed: run token device hash does not match.");
|
|
254
|
+
}
|
|
255
|
+
enforceEntitlementClaims(parsed.payload, requestedFeatures);
|
|
256
|
+
enforceRuntimePolicyClaims(parsed.payload, request);
|
|
257
|
+
}
|
|
258
|
+
async getSigningKey(keyId, algorithm) {
|
|
259
|
+
const nowMs = Date.now();
|
|
260
|
+
const cacheKey = this.licensingApiBaseUrl.toLowerCase();
|
|
261
|
+
const cached = this.signingKeyCache.get(cacheKey);
|
|
262
|
+
if (cached && cached.expiresAtUtc > nowMs) {
|
|
263
|
+
return cached;
|
|
264
|
+
}
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const timer = setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs);
|
|
267
|
+
try {
|
|
268
|
+
const { response, json } = await this.getLicensingRequest("/api/v1/licenses/signing-public-key", controller.signal);
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
throw new Error(`Failed to resolve licensing signing key. status=${response.status}`);
|
|
271
|
+
}
|
|
272
|
+
const resolvedAlgorithm = stringOrDefault(pickValue(json, "Algorithm", "alg"), algorithm).toUpperCase();
|
|
273
|
+
const publicKeyPem = stringOrDefault(pickValue(json, "PublicKeyPem", "PublicKey", "Pem"), "");
|
|
274
|
+
if (!publicKeyPem.trim()) {
|
|
275
|
+
throw new Error("Runner key validation failed: signing key response is empty.");
|
|
276
|
+
}
|
|
277
|
+
const keyRecord = {
|
|
278
|
+
keyId: stringOrDefault(pickValue(json, "KeyId", "Kid", "kid"), keyId),
|
|
279
|
+
algorithm: resolvedAlgorithm,
|
|
280
|
+
issuer: stringOrDefault(pickValue(json, "Issuer", "issuer"), ""),
|
|
281
|
+
audience: stringOrDefault(pickValue(json, "Audience", "audience"), ""),
|
|
282
|
+
publicKeyPem,
|
|
283
|
+
expiresAtUtc: nowMs + (15 * 60 * 1000)
|
|
284
|
+
};
|
|
285
|
+
this.signingKeyCache.set(cacheKey, keyRecord);
|
|
286
|
+
return keyRecord;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async sendRunTokenHeartbeat(payload, signal) {
|
|
293
|
+
const heartbeatPayload = {
|
|
294
|
+
RunToken: payload.runToken,
|
|
295
|
+
SessionId: payload.sessionId,
|
|
296
|
+
DeviceHash: payload.deviceHash,
|
|
297
|
+
MachineName: payload.machineName,
|
|
298
|
+
EnvironmentClassification: payload.environmentClassification
|
|
299
|
+
};
|
|
300
|
+
const controller = signal ? null : new AbortController();
|
|
301
|
+
const timer = controller
|
|
302
|
+
? setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs)
|
|
303
|
+
: null;
|
|
304
|
+
const { response } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
|
|
305
|
+
if (timer) {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
}
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
throw new Error(`Runner key validation denied. DenialCode=run_token_heartbeat_failed, Message=Run token heartbeat failed with status ${response.status}.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async stopLicenseLeaseIfRequired(session, _request) {
|
|
313
|
+
if (session.heartbeatTimer) {
|
|
314
|
+
clearInterval(session.heartbeatTimer);
|
|
315
|
+
}
|
|
316
|
+
if (!session.runToken) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const payload = {
|
|
320
|
+
RunToken: session.runToken,
|
|
321
|
+
SessionId: session.sessionId ?? "",
|
|
322
|
+
DeviceHash: session.deviceHash ?? ""
|
|
323
|
+
};
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const timer = setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs);
|
|
326
|
+
try {
|
|
327
|
+
await this.postLicensingRequest("/api/v1/licenses/stop", payload, controller.signal);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Stop is best-effort for local execution.
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async postLicensingRequest(path, payload, signal) {
|
|
337
|
+
const response = await fetch(`${this.licensingApiBaseUrl.replace(/\/+$/, "")}${path}`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: {
|
|
340
|
+
"Content-Type": "application/json",
|
|
341
|
+
Accept: "application/json"
|
|
342
|
+
},
|
|
343
|
+
body: JSON.stringify(payload),
|
|
344
|
+
signal
|
|
345
|
+
});
|
|
346
|
+
let json = {};
|
|
347
|
+
try {
|
|
348
|
+
json = (await response.json());
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
json = {};
|
|
352
|
+
}
|
|
353
|
+
return { response, json };
|
|
354
|
+
}
|
|
355
|
+
async getLicensingRequest(path, signal) {
|
|
356
|
+
const response = await fetch(`${this.licensingApiBaseUrl.replace(/\/+$/, "")}${path}`, {
|
|
357
|
+
method: "GET",
|
|
358
|
+
headers: {
|
|
359
|
+
Accept: "application/json"
|
|
360
|
+
},
|
|
361
|
+
signal
|
|
362
|
+
});
|
|
363
|
+
let json = {};
|
|
364
|
+
try {
|
|
365
|
+
json = (await response.json());
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
json = {};
|
|
369
|
+
}
|
|
370
|
+
return { response, json };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
exports.LoadStrikeLocalClient = LoadStrikeLocalClient;
|
|
374
|
+
function assertNoDisableLicenseEnforcementOption(value, source) {
|
|
375
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const key of Object.keys(value)) {
|
|
379
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
380
|
+
if (normalized === "disablelicenseenforcement") {
|
|
381
|
+
throw new TypeError(`${source} cannot include disable license enforcement options.`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function sanitizeRequest(request) {
|
|
386
|
+
const context = request.Context ?? {};
|
|
387
|
+
const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
|
|
388
|
+
const runArgs = Array.isArray(request.RunArgs) ? request.RunArgs : [];
|
|
389
|
+
return {
|
|
390
|
+
Context: context,
|
|
391
|
+
Scenarios: scenarios,
|
|
392
|
+
RunArgs: runArgs
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function normalizeLicensingApiBaseUrl(value) {
|
|
396
|
+
const normalized = (value ?? "").trim();
|
|
397
|
+
return normalized || exports.DEFAULT_LICENSING_API_BASE_URL;
|
|
398
|
+
}
|
|
399
|
+
function normalizeTimeoutMs(value) {
|
|
400
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
401
|
+
return 10000;
|
|
402
|
+
}
|
|
403
|
+
return Math.trunc(value);
|
|
404
|
+
}
|
|
405
|
+
function generateSessionId() {
|
|
406
|
+
return (0, node_crypto_1.randomUUID)().replace(/-/g, "");
|
|
407
|
+
}
|
|
408
|
+
function collectRequestedFeatures(request) {
|
|
409
|
+
const context = asRecord(request.Context);
|
|
410
|
+
const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
|
|
411
|
+
const features = new Set([
|
|
412
|
+
"core.runtime",
|
|
413
|
+
"reporting.formats.standard"
|
|
414
|
+
]);
|
|
415
|
+
if (toBoolean(pickValue(context, "LocalDevClusterEnabled", "localDevClusterEnabled"), false)) {
|
|
416
|
+
features.add("cluster.local_dev");
|
|
417
|
+
}
|
|
418
|
+
if (stringOrDefault(pickValue(context, "NatsServerUrl", "natsServerUrl"), "").trim()) {
|
|
419
|
+
features.add("cluster.distributed.nats");
|
|
420
|
+
}
|
|
421
|
+
const hasAdvancedTargeting = normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")).length > 0
|
|
422
|
+
|| normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios")).length > 0
|
|
423
|
+
|| resolveTimeoutSeconds(context, "ClusterCommandTimeoutSeconds", "clusterCommandTimeoutSeconds", "ClusterCommandTimeoutMs", "clusterCommandTimeoutMs", 120) !== 120;
|
|
424
|
+
if (hasAdvancedTargeting) {
|
|
425
|
+
features.add("cluster.targeting_and_tuning.advanced");
|
|
426
|
+
}
|
|
427
|
+
if (countCustomWorkerPlugins(context) > 0) {
|
|
428
|
+
features.add("extensions.worker_plugins.custom");
|
|
429
|
+
}
|
|
430
|
+
const reportingSinks = asList(pickValue(context, "ReportingSinks", "reportingSinks"));
|
|
431
|
+
let hasCustomSink = false;
|
|
432
|
+
for (const sink of reportingSinks) {
|
|
433
|
+
const sinkRecord = asRecord(sink);
|
|
434
|
+
const runtimeSink = isRuntimeReportingSink(sink);
|
|
435
|
+
const explicitFeature = stringOrDefault(pickValue(sinkRecord, "LicenseFeature", "licenseFeature", "Feature", "feature"), stringOrDefault(sink.licenseFeature, stringOrDefault(sink.LicenseFeature, ""))).trim();
|
|
436
|
+
if (explicitFeature) {
|
|
437
|
+
features.add(explicitFeature);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (!runtimeSink) {
|
|
441
|
+
const sinkKind = stringOrDefault(pickValue(sinkRecord, "Kind", "kind"), "").trim();
|
|
442
|
+
const normalizedKind = sinkKind.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
443
|
+
const mappedKindFeature = SINK_FEATURE_BY_KIND[normalizedKind];
|
|
444
|
+
if (mappedKindFeature) {
|
|
445
|
+
features.add(mappedKindFeature);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
hasCustomSink = true;
|
|
450
|
+
}
|
|
451
|
+
if (hasCustomSink) {
|
|
452
|
+
features.add("extensions.reporting_sinks.custom");
|
|
453
|
+
}
|
|
454
|
+
if (pickValue(context, "ReportFinalizer", "reportFinalizer") != null) {
|
|
455
|
+
features.add("policy.report_finalizer_and_threshold_controls");
|
|
456
|
+
}
|
|
457
|
+
for (const scenarioValue of scenarios) {
|
|
458
|
+
const scenario = asRecord(scenarioValue);
|
|
459
|
+
for (const feature of normalizeStringArray(pickValue(scenario, "LicenseFeatures", "licenseFeatures"))) {
|
|
460
|
+
features.add(feature);
|
|
461
|
+
}
|
|
462
|
+
if (asList(scenario.Thresholds).length > 0) {
|
|
463
|
+
features.add("policy.report_finalizer_and_threshold_controls");
|
|
464
|
+
}
|
|
465
|
+
const tracking = asRecord(scenario.Tracking);
|
|
466
|
+
const source = asRecord(tracking.Source);
|
|
467
|
+
const destination = asRecord(tracking.Destination);
|
|
468
|
+
if (!Object.keys(source).length && !Object.keys(destination).length) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const sourceKind = stringOrDefault(pickValue(source, "Kind", "kind"), "").trim().toLowerCase();
|
|
472
|
+
if (sourceKind && TRACKING_FEATURE_BY_KIND[sourceKind]) {
|
|
473
|
+
features.add(TRACKING_FEATURE_BY_KIND[sourceKind]);
|
|
474
|
+
}
|
|
475
|
+
const destinationKind = stringOrDefault(pickValue(destination, "Kind", "kind"), "").trim().toLowerCase();
|
|
476
|
+
if (destinationKind && TRACKING_FEATURE_BY_KIND[destinationKind]) {
|
|
477
|
+
features.add(TRACKING_FEATURE_BY_KIND[destinationKind]);
|
|
478
|
+
}
|
|
479
|
+
else if (Object.keys(source).length > 0) {
|
|
480
|
+
features.add("crossplatform.source_only_reporting");
|
|
481
|
+
}
|
|
482
|
+
features.add("crossplatform.tracking.selectors");
|
|
483
|
+
features.add("reporting.correlation.grouped");
|
|
484
|
+
const correlationStore = asRecord(pickValue(tracking, "CorrelationStore", "correlationStore"));
|
|
485
|
+
const storeKind = stringOrDefault(pickValue(correlationStore, "Kind", "kind"), "").trim().toLowerCase();
|
|
486
|
+
if (storeKind === "redis") {
|
|
487
|
+
features.add("correlation.store.redis");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return Array.from(features)
|
|
491
|
+
.map((feature) => feature.trim().toLowerCase())
|
|
492
|
+
.filter((feature) => feature.length > 0)
|
|
493
|
+
.sort((left, right) => left.localeCompare(right));
|
|
494
|
+
}
|
|
495
|
+
function mapEnvironmentClassification(value) {
|
|
496
|
+
const normalized = value.trim().toLowerCase();
|
|
497
|
+
if (normalized === "ci") {
|
|
498
|
+
return 1;
|
|
499
|
+
}
|
|
500
|
+
if (normalized === "kubernetes") {
|
|
501
|
+
return 2;
|
|
502
|
+
}
|
|
503
|
+
if (normalized === "clustercontroller") {
|
|
504
|
+
return 3;
|
|
505
|
+
}
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
function detectEnvironmentClassification(nodeType) {
|
|
509
|
+
if (isCiHost()) {
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
if (isKubernetesHost()) {
|
|
513
|
+
return 2;
|
|
514
|
+
}
|
|
515
|
+
if (isLocalHost()) {
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
return nodeType === "coordinator" ? 3 : 1;
|
|
519
|
+
}
|
|
520
|
+
function isCiHost() {
|
|
521
|
+
if (hasTruthyEnv("CI")) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
return CI_ENVIRONMENT_VARIABLES.some((name) => hasNonBlankEnv(name));
|
|
525
|
+
}
|
|
526
|
+
function isKubernetesHost() {
|
|
527
|
+
return hasNonBlankEnv("KUBERNETES_SERVICE_HOST")
|
|
528
|
+
|| hasNonBlankEnv("KUBERNETES_PORT")
|
|
529
|
+
|| fs.existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token");
|
|
530
|
+
}
|
|
531
|
+
function isLocalHost() {
|
|
532
|
+
if (process.platform === "win32") {
|
|
533
|
+
return !isWindowsServerProduct(readWindowsProductName());
|
|
534
|
+
}
|
|
535
|
+
if (process.platform === "darwin") {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (process.platform === "linux") {
|
|
539
|
+
return isLinuxLocalHost();
|
|
540
|
+
}
|
|
541
|
+
const osDescription = resolveOsDescription().toLowerCase();
|
|
542
|
+
return osDescription.includes("android")
|
|
543
|
+
|| osDescription.includes("ios")
|
|
544
|
+
|| osDescription.includes("iphone")
|
|
545
|
+
|| osDescription.includes("ipad");
|
|
546
|
+
}
|
|
547
|
+
function isLinuxLocalHost() {
|
|
548
|
+
if (isWslHost()) {
|
|
549
|
+
return !isWindowsServerProduct(readWslWindowsProductName());
|
|
550
|
+
}
|
|
551
|
+
if (isContainerHost()) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
const osDescription = resolveOsDescription().toLowerCase();
|
|
555
|
+
if (osDescription.includes("android")) {
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
return hasLinuxDesktopSession();
|
|
559
|
+
}
|
|
560
|
+
function isContainerHost() {
|
|
561
|
+
if (hasTruthyEnv("container")) {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
if (fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv")) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
return fileContains("/proc/1/cgroup", ["docker", "containerd", "podman", "kubepods", "lxc"])
|
|
568
|
+
|| fileContains("/proc/self/cgroup", ["docker", "containerd", "podman", "kubepods", "lxc"]);
|
|
569
|
+
}
|
|
570
|
+
function isWslHost() {
|
|
571
|
+
return hasNonBlankEnv("WSL_INTEROP")
|
|
572
|
+
|| hasNonBlankEnv("WSL_DISTRO_NAME")
|
|
573
|
+
|| fileContains("/proc/version", ["microsoft"])
|
|
574
|
+
|| fileContains("/proc/sys/kernel/osrelease", ["microsoft"]);
|
|
575
|
+
}
|
|
576
|
+
function hasLinuxDesktopSession() {
|
|
577
|
+
if (["DISPLAY", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP", "DESKTOP_SESSION", "MIR_SOCKET"].some((name) => hasNonBlankEnv(name))) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
const sessionType = stringOrDefault(process.env.XDG_SESSION_TYPE, "").trim().toLowerCase();
|
|
581
|
+
return sessionType === "x11" || sessionType === "wayland";
|
|
582
|
+
}
|
|
583
|
+
function hasNonBlankEnv(name) {
|
|
584
|
+
const value = process.env[name];
|
|
585
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
586
|
+
}
|
|
587
|
+
function hasTruthyEnv(name) {
|
|
588
|
+
const value = process.env[name];
|
|
589
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const normalized = value.trim().toLowerCase();
|
|
593
|
+
return normalized !== "0" && normalized !== "false" && normalized !== "no";
|
|
594
|
+
}
|
|
595
|
+
function fileContains(path, markers) {
|
|
596
|
+
try {
|
|
597
|
+
if (!fs.existsSync(path)) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const content = fs.readFileSync(path, "utf8");
|
|
601
|
+
return markers.some((marker) => content.toLowerCase().includes(marker.toLowerCase()));
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function isWindowsServerProduct(value) {
|
|
608
|
+
return value.trim().toLowerCase().includes("server");
|
|
609
|
+
}
|
|
610
|
+
function resolveOsDescription() {
|
|
611
|
+
const osVersion = node_os_1.default.version;
|
|
612
|
+
return typeof osVersion === "function"
|
|
613
|
+
? osVersion.call(node_os_1.default)
|
|
614
|
+
: `${node_os_1.default.type()} ${node_os_1.default.release()}`;
|
|
615
|
+
}
|
|
616
|
+
function deviceHash() {
|
|
617
|
+
const components = [];
|
|
618
|
+
const osDescription = resolveOsDescription();
|
|
619
|
+
addIfNotBlank(components, osDescription);
|
|
620
|
+
addIfNotBlank(components, node_os_1.default.arch());
|
|
621
|
+
addIfNotBlank(components, node_os_1.default.hostname());
|
|
622
|
+
if (process.platform === "win32") {
|
|
623
|
+
addIfNotBlank(components, readWindowsMachineGuid());
|
|
624
|
+
}
|
|
625
|
+
else if (process.platform === "linux") {
|
|
626
|
+
addIfNotBlank(components, readLinuxMachineId());
|
|
627
|
+
}
|
|
628
|
+
else if (process.platform === "darwin") {
|
|
629
|
+
addIfNotBlank(components, readMacPlatformUuid());
|
|
630
|
+
}
|
|
631
|
+
if (!components.length) {
|
|
632
|
+
components.push(node_os_1.default.hostname());
|
|
633
|
+
}
|
|
634
|
+
const raw = components
|
|
635
|
+
.map((value) => value.trim().toLowerCase())
|
|
636
|
+
.filter((value) => value.length > 0)
|
|
637
|
+
.join("|");
|
|
638
|
+
return (0, node_crypto_1.createHash)("sha256").update(raw).digest("hex");
|
|
639
|
+
}
|
|
640
|
+
function addIfNotBlank(values, value) {
|
|
641
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
642
|
+
values.push(value);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function readWindowsMachineGuid() {
|
|
646
|
+
try {
|
|
647
|
+
const output = childProcess.execFileSync("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"], {
|
|
648
|
+
encoding: "utf8",
|
|
649
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
650
|
+
});
|
|
651
|
+
const match = output.match(/MachineGuid\s+REG_\w+\s+([^\r\n]+)/i);
|
|
652
|
+
return match?.[1]?.trim() ?? "";
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return "";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function readWindowsProductName() {
|
|
659
|
+
try {
|
|
660
|
+
const output = childProcess.execFileSync("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "/v", "ProductName"], {
|
|
661
|
+
encoding: "utf8",
|
|
662
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
663
|
+
});
|
|
664
|
+
const match = output.match(/ProductName\s+REG_\w+\s+([^\r\n]+)/i);
|
|
665
|
+
return match?.[1]?.trim() ?? "";
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return "";
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function readLinuxMachineId() {
|
|
672
|
+
for (const path of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
673
|
+
try {
|
|
674
|
+
if (!fs.existsSync(path)) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const value = fs.readFileSync(path, "utf8").trim();
|
|
678
|
+
if (value.length > 0) {
|
|
679
|
+
return value;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
// Ignore fallback failure.
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return "";
|
|
687
|
+
}
|
|
688
|
+
function readMacPlatformUuid() {
|
|
689
|
+
try {
|
|
690
|
+
const output = childProcess.execFileSync("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
691
|
+
encoding: "utf8",
|
|
692
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
693
|
+
});
|
|
694
|
+
const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
695
|
+
return match?.[1]?.trim() ?? "";
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return "";
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function readWslWindowsProductName() {
|
|
702
|
+
try {
|
|
703
|
+
const output = childProcess.execFileSync("cmd.exe", ["/c", "reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "/v", "ProductName"], {
|
|
704
|
+
encoding: "utf8",
|
|
705
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
706
|
+
});
|
|
707
|
+
const match = output.match(/ProductName\s+REG_\w+\s+([^\r\n]+)/i);
|
|
708
|
+
return match?.[1]?.trim() ?? "";
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return "";
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function evaluateScenarioOutcome(scenario, requestCount, context) {
|
|
715
|
+
const tracking = asRecord(scenario.Tracking);
|
|
716
|
+
const sourceSpec = asRecord(tracking.Source);
|
|
717
|
+
if (!Object.keys(sourceSpec).length) {
|
|
718
|
+
return { requestCount, okCount: requestCount, failCount: 0 };
|
|
719
|
+
}
|
|
720
|
+
const sourceEndpoint = mapEndpointSpec(sourceSpec);
|
|
721
|
+
const destinationSpec = asRecord(tracking.Destination);
|
|
722
|
+
const hasDestination = Object.keys(destinationSpec).length > 0;
|
|
723
|
+
const destinationEndpoint = hasDestination ? mapEndpointSpec(destinationSpec) : null;
|
|
724
|
+
validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndpoint);
|
|
725
|
+
const sourceAdapter = transports_js_1.EndpointAdapterFactory.create(sourceEndpoint);
|
|
726
|
+
const destinationAdapter = destinationEndpoint ? transports_js_1.EndpointAdapterFactory.create(destinationEndpoint) : null;
|
|
727
|
+
const correlationTimeoutOverride = pickValue(tracking, "CorrelationTimeoutMs", "correlationTimeoutMs");
|
|
728
|
+
const correlationTimeoutSeconds = pickValue(tracking, "CorrelationTimeoutSeconds", "correlationTimeoutSeconds", "CorrelationTimeout");
|
|
729
|
+
const timeoutMs = correlationTimeoutOverride != null && String(correlationTimeoutOverride).trim() !== ""
|
|
730
|
+
? asInt(correlationTimeoutOverride)
|
|
731
|
+
: Math.trunc((correlationTimeoutSeconds == null || String(correlationTimeoutSeconds).trim() === ""
|
|
732
|
+
? 30
|
|
733
|
+
: asNumber(correlationTimeoutSeconds)) * 1000);
|
|
734
|
+
const timeoutCountsAsFailure = toBoolean(pickValue(tracking, "TimeoutCountsAsFailure", "timeoutCountsAsFailure"), true);
|
|
735
|
+
const correlationStore = mapCorrelationStore(tracking, buildTrackingRunNamespace(stringOrDefault(pickValue(context, "SessionId", "sessionId"), "session"), stringOrDefault(pickValue(scenario, "Name", "name"), "scenario"), sourceEndpoint.name, destinationEndpoint?.name));
|
|
736
|
+
const timeoutSweepIntervalOverride = pickValue(tracking, "TimeoutSweepIntervalMs", "timeoutSweepIntervalMs");
|
|
737
|
+
const timeoutSweepIntervalSeconds = pickValue(tracking, "TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds");
|
|
738
|
+
const timeoutSweepIntervalMs = timeoutSweepIntervalOverride != null && String(timeoutSweepIntervalOverride).trim() !== ""
|
|
739
|
+
? asInt(timeoutSweepIntervalOverride)
|
|
740
|
+
: Math.trunc((timeoutSweepIntervalSeconds == null || String(timeoutSweepIntervalSeconds).trim() === ""
|
|
741
|
+
? 1
|
|
742
|
+
: asNumber(timeoutSweepIntervalSeconds)) * 1000);
|
|
743
|
+
const timeoutBatchSizeValue = pickValue(tracking, "TimeoutBatchSize", "timeoutBatchSize");
|
|
744
|
+
const timeoutBatchSize = timeoutBatchSizeValue == null || String(timeoutBatchSizeValue).trim() === ""
|
|
745
|
+
? 200
|
|
746
|
+
: asInt(timeoutBatchSizeValue);
|
|
747
|
+
const runtime = new correlation_js_1.CrossPlatformTrackingRuntime({
|
|
748
|
+
sourceTrackingField: sourceEndpoint.trackingField,
|
|
749
|
+
destinationTrackingField: destinationEndpoint?.trackingField,
|
|
750
|
+
destinationGatherByField: destinationEndpoint?.gatherByField,
|
|
751
|
+
correlationTimeoutMs: timeoutMs,
|
|
752
|
+
timeoutCountsAsFailure,
|
|
753
|
+
store: correlationStore ?? undefined
|
|
754
|
+
});
|
|
755
|
+
const runMode = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
|
|
756
|
+
const sourceOnlyMode = !destinationEndpoint;
|
|
757
|
+
const restartOnFail = toBoolean(pickValue(scenario, "RestartIterationOnFail", "restartIterationOnFail"), false);
|
|
758
|
+
const restartMaxAttempts = Math.max(asInt(pickValue(context, "RestartIterationMaxAttempts", "restartIterationMaxAttempts")), 0);
|
|
759
|
+
const maxFailCount = Math.max(asInt(pickValue(scenario, "MaxFailCount", "maxFailCount")), 0);
|
|
760
|
+
const scenarioCompletionTimeoutSeconds = Math.max(asNumber(pickValue(context, "ScenarioCompletionTimeoutSeconds", "scenarioCompletionTimeoutSeconds")), 0);
|
|
761
|
+
const scenarioDeadlineMs = scenarioCompletionTimeoutSeconds > 0
|
|
762
|
+
? Date.now() + Math.trunc(scenarioCompletionTimeoutSeconds * 1000)
|
|
763
|
+
: Number.POSITIVE_INFINITY;
|
|
764
|
+
let okCount = 0;
|
|
765
|
+
let failCount = 0;
|
|
766
|
+
let nowMs = Date.now();
|
|
767
|
+
let processedIterations = 0;
|
|
768
|
+
let lastSweepAtMs = nowMs;
|
|
769
|
+
for (let i = 0; i < requestCount; i += 1) {
|
|
770
|
+
if (Date.now() > scenarioDeadlineMs) {
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
let iterationComplete = false;
|
|
774
|
+
let attempts = 0;
|
|
775
|
+
const maxAttempts = 1 + (restartOnFail ? restartMaxAttempts : 0);
|
|
776
|
+
while (!iterationComplete) {
|
|
777
|
+
attempts += 1;
|
|
778
|
+
if (Date.now() > scenarioDeadlineMs) {
|
|
779
|
+
iterationComplete = true;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
let sourcePayload = runMode === "correlateexistingtraffic"
|
|
783
|
+
? await sourceAdapter.consume()
|
|
784
|
+
: await sourceAdapter.produce();
|
|
785
|
+
sourcePayload = normalizePayload(sourcePayload, sourceEndpoint, i);
|
|
786
|
+
const sourceTrackingId = sourceOnlyMode
|
|
787
|
+
? readTrackingId(sourcePayload, sourceEndpoint.trackingField)
|
|
788
|
+
: await runtime.onSourceProduced(sourcePayload, nowMs);
|
|
789
|
+
if (!sourceTrackingId) {
|
|
790
|
+
if (restartOnFail && attempts < maxAttempts) {
|
|
791
|
+
nowMs += 1;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
failCount += 1;
|
|
795
|
+
iterationComplete = true;
|
|
796
|
+
nowMs += 1;
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
if (sourceOnlyMode || !destinationAdapter || !destinationEndpoint) {
|
|
800
|
+
okCount += 1;
|
|
801
|
+
iterationComplete = true;
|
|
802
|
+
nowMs += 1;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
let destinationPayload = await destinationAdapter.consume();
|
|
806
|
+
destinationPayload = normalizePayload(destinationPayload, destinationEndpoint, i);
|
|
807
|
+
let matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
|
|
808
|
+
const correlationDeadlineMs = nowMs + timeoutMs;
|
|
809
|
+
while (!matched && Date.now() <= scenarioDeadlineMs && nowMs < correlationDeadlineMs) {
|
|
810
|
+
const remainingTimeoutMs = correlationDeadlineMs - nowMs;
|
|
811
|
+
if (remainingTimeoutMs <= 0) {
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
await sleep(Math.min(destinationEndpoint.pollIntervalMs ?? 250, remainingTimeoutMs));
|
|
815
|
+
destinationPayload = normalizePayload(await destinationAdapter.consume(), destinationEndpoint, i);
|
|
816
|
+
matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
|
|
817
|
+
if (matched) {
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
nowMs += Math.max(destinationEndpoint.pollIntervalMs ?? 250, 1);
|
|
821
|
+
}
|
|
822
|
+
if (matched) {
|
|
823
|
+
okCount += 1;
|
|
824
|
+
iterationComplete = true;
|
|
825
|
+
}
|
|
826
|
+
else if (restartOnFail && attempts < maxAttempts) {
|
|
827
|
+
nowMs += 2;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
|
|
832
|
+
failCount += 1;
|
|
833
|
+
iterationComplete = true;
|
|
834
|
+
}
|
|
835
|
+
nowMs += 2;
|
|
836
|
+
}
|
|
837
|
+
processedIterations += 1;
|
|
838
|
+
if (timeoutSweepIntervalMs > 0 && nowMs - lastSweepAtMs >= timeoutSweepIntervalMs) {
|
|
839
|
+
const swept = await runtime.sweepTimeouts(nowMs, timeoutBatchSize || undefined);
|
|
840
|
+
if (swept > 0) {
|
|
841
|
+
if (timeoutCountsAsFailure) {
|
|
842
|
+
failCount += swept;
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
okCount += swept;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
lastSweepAtMs = nowMs;
|
|
849
|
+
}
|
|
850
|
+
if (maxFailCount > 0 && failCount >= maxFailCount) {
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
let expired = 0;
|
|
855
|
+
let sweptChunk = 0;
|
|
856
|
+
do {
|
|
857
|
+
sweptChunk = await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
|
|
858
|
+
expired += sweptChunk;
|
|
859
|
+
} while (timeoutBatchSize > 0 && sweptChunk >= timeoutBatchSize);
|
|
860
|
+
if (expired > 0) {
|
|
861
|
+
if (timeoutCountsAsFailure) {
|
|
862
|
+
failCount += expired;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
okCount += expired;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (processedIterations < requestCount && Date.now() > scenarioDeadlineMs) {
|
|
869
|
+
const timedOutIterations = requestCount - processedIterations;
|
|
870
|
+
if (timeoutCountsAsFailure) {
|
|
871
|
+
failCount += timedOutIterations;
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
okCount += timedOutIterations;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return { requestCount: processedIterations, okCount, failCount };
|
|
878
|
+
}
|
|
879
|
+
function validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndpoint) {
|
|
880
|
+
if (sourceEndpoint.gatherByField?.trim()) {
|
|
881
|
+
throw new Error("GatherByField is supported on destination endpoint only.");
|
|
882
|
+
}
|
|
883
|
+
const correlationTimeoutOverride = pickValue(tracking, "CorrelationTimeoutMs", "correlationTimeoutMs");
|
|
884
|
+
const correlationTimeoutSeconds = pickValue(tracking, "CorrelationTimeoutSeconds", "correlationTimeoutSeconds", "CorrelationTimeout");
|
|
885
|
+
const correlationTimeoutMs = correlationTimeoutOverride != null && String(correlationTimeoutOverride).trim() !== ""
|
|
886
|
+
? asInt(correlationTimeoutOverride)
|
|
887
|
+
: Math.trunc((correlationTimeoutSeconds == null || String(correlationTimeoutSeconds).trim() === ""
|
|
888
|
+
? 30
|
|
889
|
+
: asNumber(correlationTimeoutSeconds)) * 1000);
|
|
890
|
+
if (correlationTimeoutMs <= 0) {
|
|
891
|
+
throw new RangeError("CorrelationTimeout must be greater than zero.");
|
|
892
|
+
}
|
|
893
|
+
const timeoutSweepIntervalOverride = pickValue(tracking, "TimeoutSweepIntervalMs", "timeoutSweepIntervalMs");
|
|
894
|
+
const timeoutSweepIntervalSeconds = pickValue(tracking, "TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds");
|
|
895
|
+
const timeoutSweepIntervalMs = timeoutSweepIntervalOverride != null && String(timeoutSweepIntervalOverride).trim() !== ""
|
|
896
|
+
? asInt(timeoutSweepIntervalOverride)
|
|
897
|
+
: Math.trunc((timeoutSweepIntervalSeconds == null || String(timeoutSweepIntervalSeconds).trim() === ""
|
|
898
|
+
? 1
|
|
899
|
+
: asNumber(timeoutSweepIntervalSeconds)) * 1000);
|
|
900
|
+
if (timeoutSweepIntervalMs <= 0) {
|
|
901
|
+
throw new RangeError("TimeoutSweepInterval must be greater than zero.");
|
|
902
|
+
}
|
|
903
|
+
const timeoutBatchSizeValue = pickValue(tracking, "TimeoutBatchSize", "timeoutBatchSize");
|
|
904
|
+
const timeoutBatchSize = timeoutBatchSizeValue == null || String(timeoutBatchSizeValue).trim() === ""
|
|
905
|
+
? 200
|
|
906
|
+
: asInt(timeoutBatchSizeValue);
|
|
907
|
+
if (timeoutBatchSize <= 0) {
|
|
908
|
+
throw new RangeError("TimeoutBatchSize must be greater than zero.");
|
|
909
|
+
}
|
|
910
|
+
const metricPrefix = stringOrDefault(pickValue(tracking, "MetricPrefix", "metricPrefix"), "tracking");
|
|
911
|
+
if (!metricPrefix.trim()) {
|
|
912
|
+
throw new Error("MetricPrefix must be provided.");
|
|
913
|
+
}
|
|
914
|
+
validateRedisCorrelationStoreConfiguration(tracking);
|
|
915
|
+
const runModeRaw = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
|
|
916
|
+
const runMode = runModeRaw === "sourceonly" ? "generateandcorrelate" : runModeRaw;
|
|
917
|
+
if (runMode === "generateandcorrelate") {
|
|
918
|
+
if (sourceEndpoint.mode !== "Produce") {
|
|
919
|
+
throw new Error("Source endpoint mode must be Produce for GenerateAndCorrelate mode.");
|
|
920
|
+
}
|
|
921
|
+
if (destinationEndpoint && destinationEndpoint.mode !== "Consume") {
|
|
922
|
+
throw new Error("Destination endpoint mode must be Consume for GenerateAndCorrelate mode.");
|
|
923
|
+
}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (runMode === "correlateexistingtraffic") {
|
|
927
|
+
if (!destinationEndpoint) {
|
|
928
|
+
throw new Error("Destination endpoint must be provided for CorrelateExistingTraffic mode.");
|
|
929
|
+
}
|
|
930
|
+
if (sourceEndpoint.mode !== "Consume") {
|
|
931
|
+
throw new Error("Source endpoint mode must be Consume for CorrelateExistingTraffic mode.");
|
|
932
|
+
}
|
|
933
|
+
if (destinationEndpoint.mode !== "Consume") {
|
|
934
|
+
throw new Error("Destination endpoint mode must be Consume for CorrelateExistingTraffic mode.");
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
throw new RangeError(`Unsupported tracking run mode: ${runModeRaw}.`);
|
|
939
|
+
}
|
|
940
|
+
function normalizeHttpTrackingPayloadSourceValue(value) {
|
|
941
|
+
const normalized = String(value ?? "").trim().toLowerCase().replace(/_/g, "");
|
|
942
|
+
switch (normalized) {
|
|
943
|
+
case "":
|
|
944
|
+
return undefined;
|
|
945
|
+
case "request":
|
|
946
|
+
return "Request";
|
|
947
|
+
case "response":
|
|
948
|
+
case "body":
|
|
949
|
+
case "responsebody":
|
|
950
|
+
case "headers":
|
|
951
|
+
case "responseheaders":
|
|
952
|
+
case "status":
|
|
953
|
+
case "statuscode":
|
|
954
|
+
case "responsestatuscode":
|
|
955
|
+
return "Response";
|
|
956
|
+
default:
|
|
957
|
+
return value;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function inferLegacyHttpResponseSourceValue(value) {
|
|
961
|
+
const normalized = String(value ?? "").trim().toLowerCase().replace(/_/g, "");
|
|
962
|
+
switch (normalized) {
|
|
963
|
+
case "body":
|
|
964
|
+
case "responsebody":
|
|
965
|
+
return "ResponseBody";
|
|
966
|
+
case "headers":
|
|
967
|
+
case "responseheaders":
|
|
968
|
+
return "ResponseHeaders";
|
|
969
|
+
case "status":
|
|
970
|
+
case "statuscode":
|
|
971
|
+
case "responsestatuscode":
|
|
972
|
+
return "ResponseStatusCode";
|
|
973
|
+
default:
|
|
974
|
+
return undefined;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function mapEndpointSpec(spec) {
|
|
978
|
+
const pollIntervalOverride = pickValue(spec, "PollIntervalMs", "pollIntervalMs");
|
|
979
|
+
const pollIntervalSeconds = pickValue(spec, "PollIntervalSeconds", "pollIntervalSeconds", "PollInterval");
|
|
980
|
+
const pollIntervalMs = pollIntervalOverride != null && String(pollIntervalOverride).trim() !== ""
|
|
981
|
+
? asInt(pollIntervalOverride)
|
|
982
|
+
: Math.trunc((pollIntervalSeconds == null || String(pollIntervalSeconds).trim() === ""
|
|
983
|
+
? 0.25
|
|
984
|
+
: asNumber(pollIntervalSeconds)) * 1000);
|
|
985
|
+
const httpSpec = asRecord(pickValue(spec, "Http", "http"));
|
|
986
|
+
const delegateSpec = asRecord(pickValue(spec, "delegate", "Delegate", "DelegateStream"));
|
|
987
|
+
const payloadType = stringOrDefault(pickValue(spec, "MessagePayloadType", "messagePayloadType"), "").trim();
|
|
988
|
+
const serializerSettings = asRecord(pickValue(spec, "JsonSerializerSettings", "jsonSerializerSettings", "JsonSettings"));
|
|
989
|
+
const convertSettings = asRecord(pickValue(spec, "JsonConvertSettings", "jsonConvertSettings", "ConvertSettings"));
|
|
990
|
+
const delegateProduce = pickValue(delegateSpec, "produce", "Produce");
|
|
991
|
+
const delegateConsume = pickValue(delegateSpec, "consume", "Consume");
|
|
992
|
+
const delegateProduceAsync = pickValue(delegateSpec, "produceAsync", "ProduceAsync");
|
|
993
|
+
const delegateConsumeAsync = pickValue(delegateSpec, "consumeAsync", "ConsumeAsync");
|
|
994
|
+
const httpAuthSpec = asRecord(pickValue(httpSpec, "Auth", "auth"));
|
|
995
|
+
const oauthSpec = asRecord(pickValue(httpAuthSpec, "OAuth2ClientCredentials", "oauth2ClientCredentials"));
|
|
996
|
+
const oauthScopes = normalizeStringArray(pickValue(oauthSpec, "Scopes", "scopes"));
|
|
997
|
+
const authScopes = normalizeStringArray(pickValue(httpAuthSpec, "Scopes", "scopes"));
|
|
998
|
+
const resolvedScopes = oauthScopes.length ? oauthScopes : authScopes;
|
|
999
|
+
const scopeValue = stringOrDefault(pickValue(httpAuthSpec, "Scope", "scope"), resolvedScopes.length ? resolvedScopes.join(" ") : "");
|
|
1000
|
+
const additionalFormFields = toStringMap(pickValue(oauthSpec, "AdditionalFormFields", "additionalFormFields"));
|
|
1001
|
+
const rawTrackingPayloadSource = stringOrDefault(pickValue(httpSpec, "TrackingPayloadSource", "trackingPayloadSource"), "");
|
|
1002
|
+
const trackingPayloadSource = normalizeHttpTrackingPayloadSourceValue(rawTrackingPayloadSource);
|
|
1003
|
+
const explicitResponseSource = stringOrDefault(pickValue(httpSpec, "ResponseSource", "responseSource"), "");
|
|
1004
|
+
const consumeArrayPath = stringOrDefault(pickValue(httpSpec, "ConsumeArrayPath", "consumeArrayPath"), "");
|
|
1005
|
+
const consumeJsonArrayResponse = toBoolean(pickValue(httpSpec, "ConsumeJsonArrayResponse", "consumeJsonArrayResponse"), false);
|
|
1006
|
+
return {
|
|
1007
|
+
kind: stringOrDefault(pickValue(spec, "Kind", "kind"), "DelegateStream"),
|
|
1008
|
+
mode: stringOrDefault(pickValue(spec, "Mode", "mode"), "Produce"),
|
|
1009
|
+
name: stringOrDefault(pickValue(spec, "Name", "name"), "endpoint"),
|
|
1010
|
+
trackingField: readTrackingSelectorValue(pickValue(spec, "TrackingField", "trackingField"), "header:x-correlation-id"),
|
|
1011
|
+
gatherByField: readOptionalTrackingSelectorValue(pickValue(spec, "GatherByField", "gatherByField")),
|
|
1012
|
+
autoGenerateTrackingIdWhenMissing: toBoolean(pickValue(spec, "AutoGenerateTrackingIdWhenMissing", "autoGenerateTrackingIdWhenMissing"), true),
|
|
1013
|
+
pollIntervalMs,
|
|
1014
|
+
messageHeaders: toStringMap(pickValue(spec, "MessageHeaders", "messageHeaders")),
|
|
1015
|
+
messagePayload: normalizeEndpointPayloadValue(pickValue(spec, "MessagePayload", "messagePayload"), payloadType, serializerSettings, convertSettings),
|
|
1016
|
+
messagePayloadType: payloadType || undefined,
|
|
1017
|
+
jsonSerializerSettings: Object.keys(serializerSettings).length ? serializerSettings : undefined,
|
|
1018
|
+
jsonConvertSettings: Object.keys(convertSettings).length ? convertSettings : undefined,
|
|
1019
|
+
contentType: stringOrDefault(pickValue(spec, "ContentType", "contentType"), "") || undefined,
|
|
1020
|
+
connectionMetadata: toStringMap(pickValue(spec, "ConnectionMetadata", "connectionMetadata")),
|
|
1021
|
+
http: Object.keys(httpSpec).length
|
|
1022
|
+
? {
|
|
1023
|
+
url: stringOrDefault(pickValue(httpSpec, "Url", "url"), ""),
|
|
1024
|
+
method: stringOrDefault(pickValue(httpSpec, "Method", "method"), "GET"),
|
|
1025
|
+
bodyType: stringOrDefault(pickValue(httpSpec, "BodyType", "bodyType"), "Json"),
|
|
1026
|
+
responseSource: stringOrDefault(explicitResponseSource || inferLegacyHttpResponseSourceValue(rawTrackingPayloadSource), "None"),
|
|
1027
|
+
trackingPayloadSource,
|
|
1028
|
+
consumeArrayPath: consumeArrayPath || undefined,
|
|
1029
|
+
consumeJsonArrayResponse,
|
|
1030
|
+
requestTimeoutSeconds: (() => {
|
|
1031
|
+
const requestTimeout = pickValue(httpSpec, "RequestTimeoutSeconds", "requestTimeoutSeconds");
|
|
1032
|
+
return requestTimeout == null || String(requestTimeout).trim() === ""
|
|
1033
|
+
? undefined
|
|
1034
|
+
: asNumber(requestTimeout);
|
|
1035
|
+
})(),
|
|
1036
|
+
tokenRequestHeaders: toStringMap(pickValue(httpSpec, "TokenRequestHeaders", "tokenRequestHeaders")),
|
|
1037
|
+
auth: Object.keys(httpAuthSpec).length
|
|
1038
|
+
? {
|
|
1039
|
+
mode: stringOrDefault(pickValue(httpAuthSpec, "Mode", "mode", "Type", "type"), "None"),
|
|
1040
|
+
username: stringOrDefault(pickValue(httpAuthSpec, "Username", "username"), "") || undefined,
|
|
1041
|
+
password: (() => {
|
|
1042
|
+
const password = pickValue(httpAuthSpec, "Password", "password");
|
|
1043
|
+
return password == null ? undefined : String(password).trim();
|
|
1044
|
+
})(),
|
|
1045
|
+
bearerToken: stringOrDefault(pickValue(httpAuthSpec, "BearerToken", "bearerToken"), "") || undefined,
|
|
1046
|
+
tokenUrl: stringOrDefault(pickValue(httpAuthSpec, "TokenUrl", "tokenUrl", "TokenEndpoint", "tokenEndpoint")
|
|
1047
|
+
?? pickValue(oauthSpec, "TokenEndpoint", "tokenEndpoint"), "") || undefined,
|
|
1048
|
+
clientId: stringOrDefault(pickValue(httpAuthSpec, "ClientId", "clientId") ?? pickValue(oauthSpec, "ClientId", "clientId"), "") || undefined,
|
|
1049
|
+
clientSecret: stringOrDefault(pickValue(httpAuthSpec, "ClientSecret", "clientSecret") ?? pickValue(oauthSpec, "ClientSecret", "clientSecret"), "") || undefined,
|
|
1050
|
+
scope: scopeValue || undefined,
|
|
1051
|
+
scopes: resolvedScopes.length ? resolvedScopes : undefined,
|
|
1052
|
+
audience: stringOrDefault(pickValue(httpAuthSpec, "Audience", "audience"), "") || undefined,
|
|
1053
|
+
tokenHeaderName: stringOrDefault(pickValue(httpAuthSpec, "TokenHeaderName", "tokenHeaderName"), "") || undefined,
|
|
1054
|
+
additionalFormFields: Object.keys(additionalFormFields).length ? additionalFormFields : undefined,
|
|
1055
|
+
oauth2ClientCredentials: Object.keys(oauthSpec).length
|
|
1056
|
+
? {
|
|
1057
|
+
tokenEndpoint: stringOrDefault(pickValue(oauthSpec, "TokenEndpoint", "tokenEndpoint"), "") || undefined,
|
|
1058
|
+
clientId: stringOrDefault(pickValue(oauthSpec, "ClientId", "clientId"), "") || undefined,
|
|
1059
|
+
clientSecret: stringOrDefault(pickValue(oauthSpec, "ClientSecret", "clientSecret"), "") || undefined,
|
|
1060
|
+
scopes: resolvedScopes.length ? resolvedScopes : undefined,
|
|
1061
|
+
additionalFormFields: Object.keys(additionalFormFields).length ? additionalFormFields : undefined
|
|
1062
|
+
}
|
|
1063
|
+
: undefined
|
|
1064
|
+
}
|
|
1065
|
+
: undefined
|
|
1066
|
+
}
|
|
1067
|
+
: undefined,
|
|
1068
|
+
kafka: asRecord(pickValue(spec, "Kafka", "kafka")),
|
|
1069
|
+
rabbitMq: asRecord(pickValue(spec, "RabbitMq", "rabbitMq")),
|
|
1070
|
+
nats: asRecord(pickValue(spec, "Nats", "nats")),
|
|
1071
|
+
redisStreams: asRecord(pickValue(spec, "RedisStreams", "redisStreams")),
|
|
1072
|
+
azureEventHubs: asRecord(pickValue(spec, "AzureEventHubs", "azureEventHubs")),
|
|
1073
|
+
pushDiffusion: asRecord(pickValue(spec, "PushDiffusion", "pushDiffusion")),
|
|
1074
|
+
delegate: (typeof delegateProduce === "function"
|
|
1075
|
+
|| typeof delegateConsume === "function"
|
|
1076
|
+
|| typeof delegateProduceAsync === "function"
|
|
1077
|
+
|| typeof delegateConsumeAsync === "function")
|
|
1078
|
+
? {
|
|
1079
|
+
produce: typeof delegateProduce === "function"
|
|
1080
|
+
? delegateProduce
|
|
1081
|
+
: undefined,
|
|
1082
|
+
consume: typeof delegateConsume === "function"
|
|
1083
|
+
? delegateConsume
|
|
1084
|
+
: undefined,
|
|
1085
|
+
produceAsync: typeof delegateProduceAsync === "function"
|
|
1086
|
+
? delegateProduceAsync
|
|
1087
|
+
: undefined,
|
|
1088
|
+
consumeAsync: typeof delegateConsumeAsync === "function"
|
|
1089
|
+
? delegateConsumeAsync
|
|
1090
|
+
: undefined,
|
|
1091
|
+
connectionMetadata: toStringMap(pickValue(delegateSpec, "ConnectionMetadata", "connectionMetadata"))
|
|
1092
|
+
}
|
|
1093
|
+
: undefined
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function normalizePayload(payload, endpoint, index) {
|
|
1097
|
+
const resolvedBody = payload?.body ?? endpoint.messagePayload;
|
|
1098
|
+
const normalized = {
|
|
1099
|
+
headers: {
|
|
1100
|
+
...(endpoint.messageHeaders ?? {}),
|
|
1101
|
+
...(payload?.headers ?? {})
|
|
1102
|
+
},
|
|
1103
|
+
body: normalizeEndpointPayloadValue(resolvedBody, endpoint.messagePayloadType, endpoint.jsonSerializerSettings, endpoint.jsonConvertSettings)
|
|
1104
|
+
};
|
|
1105
|
+
const selector = endpoint.trackingField.trim();
|
|
1106
|
+
if (!selector) {
|
|
1107
|
+
return normalized;
|
|
1108
|
+
}
|
|
1109
|
+
const existing = readTrackingId(normalized, selector);
|
|
1110
|
+
if (existing || !endpoint.autoGenerateTrackingIdWhenMissing) {
|
|
1111
|
+
return normalized;
|
|
1112
|
+
}
|
|
1113
|
+
const generated = `${endpoint.name}-auto-${index + 1}`;
|
|
1114
|
+
if (selector.toLowerCase().startsWith("header:")) {
|
|
1115
|
+
const headerName = selector.slice("header:".length).trim();
|
|
1116
|
+
if (headerName) {
|
|
1117
|
+
normalized.headers = normalized.headers ?? {};
|
|
1118
|
+
normalized.headers[headerName] = generated;
|
|
1119
|
+
}
|
|
1120
|
+
return normalized;
|
|
1121
|
+
}
|
|
1122
|
+
if (selector.toLowerCase().startsWith("json:")) {
|
|
1123
|
+
const path = selector.slice("json:".length).trim().replace(/^\$\./, "");
|
|
1124
|
+
normalized.body = setJsonPathValue(normalized.body, path, generated);
|
|
1125
|
+
}
|
|
1126
|
+
return normalized;
|
|
1127
|
+
}
|
|
1128
|
+
function readTrackingId(payload, selector) {
|
|
1129
|
+
const normalized = selector.trim().toLowerCase();
|
|
1130
|
+
if (normalized.startsWith("header:")) {
|
|
1131
|
+
const expected = selector.slice("header:".length).trim().toLowerCase();
|
|
1132
|
+
const headers = payload.headers ?? {};
|
|
1133
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1134
|
+
const normalizedValue = String(value ?? "").trim();
|
|
1135
|
+
if (key.toLowerCase() === expected && normalizedValue) {
|
|
1136
|
+
return normalizedValue;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
if (normalized.startsWith("json:")) {
|
|
1142
|
+
const path = selector.slice("json:".length).trim().replace(/^\$\./, "");
|
|
1143
|
+
const body = parseBodyAsObject(payload.body);
|
|
1144
|
+
if (!body) {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
let current = body;
|
|
1148
|
+
for (const segment of path.split(".").filter(Boolean)) {
|
|
1149
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1150
|
+
current = current[segment];
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return current == null ? null : String(current);
|
|
1157
|
+
}
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
function readTrackingSelectorValue(value, fallback) {
|
|
1161
|
+
return readOptionalTrackingSelectorValue(value) ?? fallback;
|
|
1162
|
+
}
|
|
1163
|
+
function readOptionalTrackingSelectorValue(value) {
|
|
1164
|
+
if (value == null) {
|
|
1165
|
+
return undefined;
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof value === "string") {
|
|
1168
|
+
return value.trim() || undefined;
|
|
1169
|
+
}
|
|
1170
|
+
if (value instanceof correlation_js_1.TrackingFieldSelector) {
|
|
1171
|
+
return value.toString();
|
|
1172
|
+
}
|
|
1173
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1174
|
+
const record = value;
|
|
1175
|
+
const location = record.Location ?? record.location ?? record.Kind ?? record.kind;
|
|
1176
|
+
if (location != null || record.Path != null || record.path != null) {
|
|
1177
|
+
return new correlation_js_1.TrackingFieldSelector(location, String(record.Path ?? record.path ?? "")).toString();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return undefined;
|
|
1181
|
+
}
|
|
1182
|
+
function setJsonPathValue(body, path, value) {
|
|
1183
|
+
const target = body && typeof body === "object" && !Array.isArray(body)
|
|
1184
|
+
? { ...body }
|
|
1185
|
+
: {};
|
|
1186
|
+
const segments = path.split(".").filter(Boolean);
|
|
1187
|
+
if (!segments.length) {
|
|
1188
|
+
return target;
|
|
1189
|
+
}
|
|
1190
|
+
let current = target;
|
|
1191
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
1192
|
+
const segment = segments[i];
|
|
1193
|
+
const next = current[segment];
|
|
1194
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
1195
|
+
current[segment] = {};
|
|
1196
|
+
}
|
|
1197
|
+
current = current[segment];
|
|
1198
|
+
}
|
|
1199
|
+
current[segments[segments.length - 1]] = value;
|
|
1200
|
+
return target;
|
|
1201
|
+
}
|
|
1202
|
+
function mapCorrelationStore(tracking, runNamespace) {
|
|
1203
|
+
const storeSpec = asRecord(pickValue(tracking, "CorrelationStore", "correlationStore"));
|
|
1204
|
+
const kind = stringOrDefault(pickValue(storeSpec, "Kind", "kind"), "").trim().toLowerCase();
|
|
1205
|
+
const redisSpec = asRecord(pickValue(storeSpec, "Redis", "redis"));
|
|
1206
|
+
const useRedis = kind === "redis" || kind === "rediscorrelationstore" || Object.keys(redisSpec).length > 0;
|
|
1207
|
+
if (!useRedis) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const options = readRedisCorrelationStoreOptions(redisSpec);
|
|
1211
|
+
return correlation_js_1.RedisCorrelationStore.fromOptions(options, runNamespace);
|
|
1212
|
+
}
|
|
1213
|
+
function validateRedisCorrelationStoreConfiguration(tracking) {
|
|
1214
|
+
const storeSpec = asRecord(pickValue(tracking, "CorrelationStore", "correlationStore"));
|
|
1215
|
+
const kind = stringOrDefault(pickValue(storeSpec, "Kind", "kind"), "").trim().toLowerCase();
|
|
1216
|
+
const redisSpec = asRecord(pickValue(storeSpec, "Redis", "redis"));
|
|
1217
|
+
const useRedis = kind === "redis" || kind === "rediscorrelationstore" || Object.keys(redisSpec).length > 0;
|
|
1218
|
+
if (!useRedis) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const configuration = correlation_js_1.CorrelationStoreConfiguration.RedisStore(readRedisCorrelationStoreOptions(redisSpec));
|
|
1222
|
+
configuration.validate();
|
|
1223
|
+
}
|
|
1224
|
+
function readRedisCorrelationStoreOptions(redisSpec) {
|
|
1225
|
+
const options = new correlation_js_1.RedisCorrelationStoreOptions();
|
|
1226
|
+
options.ConnectionString = stringOrDefault(pickValue(redisSpec, "ConnectionString", "connectionString"), "");
|
|
1227
|
+
const database = pickValue(redisSpec, "Database", "database");
|
|
1228
|
+
if (database != null && String(database).trim() !== "") {
|
|
1229
|
+
options.Database = asInt(database);
|
|
1230
|
+
}
|
|
1231
|
+
options.KeyPrefix = stringOrDefault(pickValue(redisSpec, "KeyPrefix", "keyPrefix"), options.KeyPrefix);
|
|
1232
|
+
const entryTtlSeconds = asNumber(pickValue(redisSpec, "EntryTtlSeconds", "entryTtlSeconds", "EntryTtl", "entryTtl"));
|
|
1233
|
+
if (!Number.isNaN(entryTtlSeconds)) {
|
|
1234
|
+
options.EntryTtlSeconds = entryTtlSeconds;
|
|
1235
|
+
}
|
|
1236
|
+
return options;
|
|
1237
|
+
}
|
|
1238
|
+
function buildTrackingRunNamespace(sessionId, scenarioName, sourceName, destinationName) {
|
|
1239
|
+
return [
|
|
1240
|
+
sanitizeTrackingNamespacePart(sessionId),
|
|
1241
|
+
sanitizeTrackingNamespacePart(scenarioName),
|
|
1242
|
+
sanitizeTrackingNamespacePart(sourceName),
|
|
1243
|
+
sanitizeTrackingNamespacePart(destinationName ?? "source-only")
|
|
1244
|
+
].join(":");
|
|
1245
|
+
}
|
|
1246
|
+
function sanitizeTrackingNamespacePart(value) {
|
|
1247
|
+
return String(value ?? "")
|
|
1248
|
+
.trim()
|
|
1249
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_")
|
|
1250
|
+
.replace(/\s+/g, "_");
|
|
1251
|
+
}
|
|
1252
|
+
function normalizeEndpointPayloadValue(value, payloadType, serializerSettings = {}, convertSettings = {}) {
|
|
1253
|
+
if (value == null) {
|
|
1254
|
+
return value;
|
|
1255
|
+
}
|
|
1256
|
+
const normalizedType = stringOrDefault(payloadType, "").trim().toLowerCase();
|
|
1257
|
+
if (typeof value === "string") {
|
|
1258
|
+
const text = value.trim();
|
|
1259
|
+
if (!text) {
|
|
1260
|
+
return value;
|
|
1261
|
+
}
|
|
1262
|
+
if (normalizedType === "text" || normalizedType === "string") {
|
|
1263
|
+
return value;
|
|
1264
|
+
}
|
|
1265
|
+
if (normalizedType === "number") {
|
|
1266
|
+
const parsed = Number.parseFloat(text);
|
|
1267
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
1268
|
+
}
|
|
1269
|
+
if (normalizedType === "boolean") {
|
|
1270
|
+
const lowered = text.toLowerCase();
|
|
1271
|
+
if (lowered === "true" || lowered === "1" || lowered === "yes") {
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
if (lowered === "false" || lowered === "0" || lowered === "no") {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
return value;
|
|
1278
|
+
}
|
|
1279
|
+
if (normalizedType === "base64") {
|
|
1280
|
+
try {
|
|
1281
|
+
return Buffer.from(text, "base64").toString("utf8");
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
return value;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const prefersJson = normalizedType === "json" || normalizedType === "object" || normalizedType === "auto" || !normalizedType;
|
|
1288
|
+
if (!prefersJson) {
|
|
1289
|
+
return value;
|
|
1290
|
+
}
|
|
1291
|
+
const strict = tryParseJson(text);
|
|
1292
|
+
if (strict !== undefined) {
|
|
1293
|
+
return strict;
|
|
1294
|
+
}
|
|
1295
|
+
const allowRelaxed = toBoolean(pickValue(serializerSettings, "AllowTrailingCommas", "allowTrailingCommas", "AllowSingleQuotes", "allowSingleQuotes"), true) || toBoolean(pickValue(convertSettings, "AllowTrailingCommas", "allowTrailingCommas", "AllowSingleQuotes", "allowSingleQuotes"), true);
|
|
1296
|
+
if (!allowRelaxed) {
|
|
1297
|
+
return value;
|
|
1298
|
+
}
|
|
1299
|
+
const relaxed = tryParseJson(sanitizeLooseJson(text));
|
|
1300
|
+
return relaxed === undefined ? value : relaxed;
|
|
1301
|
+
}
|
|
1302
|
+
return value;
|
|
1303
|
+
}
|
|
1304
|
+
function parseBodyAsObject(body) {
|
|
1305
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
1306
|
+
return body;
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof body !== "string" || !body.trim()) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
const parsed = normalizeEndpointPayloadValue(body, "json");
|
|
1312
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1313
|
+
return parsed;
|
|
1314
|
+
}
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
function tryParseJson(value) {
|
|
1318
|
+
try {
|
|
1319
|
+
return JSON.parse(value);
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function sanitizeLooseJson(value) {
|
|
1326
|
+
let normalized = value.trim();
|
|
1327
|
+
normalized = normalized.replace(/,\s*([}\]])/g, "$1");
|
|
1328
|
+
normalized = normalized.replace(/'([^']*)'/g, (_match, group) => `"${group.replace(/"/g, '\\"')}"`);
|
|
1329
|
+
return normalized;
|
|
1330
|
+
}
|
|
1331
|
+
async function sleep(ms) {
|
|
1332
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(ms, 0)));
|
|
1333
|
+
}
|
|
1334
|
+
function computeScenarioRequestCount(scenario) {
|
|
1335
|
+
const simulations = Array.isArray(scenario.LoadSimulations) ? scenario.LoadSimulations : [];
|
|
1336
|
+
let total = 0;
|
|
1337
|
+
for (const item of simulations) {
|
|
1338
|
+
const simulation = asRecord(item);
|
|
1339
|
+
const kind = stringOrDefault(simulation.Kind, "");
|
|
1340
|
+
const rate = asInt(simulation.Rate);
|
|
1341
|
+
const minRate = asInt(simulation.MinRate);
|
|
1342
|
+
const maxRate = asInt(simulation.MaxRate);
|
|
1343
|
+
const copies = Math.max(asInt(simulation.Copies), 1);
|
|
1344
|
+
const iterations = Math.max(asInt(simulation.Iterations), 0);
|
|
1345
|
+
const intervalSeconds = Math.max(asNumber(simulation.IntervalSeconds), 1);
|
|
1346
|
+
const duringSeconds = Math.max(asNumber(simulation.DuringSeconds), 0);
|
|
1347
|
+
switch (kind) {
|
|
1348
|
+
case "IterationsForConstant":
|
|
1349
|
+
total += copies * iterations;
|
|
1350
|
+
break;
|
|
1351
|
+
case "IterationsForInject":
|
|
1352
|
+
total += iterations;
|
|
1353
|
+
break;
|
|
1354
|
+
case "Inject":
|
|
1355
|
+
total += Math.ceil((duringSeconds / intervalSeconds) * Math.max(rate, 0));
|
|
1356
|
+
break;
|
|
1357
|
+
case "InjectRandom":
|
|
1358
|
+
total += Math.ceil((duringSeconds / intervalSeconds) * ((Math.max(minRate, 0) + Math.max(maxRate, 0)) / 2));
|
|
1359
|
+
break;
|
|
1360
|
+
case "RampingInject":
|
|
1361
|
+
total += Math.ceil((duringSeconds / intervalSeconds) * Math.max(rate, 0) * 0.5);
|
|
1362
|
+
break;
|
|
1363
|
+
case "KeepConstant":
|
|
1364
|
+
total += Math.ceil(duringSeconds) * copies;
|
|
1365
|
+
break;
|
|
1366
|
+
case "RampingConstant":
|
|
1367
|
+
total += Math.ceil(duringSeconds) * Math.max(Math.ceil(copies * 0.5), 1);
|
|
1368
|
+
break;
|
|
1369
|
+
default:
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return Math.max(total, 0);
|
|
1374
|
+
}
|
|
1375
|
+
function buildNodeStatsContract(context, createdUtc, completedUtc, values) {
|
|
1376
|
+
const durationMs = Math.max(completedUtc.getTime() - createdUtc.getTime(), 0);
|
|
1377
|
+
return {
|
|
1378
|
+
AllBytes: 0,
|
|
1379
|
+
AllFailCount: values.allFailCount,
|
|
1380
|
+
AllOkCount: values.allOkCount,
|
|
1381
|
+
AllRequestCount: values.allRequestCount,
|
|
1382
|
+
DurationMs: durationMs,
|
|
1383
|
+
NodeInfo: buildNodeInfoContract(context),
|
|
1384
|
+
TestInfo: buildTestInfoContract(context, createdUtc),
|
|
1385
|
+
Metrics: {
|
|
1386
|
+
DurationMs: 0,
|
|
1387
|
+
Counters: [],
|
|
1388
|
+
Gauges: []
|
|
1389
|
+
},
|
|
1390
|
+
Scenarios: values.scenarios,
|
|
1391
|
+
Thresholds: values.thresholds,
|
|
1392
|
+
Plugins: []
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function buildScenarioStatsContractRow(scenarioName, sortIndex, requestCount, okCount, failCount, durationMs) {
|
|
1396
|
+
return {
|
|
1397
|
+
ScenarioName: scenarioName,
|
|
1398
|
+
SortIndex: sortIndex,
|
|
1399
|
+
AllBytes: 0,
|
|
1400
|
+
AllFailCount: failCount,
|
|
1401
|
+
AllOkCount: okCount,
|
|
1402
|
+
AllRequestCount: requestCount,
|
|
1403
|
+
CurrentOperation: "Complete",
|
|
1404
|
+
DurationMs: durationMs,
|
|
1405
|
+
Ok: buildMeasurementStatsContract(okCount, requestCount, durationMs),
|
|
1406
|
+
Fail: buildMeasurementStatsContract(failCount, requestCount, durationMs),
|
|
1407
|
+
Steps: []
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function buildMeasurementStatsContract(count, totalCount, durationMs) {
|
|
1411
|
+
return {
|
|
1412
|
+
Request: {
|
|
1413
|
+
Count: count,
|
|
1414
|
+
Percent: totalCount <= 0 ? 0 : Math.round((100 * count) / totalCount),
|
|
1415
|
+
Rps: durationMs <= 0 ? 0 : count / (durationMs / 1000)
|
|
1416
|
+
},
|
|
1417
|
+
Latency: {
|
|
1418
|
+
MinMs: 0,
|
|
1419
|
+
MeanMs: 0,
|
|
1420
|
+
MaxMs: 0,
|
|
1421
|
+
StdDev: 0,
|
|
1422
|
+
Percent50: 0,
|
|
1423
|
+
Percent75: 0,
|
|
1424
|
+
Percent95: 0,
|
|
1425
|
+
Percent99: 0
|
|
1426
|
+
},
|
|
1427
|
+
DataTransfer: {
|
|
1428
|
+
AllBytes: 0,
|
|
1429
|
+
MinBytes: 0,
|
|
1430
|
+
MeanBytes: 0,
|
|
1431
|
+
MaxBytes: 0,
|
|
1432
|
+
StdDev: 0,
|
|
1433
|
+
Percent50: 0,
|
|
1434
|
+
Percent75: 0,
|
|
1435
|
+
Percent95: 0,
|
|
1436
|
+
Percent99: 0
|
|
1437
|
+
},
|
|
1438
|
+
StatusCodes: []
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function buildNodeInfoContract(context) {
|
|
1442
|
+
return {
|
|
1443
|
+
CoresCount: Math.max(node_os_1.default.cpus()?.length ?? 1, 1),
|
|
1444
|
+
CurrentOperation: "Complete",
|
|
1445
|
+
DotNetVersion: process.version,
|
|
1446
|
+
MachineName: node_os_1.default.hostname(),
|
|
1447
|
+
EngineVersion: "loadstrike-ts-sdk",
|
|
1448
|
+
NodeType: toContractNodeType(pickValue(context, "NodeType", "nodeType")),
|
|
1449
|
+
OS: `${node_os_1.default.platform()} ${node_os_1.default.release()}`,
|
|
1450
|
+
Processor: node_os_1.default.cpus()?.[0]?.model ?? "unknown"
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function buildTestInfoContract(context, createdUtc) {
|
|
1454
|
+
return {
|
|
1455
|
+
ClusterId: stringOrDefault(pickValue(context, "ClusterId", "clusterId"), "local"),
|
|
1456
|
+
CreatedUtc: createdUtc,
|
|
1457
|
+
SessionId: stringOrDefault(pickValue(context, "SessionId", "sessionId"), ""),
|
|
1458
|
+
TestName: stringOrDefault(pickValue(context, "TestName", "testName"), ""),
|
|
1459
|
+
TestSuite: stringOrDefault(pickValue(context, "TestSuite", "testSuite"), "")
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function evaluateScenarioThresholds(scenario, values) {
|
|
1463
|
+
const thresholds = Array.isArray(scenario.Thresholds) ? scenario.Thresholds : [];
|
|
1464
|
+
const results = [];
|
|
1465
|
+
for (const item of thresholds) {
|
|
1466
|
+
const threshold = asRecord(item);
|
|
1467
|
+
const scope = stringOrDefault(threshold.Scope, "").toLowerCase();
|
|
1468
|
+
const stepName = stringOrDefault(threshold.StepName, "");
|
|
1469
|
+
const field = stringOrDefault(threshold.Field, "").toLowerCase();
|
|
1470
|
+
const operator = stringOrDefault(threshold.Operator, "").toLowerCase();
|
|
1471
|
+
const expected = asNumber(threshold.Value);
|
|
1472
|
+
const abortWhenErrorCount = asNullableInt(pickValue(threshold, "AbortWhenErrorCount", "abortWhenErrorCount"));
|
|
1473
|
+
const startCheckAfterSeconds = Math.max(asNumber(pickValue(threshold, "StartCheckAfterSeconds", "startCheckAfterSeconds")), 0);
|
|
1474
|
+
const checkExpression = buildThresholdExpression(scope, stepName);
|
|
1475
|
+
if (startCheckAfterSeconds > 0 && values.durationMs < Math.trunc(startCheckAfterSeconds * 1000)) {
|
|
1476
|
+
results.push({
|
|
1477
|
+
ScenarioName: values.scenarioName,
|
|
1478
|
+
StepName: stepName,
|
|
1479
|
+
CheckExpression: checkExpression,
|
|
1480
|
+
ErrorCount: 0,
|
|
1481
|
+
IsFailed: false,
|
|
1482
|
+
ExceptionMessage: ""
|
|
1483
|
+
});
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const actual = field === "allokcount"
|
|
1487
|
+
? values.okCount
|
|
1488
|
+
: field === "allfailcount"
|
|
1489
|
+
? values.failCount
|
|
1490
|
+
: field === "durationms"
|
|
1491
|
+
? values.durationMs
|
|
1492
|
+
: values.requestCount;
|
|
1493
|
+
let errorCount = 0;
|
|
1494
|
+
if (scope === "step" && stepName.trim().length > 0) {
|
|
1495
|
+
errorCount += 1;
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
let failed = false;
|
|
1499
|
+
if (operator === "gt") {
|
|
1500
|
+
failed = !(actual > expected);
|
|
1501
|
+
}
|
|
1502
|
+
else if (operator === "gte") {
|
|
1503
|
+
failed = !(actual >= expected);
|
|
1504
|
+
}
|
|
1505
|
+
else if (operator === "lt") {
|
|
1506
|
+
failed = !(actual < expected);
|
|
1507
|
+
}
|
|
1508
|
+
else if (operator === "lte") {
|
|
1509
|
+
failed = !(actual <= expected);
|
|
1510
|
+
}
|
|
1511
|
+
else if (operator === "eq") {
|
|
1512
|
+
failed = actual !== expected;
|
|
1513
|
+
}
|
|
1514
|
+
else if (operator === "neq") {
|
|
1515
|
+
failed = actual === expected;
|
|
1516
|
+
}
|
|
1517
|
+
if (failed) {
|
|
1518
|
+
errorCount += 1;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
let isFailed = errorCount > 0;
|
|
1522
|
+
if (abortWhenErrorCount != null && errorCount >= abortWhenErrorCount) {
|
|
1523
|
+
isFailed = true;
|
|
1524
|
+
}
|
|
1525
|
+
results.push({
|
|
1526
|
+
ScenarioName: values.scenarioName,
|
|
1527
|
+
StepName: stepName,
|
|
1528
|
+
CheckExpression: checkExpression,
|
|
1529
|
+
ErrorCount: errorCount,
|
|
1530
|
+
IsFailed: isFailed,
|
|
1531
|
+
ExceptionMessage: ""
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
return results;
|
|
1535
|
+
}
|
|
1536
|
+
function buildThresholdExpression(scope, stepName) {
|
|
1537
|
+
if (scope === "step" || stepName.trim().length > 0) {
|
|
1538
|
+
return `step-threshold:${stepName}`;
|
|
1539
|
+
}
|
|
1540
|
+
if (scope === "metric") {
|
|
1541
|
+
return "metric-threshold";
|
|
1542
|
+
}
|
|
1543
|
+
if (scope === "scenario") {
|
|
1544
|
+
return "scenario-threshold";
|
|
1545
|
+
}
|
|
1546
|
+
return "threshold";
|
|
1547
|
+
}
|
|
1548
|
+
function pickValue(record, ...keys) {
|
|
1549
|
+
for (const key of keys) {
|
|
1550
|
+
if (key in record) {
|
|
1551
|
+
return record[key];
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
function asRecord(value) {
|
|
1557
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1558
|
+
return value;
|
|
1559
|
+
}
|
|
1560
|
+
return {};
|
|
1561
|
+
}
|
|
1562
|
+
function isRuntimeReportingSink(value) {
|
|
1563
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
const record = value;
|
|
1567
|
+
return [
|
|
1568
|
+
"init",
|
|
1569
|
+
"Init",
|
|
1570
|
+
"start",
|
|
1571
|
+
"Start",
|
|
1572
|
+
"saveRealtimeStats",
|
|
1573
|
+
"SaveRealtimeStats",
|
|
1574
|
+
"saveRealtimeMetrics",
|
|
1575
|
+
"SaveRealtimeMetrics",
|
|
1576
|
+
"saveFinalStats",
|
|
1577
|
+
"SaveFinalStats",
|
|
1578
|
+
"stop",
|
|
1579
|
+
"Stop"
|
|
1580
|
+
].some((name) => typeof record[name] === "function");
|
|
1581
|
+
}
|
|
1582
|
+
function toStringMap(value) {
|
|
1583
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1584
|
+
return {};
|
|
1585
|
+
}
|
|
1586
|
+
const output = {};
|
|
1587
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1588
|
+
if (item != null) {
|
|
1589
|
+
output[key] = String(item);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return output;
|
|
1593
|
+
}
|
|
1594
|
+
function stringOrDefault(value, fallback) {
|
|
1595
|
+
if (typeof value === "string") {
|
|
1596
|
+
return value;
|
|
1597
|
+
}
|
|
1598
|
+
if (value == null) {
|
|
1599
|
+
return fallback;
|
|
1600
|
+
}
|
|
1601
|
+
return String(value);
|
|
1602
|
+
}
|
|
1603
|
+
function asInt(value) {
|
|
1604
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1605
|
+
return Math.trunc(value);
|
|
1606
|
+
}
|
|
1607
|
+
const parsed = Number.parseInt(String(value ?? "0"), 10);
|
|
1608
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1609
|
+
}
|
|
1610
|
+
function asNullableInt(value) {
|
|
1611
|
+
if (value == null) {
|
|
1612
|
+
return undefined;
|
|
1613
|
+
}
|
|
1614
|
+
return asInt(value);
|
|
1615
|
+
}
|
|
1616
|
+
function asNumber(value) {
|
|
1617
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1618
|
+
return value;
|
|
1619
|
+
}
|
|
1620
|
+
const parsed = Number.parseFloat(String(value ?? "0"));
|
|
1621
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1622
|
+
}
|
|
1623
|
+
function toBoolean(value, fallback) {
|
|
1624
|
+
if (typeof value === "boolean") {
|
|
1625
|
+
return value;
|
|
1626
|
+
}
|
|
1627
|
+
if (typeof value === "number") {
|
|
1628
|
+
return value !== 0;
|
|
1629
|
+
}
|
|
1630
|
+
if (typeof value === "string") {
|
|
1631
|
+
const normalized = value.trim().toLowerCase();
|
|
1632
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") {
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return fallback;
|
|
1640
|
+
}
|
|
1641
|
+
function parseJwt(token) {
|
|
1642
|
+
const parts = token.split(".");
|
|
1643
|
+
if (parts.length !== 3) {
|
|
1644
|
+
throw new Error("Invalid run token format.");
|
|
1645
|
+
}
|
|
1646
|
+
const [headerSegment, payloadSegment, signatureSegment] = parts;
|
|
1647
|
+
const header = decodeJwtJsonSegment(headerSegment);
|
|
1648
|
+
const payload = decodeJwtJsonSegment(payloadSegment);
|
|
1649
|
+
return {
|
|
1650
|
+
header,
|
|
1651
|
+
payload,
|
|
1652
|
+
signingInput: `${headerSegment}.${payloadSegment}`,
|
|
1653
|
+
signature: decodeBase64Url(signatureSegment)
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function decodeJwtJsonSegment(segment) {
|
|
1657
|
+
const decoded = decodeBase64Url(segment).toString("utf8");
|
|
1658
|
+
const parsed = JSON.parse(decoded);
|
|
1659
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1660
|
+
throw new Error("Invalid run token payload.");
|
|
1661
|
+
}
|
|
1662
|
+
return parsed;
|
|
1663
|
+
}
|
|
1664
|
+
function decodeBase64Url(value) {
|
|
1665
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1666
|
+
const remainder = normalized.length % 4;
|
|
1667
|
+
const padded = remainder ? normalized.padEnd(normalized.length + (4 - remainder), "=") : normalized;
|
|
1668
|
+
return Buffer.from(padded, "base64");
|
|
1669
|
+
}
|
|
1670
|
+
function verifyJwtSignature(token, keyRecord, algorithm) {
|
|
1671
|
+
const parsed = parseJwt(token);
|
|
1672
|
+
if (algorithm !== "RS256") {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
if (!keyRecord.publicKeyPem) {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
const verifier = (0, node_crypto_1.createVerify)("RSA-SHA256");
|
|
1679
|
+
verifier.update(parsed.signingInput);
|
|
1680
|
+
verifier.end();
|
|
1681
|
+
return verifier.verify(keyRecord.publicKeyPem, parsed.signature);
|
|
1682
|
+
}
|
|
1683
|
+
function enforceJwtLifetime(payload) {
|
|
1684
|
+
const clockSkewSeconds = 30;
|
|
1685
|
+
const nowSeconds = Math.trunc(Date.now() / 1000);
|
|
1686
|
+
const exp = readEpochClaim(payload, "exp", "Exp");
|
|
1687
|
+
if (!exp || exp <= 0) {
|
|
1688
|
+
throw new Error("Runner key validation failed: run token expiration is missing.");
|
|
1689
|
+
}
|
|
1690
|
+
if (nowSeconds > (exp + clockSkewSeconds)) {
|
|
1691
|
+
throw new Error("Runner key validation failed: run token is expired.");
|
|
1692
|
+
}
|
|
1693
|
+
const nbf = readEpochClaim(payload, "nbf", "Nbf");
|
|
1694
|
+
if (nbf && nbf > 0 && (nowSeconds + clockSkewSeconds) < nbf) {
|
|
1695
|
+
throw new Error("Runner key validation failed: run token is not yet valid.");
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function readEpochClaim(payload, ...keys) {
|
|
1699
|
+
const value = pickValue(payload, ...keys);
|
|
1700
|
+
if (value == null) {
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1704
|
+
return Math.trunc(value);
|
|
1705
|
+
}
|
|
1706
|
+
const text = String(value).trim();
|
|
1707
|
+
if (!text) {
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
const parsed = Number.parseFloat(text);
|
|
1711
|
+
if (!Number.isFinite(parsed)) {
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
return Math.trunc(parsed);
|
|
1715
|
+
}
|
|
1716
|
+
function enforceEntitlementClaims(payload, requestedFeatures) {
|
|
1717
|
+
const entitlements = collectClaimStrings(payload, "entitlements", "Entitlements", "features", "Features", "feature", "Feature");
|
|
1718
|
+
const normalized = new Set(entitlements.map((value) => value.toLowerCase()));
|
|
1719
|
+
for (const feature of requestedFeatures) {
|
|
1720
|
+
if (!normalized.has(feature.toLowerCase())) {
|
|
1721
|
+
throw new Error(`Runner key validation denied. DenialCode=missing_entitlement, Message=Missing entitlement for ${feature}.`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function enforceRuntimePolicyClaims(payload, request) {
|
|
1726
|
+
const policy = resolveRuntimePolicy(payload);
|
|
1727
|
+
if (!Object.keys(policy).length) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const context = asRecord(request.Context);
|
|
1731
|
+
const maxScenarios = asInt(pickValue(policy, "maxScenarios", "MaxScenarios"));
|
|
1732
|
+
const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
|
|
1733
|
+
if (maxScenarios > 0 && scenarios.length > maxScenarios) {
|
|
1734
|
+
throw new Error(`Runner key validation denied. DenialCode=max_scenarios_exceeded, Message=Scenario count ${scenarios.length} exceeds policy limit ${maxScenarios}.`);
|
|
1735
|
+
}
|
|
1736
|
+
const maxAgents = asInt(pickValue(policy, "maxAgentsCount", "MaxAgentsCount"));
|
|
1737
|
+
const agentsCount = Math.max(asInt(pickValue(context, "AgentsCount", "agentsCount")), 1);
|
|
1738
|
+
if (maxAgents > 0 && agentsCount > maxAgents) {
|
|
1739
|
+
throw new Error(`Runner key validation denied. DenialCode=max_agents_exceeded, Message=Agents count ${agentsCount} exceeds policy limit ${maxAgents}.`);
|
|
1740
|
+
}
|
|
1741
|
+
const maxCustomWorkerPlugins = asInt(pickValue(policy, "maxCustomWorkerPlugins", "MaxCustomWorkerPlugins"));
|
|
1742
|
+
const customWorkerPluginsCount = countCustomWorkerPlugins(context);
|
|
1743
|
+
if (maxCustomWorkerPlugins > 0 && customWorkerPluginsCount > maxCustomWorkerPlugins) {
|
|
1744
|
+
throw new Error("Runner key validation denied. DenialCode=max_custom_worker_plugins_exceeded, "
|
|
1745
|
+
+ `Message=Custom worker plugin count ${customWorkerPluginsCount} exceeds policy limit ${maxCustomWorkerPlugins}.`);
|
|
1746
|
+
}
|
|
1747
|
+
const maxReportingSinks = asInt(pickValue(policy, "maxReportingSinks", "MaxReportingSinks"));
|
|
1748
|
+
const reportingSinksCount = asList(pickValue(context, "ReportingSinks", "reportingSinks")).length;
|
|
1749
|
+
if (maxReportingSinks > 0 && reportingSinksCount > maxReportingSinks) {
|
|
1750
|
+
throw new Error("Runner key validation denied. DenialCode=max_reporting_sinks_exceeded, "
|
|
1751
|
+
+ `Message=Reporting sink count ${reportingSinksCount} exceeds policy limit ${maxReportingSinks}.`);
|
|
1752
|
+
}
|
|
1753
|
+
const maxTargetScenarioCount = asInt(pickValue(policy, "maxTargetScenarioCount", "MaxTargetScenarioCount"));
|
|
1754
|
+
const targetedScenarioCount = countTargetedScenarios(context);
|
|
1755
|
+
if (maxTargetScenarioCount > 0 && targetedScenarioCount > maxTargetScenarioCount) {
|
|
1756
|
+
throw new Error("Runner key validation denied. DenialCode=max_target_scenarios_exceeded, "
|
|
1757
|
+
+ `Message=Targeted scenario count ${targetedScenarioCount} exceeds policy limit ${maxTargetScenarioCount}.`);
|
|
1758
|
+
}
|
|
1759
|
+
const maxClusterCommandTimeoutSeconds = asInt(pickValue(policy, "maxClusterCommandTimeoutSeconds", "MaxClusterCommandTimeoutSeconds"));
|
|
1760
|
+
const clusterCommandTimeoutSeconds = resolveTimeoutSeconds(context, "ClusterCommandTimeoutSeconds", "clusterCommandTimeoutSeconds", "ClusterCommandTimeoutMs", "clusterCommandTimeoutMs", 120);
|
|
1761
|
+
if (maxClusterCommandTimeoutSeconds > 0
|
|
1762
|
+
&& clusterCommandTimeoutSeconds > maxClusterCommandTimeoutSeconds) {
|
|
1763
|
+
throw new Error("Runner key validation denied. DenialCode=cluster_command_timeout_exceeded, "
|
|
1764
|
+
+ `Message=Cluster command timeout ${clusterCommandTimeoutSeconds.toFixed(3)}s exceeds policy limit ${maxClusterCommandTimeoutSeconds}s.`);
|
|
1765
|
+
}
|
|
1766
|
+
const maxScenarioCompletionTimeoutSeconds = asInt(pickValue(policy, "maxScenarioCompletionTimeoutSeconds", "MaxScenarioCompletionTimeoutSeconds"));
|
|
1767
|
+
const scenarioCompletionTimeoutSeconds = resolveTimeoutSeconds(context, "ScenarioCompletionTimeoutSeconds", "scenarioCompletionTimeoutSeconds", "ScenarioCompletionTimeoutMs", "scenarioCompletionTimeoutMs", 30);
|
|
1768
|
+
if (maxScenarioCompletionTimeoutSeconds > 0
|
|
1769
|
+
&& scenarioCompletionTimeoutSeconds > maxScenarioCompletionTimeoutSeconds) {
|
|
1770
|
+
throw new Error("Runner key validation denied. DenialCode=scenario_completion_timeout_exceeded, "
|
|
1771
|
+
+ `Message=Scenario completion timeout ${scenarioCompletionTimeoutSeconds.toFixed(3)}s exceeds policy limit ${maxScenarioCompletionTimeoutSeconds}s.`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function normalizeStringArray(value) {
|
|
1775
|
+
if (Array.isArray(value)) {
|
|
1776
|
+
return value
|
|
1777
|
+
.map((entry) => stringOrDefault(entry, "").trim())
|
|
1778
|
+
.filter((entry) => entry.length > 0);
|
|
1779
|
+
}
|
|
1780
|
+
if (typeof value === "string" && value.trim()) {
|
|
1781
|
+
return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1782
|
+
}
|
|
1783
|
+
return [];
|
|
1784
|
+
}
|
|
1785
|
+
function collectClaimStrings(payload, ...keys) {
|
|
1786
|
+
const values = [];
|
|
1787
|
+
for (const key of keys) {
|
|
1788
|
+
if (!(key in payload)) {
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
values.push(...normalizeStringArray(payload[key]));
|
|
1792
|
+
}
|
|
1793
|
+
const deduped = [];
|
|
1794
|
+
const seen = new Set();
|
|
1795
|
+
for (const value of values) {
|
|
1796
|
+
const normalized = value.toLowerCase();
|
|
1797
|
+
if (seen.has(normalized)) {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
seen.add(normalized);
|
|
1801
|
+
deduped.push(value);
|
|
1802
|
+
}
|
|
1803
|
+
return deduped;
|
|
1804
|
+
}
|
|
1805
|
+
function resolveRuntimePolicy(payload) {
|
|
1806
|
+
const policy = asRecord(pickValue(payload, "runtimePolicy", "RuntimePolicy", "policy", "Policy"));
|
|
1807
|
+
policy.maxAgentsCount = pickPositiveInt(policy, payload, "maxAgentsCount", "MaxAgentsCount", "policy.max_agents_count");
|
|
1808
|
+
policy.maxScenarios = pickPositiveInt(policy, payload, "maxScenarios", "MaxScenarios", "maxScenarioCount", "MaxScenarioCount", "policy.max_scenario_count");
|
|
1809
|
+
policy.maxCustomWorkerPlugins = pickPositiveInt(policy, payload, "maxCustomWorkerPlugins", "MaxCustomWorkerPlugins", "policy.max_custom_worker_plugins");
|
|
1810
|
+
policy.maxReportingSinks = pickPositiveInt(policy, payload, "maxReportingSinks", "MaxReportingSinks", "policy.max_reporting_sinks");
|
|
1811
|
+
policy.maxTargetScenarioCount = pickPositiveInt(policy, payload, "maxTargetScenarioCount", "MaxTargetScenarioCount", "policy.max_target_scenario_count");
|
|
1812
|
+
policy.maxClusterCommandTimeoutSeconds = pickPositiveInt(policy, payload, "maxClusterCommandTimeoutSeconds", "MaxClusterCommandTimeoutSeconds", "policy.max_cluster_command_timeout_seconds");
|
|
1813
|
+
policy.maxScenarioCompletionTimeoutSeconds = pickPositiveInt(policy, payload, "maxScenarioCompletionTimeoutSeconds", "MaxScenarioCompletionTimeoutSeconds", "policy.max_scenario_completion_timeout_seconds");
|
|
1814
|
+
return policy;
|
|
1815
|
+
}
|
|
1816
|
+
function pickPositiveInt(primary, secondary, ...keys) {
|
|
1817
|
+
let value = pickValue(primary, ...keys);
|
|
1818
|
+
if (value == null) {
|
|
1819
|
+
value = pickValue(secondary, ...keys);
|
|
1820
|
+
}
|
|
1821
|
+
const parsed = asInt(value);
|
|
1822
|
+
return parsed > 0 ? parsed : 0;
|
|
1823
|
+
}
|
|
1824
|
+
function normalizeNodeType(value) {
|
|
1825
|
+
const normalized = stringOrDefault(value, "single").trim().toLowerCase();
|
|
1826
|
+
if (normalized === "0" || normalized === "single" || normalized === "singlenode") {
|
|
1827
|
+
return "single";
|
|
1828
|
+
}
|
|
1829
|
+
if (normalized === "1" || normalized === "coordinator") {
|
|
1830
|
+
return "coordinator";
|
|
1831
|
+
}
|
|
1832
|
+
if (normalized === "2" || normalized === "agent") {
|
|
1833
|
+
return "agent";
|
|
1834
|
+
}
|
|
1835
|
+
return normalized;
|
|
1836
|
+
}
|
|
1837
|
+
function toContractNodeType(value) {
|
|
1838
|
+
switch (normalizeNodeType(value)) {
|
|
1839
|
+
case "coordinator":
|
|
1840
|
+
return "Coordinator";
|
|
1841
|
+
case "agent":
|
|
1842
|
+
return "Agent";
|
|
1843
|
+
case "single":
|
|
1844
|
+
case "singlenode":
|
|
1845
|
+
return "SingleNode";
|
|
1846
|
+
default:
|
|
1847
|
+
return stringOrDefault(value, "SingleNode");
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function countTargetedScenarios(context) {
|
|
1851
|
+
const values = [
|
|
1852
|
+
...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
|
|
1853
|
+
...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
|
|
1854
|
+
...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
|
|
1855
|
+
];
|
|
1856
|
+
return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)).size;
|
|
1857
|
+
}
|
|
1858
|
+
function countCustomWorkerPlugins(context) {
|
|
1859
|
+
const workerPlugins = asList(pickValue(context, "WorkerPlugins", "workerPlugins"));
|
|
1860
|
+
let count = 0;
|
|
1861
|
+
for (const plugin of workerPlugins) {
|
|
1862
|
+
const pluginRecord = asRecord(plugin);
|
|
1863
|
+
const pluginName = stringOrDefault(pickValue(pluginRecord, "PluginName", "pluginName"), stringOrDefault(plugin.pluginName, stringOrDefault(plugin.PluginName, ""))).trim().toLowerCase();
|
|
1864
|
+
if (!pluginName || BUILT_IN_WORKER_PLUGIN_NAMES.has(pluginName)) {
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
count += 1;
|
|
1868
|
+
}
|
|
1869
|
+
return count;
|
|
1870
|
+
}
|
|
1871
|
+
function resolveTimeoutSeconds(context, secondsKey, secondsAlias, millisecondsKey, millisecondsAlias, defaultSeconds = 0) {
|
|
1872
|
+
const seconds = asNumber(pickValue(context, secondsKey, secondsAlias));
|
|
1873
|
+
if (seconds > 0) {
|
|
1874
|
+
return seconds;
|
|
1875
|
+
}
|
|
1876
|
+
const milliseconds = asNumber(pickValue(context, millisecondsKey, millisecondsAlias));
|
|
1877
|
+
if (milliseconds > 0) {
|
|
1878
|
+
return milliseconds / 1000;
|
|
1879
|
+
}
|
|
1880
|
+
return defaultSeconds;
|
|
1881
|
+
}
|
|
1882
|
+
function asList(value) {
|
|
1883
|
+
return Array.isArray(value) ? value : [];
|
|
1884
|
+
}
|