@elench/testkit 0.1.81 → 0.1.83

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.
Files changed (55) hide show
  1. package/README.md +64 -27
  2. package/lib/cli/agents/index.mjs +64 -0
  3. package/lib/cli/agents/investigate.mjs +75 -0
  4. package/lib/cli/agents/investigation-context.mjs +102 -0
  5. package/lib/cli/agents/investigation-context.test.mjs +144 -0
  6. package/lib/cli/agents/prompt-builder.mjs +25 -0
  7. package/lib/cli/agents/providers/claude.mjs +74 -0
  8. package/lib/cli/agents/providers/claude.test.mjs +95 -0
  9. package/lib/cli/agents/providers/codex.mjs +83 -0
  10. package/lib/cli/agents/providers/codex.test.mjs +93 -0
  11. package/lib/cli/agents/providers/shared.mjs +134 -0
  12. package/lib/cli/command-helpers.mjs +53 -25
  13. package/lib/cli/command-helpers.test.mjs +122 -0
  14. package/lib/cli/commands/investigate.mjs +87 -0
  15. package/lib/cli/commands/investigate.test.mjs +83 -0
  16. package/lib/cli/entrypoint.mjs +3 -0
  17. package/lib/cli/presentation/colors.mjs +12 -0
  18. package/lib/cli/presentation/events-reporter.mjs +135 -0
  19. package/lib/cli/presentation/events-reporter.test.mjs +73 -0
  20. package/lib/cli/presentation/summary-box.mjs +11 -11
  21. package/lib/cli/presentation/summary-box.test.mjs +17 -0
  22. package/lib/cli/presentation/tree-reporter.mjs +159 -0
  23. package/lib/cli/presentation/tree-reporter.test.mjs +166 -0
  24. package/lib/cli/tui/run-app.mjs +1 -0
  25. package/lib/cli/tui/run-session-app.mjs +370 -0
  26. package/lib/cli/tui/run-session-app.test.mjs +50 -0
  27. package/lib/cli/tui/run-session-state.mjs +481 -0
  28. package/lib/cli/tui/run-tree-state.mjs +1 -0
  29. package/lib/cli/tui/run-tree-state.test.mjs +324 -0
  30. package/lib/config-api/auth-fixtures.mjs +767 -0
  31. package/lib/config-api/index.d.ts +92 -108
  32. package/lib/config-api/index.mjs +22 -12
  33. package/lib/config-api/index.test.mjs +103 -210
  34. package/lib/discovery/index.mjs +1 -1
  35. package/lib/index.d.ts +34 -10
  36. package/lib/runner/orchestrator.mjs +1 -0
  37. package/lib/runtime/index.d.ts +177 -27
  38. package/lib/runtime/index.mjs +68 -3
  39. package/lib/runtime-src/k6/http-assertions.js +31 -1
  40. package/lib/runtime-src/k6/http-checks.js +120 -0
  41. package/lib/runtime-src/k6/http-checks.test.mjs +120 -0
  42. package/lib/runtime-src/k6/http-suite-runtime.js +151 -0
  43. package/lib/runtime-src/k6/http.js +285 -56
  44. package/lib/runtime-src/k6/http.test.mjs +205 -0
  45. package/lib/runtime-src/k6/scenario-suite.js +13 -110
  46. package/lib/runtime-src/k6/suite.js +13 -107
  47. package/lib/runtime-src/shared/error-body.mjs +42 -0
  48. package/lib/runtime-src/shared/http-parsing.mjs +68 -0
  49. package/lib/runtime-src/shared/http-parsing.test.mjs +69 -0
  50. package/node_modules/@elench/next-analysis/package.json +1 -1
  51. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  52. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  53. package/node_modules/@elench/ts-analysis/package.json +1 -1
  54. package/package.json +5 -5
  55. package/lib/config-api/profiles.mjs +0 -640
@@ -0,0 +1,767 @@
1
+ import { runtimeHttp, runtimeJson } from "./runtime.mjs";
2
+
3
+ const DEFAULT_PASSWORD = "TestkitPass2026";
4
+ const SUPPORTED_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
5
+
6
+ export function jsonSessionContract(options = {}) {
7
+ const normalized = normalizePlainObject(options, "auth.contracts.jsonSession(...)");
8
+ const authCookie = normalizeOptionalString(normalized.authCookie);
9
+ const refreshCookie = normalizeOptionalString(normalized.refreshCookie);
10
+ const organizationIdPath = normalizeOptionalString(normalized.organizationIdPath);
11
+ const sessionCookies = normalizeStringMap(normalized.sessionCookies);
12
+ const sessionFields = normalizeStringMap(normalized.sessionFields);
13
+
14
+ return {
15
+ kind: "json-session",
16
+ contentTypeJson: normalized.contentTypeJson !== false,
17
+ forwardedFor: normalized.forwardedFor ?? "deterministic",
18
+ login: {
19
+ expect: normalized.loginExpect ?? 200,
20
+ path: normalizeOptionalString(normalized.loginPath) || "/api/v1/auth/login",
21
+ },
22
+ organization: normalizeOrganizationHeaderConfig(normalized),
23
+ session: {
24
+ auth: normalizeAuthCaptureConfig({
25
+ authCookie,
26
+ authHeader: normalized.authHeader,
27
+ authPrefix: normalized.authPrefix,
28
+ authSourceKey: normalized.authSourceKey,
29
+ }),
30
+ cookies: {
31
+ ...(authCookie ? { [(normalizeOptionalString(normalized.authSourceKey) || "jwt")]: authCookie } : {}),
32
+ ...(refreshCookie ? { refreshToken: refreshCookie } : {}),
33
+ ...sessionCookies,
34
+ },
35
+ fields: {
36
+ ...(organizationIdPath ? { organizationId: organizationIdPath } : {}),
37
+ ...sessionFields,
38
+ },
39
+ },
40
+ signup: normalized.signup === false
41
+ ? { enabled: false, expect: [], path: null }
42
+ : {
43
+ enabled: normalized.signup !== false,
44
+ expect: normalized.signupExpect ?? [201, 409],
45
+ path: normalizeOptionalString(normalized.signupPath) || "/api/v1/auth/signup",
46
+ },
47
+ };
48
+ }
49
+
50
+ export function singleOrgTopology(options = {}) {
51
+ const normalized = normalizePlainObject(options, "auth.topologies.singleOrg(...)");
52
+ const namespace = normalizeRequiredString(normalized.namespace, "auth.topologies.singleOrg(...).namespace");
53
+ const defaultOrgKey = normalizeOptionalString(normalized.org) || "primary";
54
+ const actorEntries = normalizeTopologyActors(normalized.actors, defaultOrgKey);
55
+
56
+ return {
57
+ kind: "auth-topology",
58
+ namespace,
59
+ defaultPassword: normalizeOptionalString(normalized.password) || DEFAULT_PASSWORD,
60
+ actors: finalizeActorEntries({
61
+ namespace,
62
+ actorEntries,
63
+ orgs: normalizeTopologyOrgs(normalized.orgs, [defaultOrgKey]),
64
+ defaultOrgKey,
65
+ defaultPassword: normalizeOptionalString(normalized.password) || DEFAULT_PASSWORD,
66
+ label: "auth.topologies.singleOrg(...)",
67
+ }),
68
+ };
69
+ }
70
+
71
+ export function crossOrgTopology(options = {}) {
72
+ const normalized = normalizePlainObject(options, "auth.topologies.crossOrg(...)");
73
+ const namespace = normalizeRequiredString(normalized.namespace, "auth.topologies.crossOrg(...).namespace");
74
+ const actorEntries = normalizeTopologyActors(normalized.actors, null);
75
+ if (actorEntries.length === 0) {
76
+ throw new Error("auth.topologies.crossOrg(...) requires at least one actor");
77
+ }
78
+ const orgKeys = [...new Set(actorEntries.map((entry) => entry.orgKey))];
79
+
80
+ return {
81
+ kind: "auth-topology",
82
+ namespace,
83
+ defaultPassword: normalizeOptionalString(normalized.password) || DEFAULT_PASSWORD,
84
+ actors: finalizeActorEntries({
85
+ namespace,
86
+ actorEntries,
87
+ orgs: normalizeTopologyOrgs(normalized.orgs, orgKeys),
88
+ defaultOrgKey: actorEntries[0].orgKey,
89
+ defaultPassword: normalizeOptionalString(normalized.password) || DEFAULT_PASSWORD,
90
+ label: "auth.topologies.crossOrg(...)",
91
+ }),
92
+ };
93
+ }
94
+
95
+ export function authFixture(options = {}) {
96
+ const normalized = normalizePlainObject(options, "auth.fixture(...)");
97
+ const contract = normalizeContract(normalized.contract);
98
+ const topology = normalizeTopology(normalized.topology);
99
+
100
+ return {
101
+ contract,
102
+ topology,
103
+ profiles(profileDefinitions = {}) {
104
+ if (!profileDefinitions || typeof profileDefinitions !== "object" || Array.isArray(profileDefinitions)) {
105
+ throw new Error("auth.fixture(...).profiles(...) requires an object");
106
+ }
107
+
108
+ const resolved = {};
109
+ for (const [profileName, descriptor] of Object.entries(profileDefinitions)) {
110
+ resolved[profileName] = buildFixtureProfile({
111
+ contract,
112
+ topology,
113
+ descriptor,
114
+ label: `auth.fixture(...).profiles().${profileName}`,
115
+ });
116
+ }
117
+ return resolved;
118
+ },
119
+ };
120
+ }
121
+
122
+ export function actorProfile(actorName, options = {}) {
123
+ return {
124
+ kind: "actor",
125
+ actor: normalizeRequiredString(actorName, "auth.profile.actor(..., ... ) actor"),
126
+ env: options.env,
127
+ headers: normalizeHeaderOptions(options.headers),
128
+ options: options.options,
129
+ };
130
+ }
131
+
132
+ export function actorsProfile(options = {}) {
133
+ const normalized = normalizePlainObject(options, "auth.profile.actors(...)");
134
+ const actors = normalizeActorNameList(normalized.actors);
135
+ if (actors.length === 0) {
136
+ throw new Error("auth.profile.actors(...) requires at least one actor");
137
+ }
138
+
139
+ const primaryActor = normalizeOptionalString(normalized.primaryActor) || actors[0];
140
+ if (!actors.includes(primaryActor)) {
141
+ throw new Error(`auth.profile.actors(...) primaryActor "${primaryActor}" is not included in actors`);
142
+ }
143
+
144
+ return {
145
+ kind: "actors",
146
+ actors,
147
+ primaryActor,
148
+ env: normalized.env,
149
+ headers: normalizeHeaderOptions(normalized.headers),
150
+ options: normalized.options,
151
+ };
152
+ }
153
+
154
+ export function rawAuthProfile(options = {}) {
155
+ const normalized = normalizePlainObject(options, "auth.profile.raw(...)");
156
+ return {
157
+ kind: "raw",
158
+ env: normalized.env,
159
+ headers: normalizeHeaderOptions(normalized.headers),
160
+ options: normalized.options,
161
+ };
162
+ }
163
+
164
+ function buildFixtureProfile({ contract, topology, descriptor, label }) {
165
+ const normalizedDescriptor = normalizeProfileDescriptor(descriptor, label, topology);
166
+ const actorNames = normalizedDescriptor.kind === "raw"
167
+ ? []
168
+ : normalizedDescriptor.kind === "actor"
169
+ ? [normalizedDescriptor.actor]
170
+ : normalizedDescriptor.actors;
171
+ const primaryActor = normalizedDescriptor.kind === "raw"
172
+ ? null
173
+ : normalizedDescriptor.kind === "actor"
174
+ ? normalizedDescriptor.actor
175
+ : normalizedDescriptor.primaryActor;
176
+
177
+ const headerBuilder = createCompositeHeaderBuilder({
178
+ defaultActor: primaryActor,
179
+ headers: mergeHeaderOptions(buildContractHeaderOptions(contract), normalizedDescriptor.headers),
180
+ resolveActorRecord(bundle, actorName) {
181
+ return resolveActorRecord(bundle, actorName);
182
+ },
183
+ });
184
+
185
+ return {
186
+ env: normalizedDescriptor.env,
187
+ options: normalizedDescriptor.options,
188
+ auth: normalizedDescriptor.kind === "raw"
189
+ ? null
190
+ : {
191
+ setup({ env }) {
192
+ return buildSessionBundle({
193
+ actorNames,
194
+ contract,
195
+ env,
196
+ primaryActor,
197
+ topology,
198
+ });
199
+ },
200
+ headers(sessionBundle, context = {}) {
201
+ const actorName = context.actor || primaryActor;
202
+ return buildAuthHeaders({
203
+ actorRecord: resolveActorRecord(sessionBundle, actorName),
204
+ auth: contract.session.auth,
205
+ });
206
+ },
207
+ },
208
+ headers(sessionBundle, context = {}) {
209
+ return headerBuilder(sessionBundle, context.env, context.actor || primaryActor);
210
+ },
211
+ rawHeaders(sessionBundle, context = {}) {
212
+ return headerBuilder(sessionBundle, context.env, context.actor || null);
213
+ },
214
+ __testkitAuthProfile: {
215
+ actorNames,
216
+ kind: normalizedDescriptor.kind,
217
+ primaryActor,
218
+ },
219
+ };
220
+ }
221
+
222
+ function buildSessionBundle({ actorNames, contract, env, primaryActor, topology }) {
223
+ const actors = {};
224
+
225
+ actorNames.forEach((actorName, actorIndex) => {
226
+ const actorDefinition = topology.actors[actorName];
227
+ if (!actorDefinition) {
228
+ throw new Error(`Unknown actor "${actorName}" in auth fixture topology`);
229
+ }
230
+ const actorRecord = resolveActorSession({
231
+ actorDefinition,
232
+ actorIndex,
233
+ contract,
234
+ env,
235
+ });
236
+ actors[actorName] = actorRecord;
237
+ });
238
+
239
+ return {
240
+ actors,
241
+ primaryActor,
242
+ };
243
+ }
244
+
245
+ function resolveActorSession({ actorDefinition, actorIndex, contract, env }) {
246
+ const context = {
247
+ actor: actorDefinition.actorName,
248
+ actorIndex,
249
+ env,
250
+ };
251
+
252
+ if (contract.signup.enabled) {
253
+ try {
254
+ runProfileRequest({
255
+ requestConfig: {
256
+ body: () => buildSignupBody(actorDefinition),
257
+ expect: contract.signup.expect,
258
+ method: "POST",
259
+ path: contract.signup.path,
260
+ },
261
+ context: { ...context, phase: "signup" },
262
+ label: `auth.fixture signup for actor "${actorDefinition.actorName}"`,
263
+ });
264
+ } catch {
265
+ // Provisioning is best-effort. Some apps report duplicate-account races as 500s
266
+ // instead of a clean 409, and a successful login is the authoritative signal.
267
+ }
268
+ }
269
+
270
+ const response = runProfileRequest({
271
+ requestConfig: {
272
+ body: () => buildLoginBody(actorDefinition),
273
+ expect: contract.login.expect,
274
+ method: "POST",
275
+ path: contract.login.path,
276
+ },
277
+ context: { ...context, phase: "login" },
278
+ label: `auth.fixture login for actor "${actorDefinition.actorName}"`,
279
+ });
280
+ const session = extractSessionData(response, contract.session);
281
+
282
+ return {
283
+ actorIndex,
284
+ actorName: actorDefinition.actorName,
285
+ email: actorDefinition.email,
286
+ name: actorDefinition.name,
287
+ organizationKey: actorDefinition.organizationKey,
288
+ organizationName: actorDefinition.organizationName,
289
+ session,
290
+ ...session,
291
+ };
292
+ }
293
+
294
+ function buildSignupBody(actorDefinition) {
295
+ return {
296
+ email: actorDefinition.email,
297
+ password: actorDefinition.password,
298
+ name: actorDefinition.name,
299
+ organizationName: actorDefinition.organizationName,
300
+ ...normalizePlainObject(actorDefinition.signupBody, "actor signupBody"),
301
+ };
302
+ }
303
+
304
+ function buildLoginBody(actorDefinition) {
305
+ return {
306
+ email: actorDefinition.email,
307
+ password: actorDefinition.password,
308
+ ...normalizePlainObject(actorDefinition.loginBody, "actor loginBody"),
309
+ };
310
+ }
311
+
312
+ function normalizeContract(contract) {
313
+ if (!contract || contract.kind !== "json-session") {
314
+ throw new Error("auth.fixture(...).contract must come from auth.contracts.jsonSession(...)");
315
+ }
316
+ return contract;
317
+ }
318
+
319
+ function normalizeTopology(topology) {
320
+ if (!topology || topology.kind !== "auth-topology" || !topology.actors) {
321
+ throw new Error("auth.fixture(...).topology must come from auth.topologies.*(...)");
322
+ }
323
+ return topology;
324
+ }
325
+
326
+ function normalizeProfileDescriptor(descriptor, label, topology) {
327
+ if (!descriptor || typeof descriptor !== "object" || Array.isArray(descriptor)) {
328
+ throw new Error(`${label} must be built with auth.profile.*(...)`);
329
+ }
330
+
331
+ if (descriptor.kind === "raw") {
332
+ return descriptor;
333
+ }
334
+
335
+ if (descriptor.kind === "actor") {
336
+ if (!topology.actors[descriptor.actor]) {
337
+ throw new Error(`${label} references unknown actor "${descriptor.actor}"`);
338
+ }
339
+ return descriptor;
340
+ }
341
+
342
+ if (descriptor.kind === "actors") {
343
+ descriptor.actors.forEach((actorName) => {
344
+ if (!topology.actors[actorName]) {
345
+ throw new Error(`${label} references unknown actor "${actorName}"`);
346
+ }
347
+ });
348
+ return descriptor;
349
+ }
350
+
351
+ throw new Error(`${label} must be built with auth.profile.*(...)`);
352
+ }
353
+
354
+ function normalizeTopologyActors(value, defaultOrgKey) {
355
+ if (value == null) {
356
+ if (!defaultOrgKey) {
357
+ return [];
358
+ }
359
+ return [{ actorName: "primary", orgKey: defaultOrgKey, config: {} }];
360
+ }
361
+
362
+ if (Array.isArray(value)) {
363
+ return value.map((actorName) => ({
364
+ actorName: normalizeRequiredString(actorName, "actor name"),
365
+ orgKey: defaultOrgKey || "primary",
366
+ config: {},
367
+ }));
368
+ }
369
+
370
+ if (typeof value !== "object") {
371
+ throw new Error("topology actors must be an array or object");
372
+ }
373
+
374
+ return Object.entries(value).map(([actorName, actorConfig]) => {
375
+ if (typeof actorConfig === "string") {
376
+ return {
377
+ actorName: normalizeRequiredString(actorName, "actor name"),
378
+ orgKey: normalizeRequiredString(actorConfig, `topology actor "${actorName}" org`),
379
+ config: {},
380
+ };
381
+ }
382
+
383
+ const normalizedConfig = normalizePlainObject(actorConfig, `topology actor "${actorName}"`);
384
+ const orgKey = normalizeOptionalString(normalizedConfig.org) || defaultOrgKey;
385
+ if (!orgKey) {
386
+ throw new Error(`topology actor "${actorName}" requires an org`);
387
+ }
388
+ return {
389
+ actorName: normalizeRequiredString(actorName, "actor name"),
390
+ orgKey,
391
+ config: normalizedConfig,
392
+ };
393
+ });
394
+ }
395
+
396
+ function normalizeTopologyOrgs(value, fallbackOrgKeys) {
397
+ const orgs = {};
398
+ const input = value == null ? {} : normalizePlainObject(value, "topology orgs");
399
+
400
+ fallbackOrgKeys.forEach((orgKey) => {
401
+ orgs[orgKey] = normalizePlainObject(input[orgKey], `topology org "${orgKey}"`);
402
+ });
403
+
404
+ for (const [orgKey, orgConfig] of Object.entries(input)) {
405
+ orgs[orgKey] = normalizePlainObject(orgConfig, `topology org "${orgKey}"`);
406
+ }
407
+
408
+ return orgs;
409
+ }
410
+
411
+ function finalizeActorEntries({ namespace, actorEntries, orgs, defaultOrgKey, defaultPassword, label }) {
412
+ const actors = {};
413
+ actorEntries.forEach(({ actorName, orgKey, config }) => {
414
+ const resolvedOrgKey = orgKey || defaultOrgKey;
415
+ if (!resolvedOrgKey) {
416
+ throw new Error(`${label} actor "${actorName}" requires an org`);
417
+ }
418
+ const orgConfig = orgs[resolvedOrgKey] || {};
419
+ actors[actorName] = {
420
+ actorName,
421
+ email:
422
+ normalizeOptionalString(config.email) ||
423
+ buildGeneratedEmail(namespace, actorName),
424
+ loginBody: normalizePlainObject(config.loginBody, `${label} actor "${actorName}" loginBody`),
425
+ name:
426
+ normalizeOptionalString(config.name) ||
427
+ `Testkit ${humanizeToken(namespace)} ${humanizeToken(actorName)}`,
428
+ organizationKey: resolvedOrgKey,
429
+ organizationName:
430
+ normalizeOptionalString(config.organizationName) ||
431
+ normalizeOptionalString(orgConfig.name) ||
432
+ `Testkit ${humanizeToken(namespace)} ${humanizeToken(resolvedOrgKey)} Org`,
433
+ password: normalizeOptionalString(config.password) || defaultPassword,
434
+ signupBody: normalizePlainObject(config.signupBody, `${label} actor "${actorName}" signupBody`),
435
+ };
436
+ });
437
+ return actors;
438
+ }
439
+
440
+ function buildGeneratedEmail(namespace, actorName) {
441
+ return `testkit+${slugifyToken(namespace)}.${slugifyToken(actorName)}@example.test`;
442
+ }
443
+
444
+ function normalizeAuthCaptureConfig(options) {
445
+ const authSourceKey = normalizeOptionalString(options.authSourceKey) || "jwt";
446
+ const authCookie = normalizeOptionalString(options.authCookie);
447
+ if (!authCookie) return null;
448
+
449
+ return {
450
+ cookie: authCookie,
451
+ header: normalizeOptionalString(options.authHeader) || "Authorization",
452
+ prefix: options.authPrefix === undefined ? "Bearer " : String(options.authPrefix),
453
+ source: {
454
+ key: authSourceKey,
455
+ },
456
+ };
457
+ }
458
+
459
+ function normalizeOrganizationHeaderConfig(options) {
460
+ const organizationHeader = options.organizationHeader;
461
+ if (organizationHeader === false) return false;
462
+ if (organizationHeader == null) return "X-Organization-Id";
463
+ if (typeof organizationHeader === "string") return organizationHeader;
464
+ throw new Error("auth.contracts.jsonSession(...).organizationHeader must be a string or false");
465
+ }
466
+
467
+ function buildContractHeaderOptions(contract) {
468
+ const fromSession = [];
469
+ if (contract.organization !== false) {
470
+ fromSession.push({
471
+ field: "organizationId",
472
+ header: contract.organization,
473
+ });
474
+ }
475
+
476
+ return {
477
+ contentTypeJson: contract.contentTypeJson,
478
+ forwardedFor: contract.forwardedFor,
479
+ fromSession,
480
+ values: undefined,
481
+ };
482
+ }
483
+
484
+ function normalizeHeaderOptions(options) {
485
+ if (options == null) return {};
486
+ const normalized = normalizePlainObject(options, "auth profile headers");
487
+ return {
488
+ contentTypeJson: normalized.contentTypeJson,
489
+ forwardedFor: normalized.forwardedFor,
490
+ fromSession: Array.isArray(normalized.fromSession)
491
+ ? normalized.fromSession.map((entry, index) => normalizeFromSessionEntry(entry, index))
492
+ : [],
493
+ values: normalized.values,
494
+ };
495
+ }
496
+
497
+ function mergeHeaderOptions(baseOptions, overrideOptions) {
498
+ const base = normalizeHeaderOptions(baseOptions);
499
+ const override = normalizeHeaderOptions(overrideOptions);
500
+ return {
501
+ contentTypeJson:
502
+ override.contentTypeJson !== undefined ? override.contentTypeJson : base.contentTypeJson,
503
+ forwardedFor: override.forwardedFor !== undefined ? override.forwardedFor : base.forwardedFor,
504
+ fromSession: override.fromSession.length > 0 ? override.fromSession : base.fromSession,
505
+ values: override.values !== undefined ? override.values : base.values,
506
+ };
507
+ }
508
+
509
+ function normalizeFromSessionEntry(entry, index) {
510
+ const normalized = normalizePlainObject(entry, `headers.fromSession[${index}]`);
511
+ return {
512
+ actor: normalizeOptionalString(normalized.actor) || null,
513
+ field: normalizeRequiredString(normalized.field, `headers.fromSession[${index}].field`),
514
+ header: normalizeRequiredString(normalized.header, `headers.fromSession[${index}].header`),
515
+ };
516
+ }
517
+
518
+ function normalizeActorNameList(value) {
519
+ if (value == null) return [];
520
+ if (Array.isArray(value)) {
521
+ return value.map((entry, index) => normalizeRequiredString(entry, `actors[${index}]`));
522
+ }
523
+ throw new Error("auth.profile.actors(...).actors must be an array");
524
+ }
525
+
526
+ function normalizePlainObject(value, label) {
527
+ if (value == null) return {};
528
+ if (typeof value !== "object" || Array.isArray(value)) {
529
+ throw new Error(`${label} must be an object`);
530
+ }
531
+ return { ...value };
532
+ }
533
+
534
+ function normalizeStringMap(value) {
535
+ const normalized = {};
536
+ if (!value) return normalized;
537
+ if (typeof value !== "object" || Array.isArray(value)) {
538
+ throw new Error("Expected a string map");
539
+ }
540
+ for (const [key, entry] of Object.entries(value)) {
541
+ normalized[key] = normalizeRequiredString(entry, `string map entry "${key}"`);
542
+ }
543
+ return normalized;
544
+ }
545
+
546
+ function normalizeRequiredString(value, label) {
547
+ const normalized = normalizeOptionalString(value);
548
+ if (!normalized) {
549
+ throw new Error(`${label} must be a non-empty string`);
550
+ }
551
+ return normalized;
552
+ }
553
+
554
+ function normalizeOptionalString(value) {
555
+ if (value == null) return "";
556
+ const normalized = String(value).trim();
557
+ return normalized.length > 0 ? normalized : "";
558
+ }
559
+
560
+ function humanizeToken(value) {
561
+ return String(value || "")
562
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
563
+ .replace(/[^A-Za-z0-9]+/g, " ")
564
+ .trim()
565
+ .split(/\s+/)
566
+ .filter(Boolean)
567
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
568
+ .join(" ");
569
+ }
570
+
571
+ function slugifyToken(value) {
572
+ return String(value || "")
573
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
574
+ .replace(/[^A-Za-z0-9]+/g, "-")
575
+ .replace(/^-+|-+$/g, "")
576
+ .toLowerCase();
577
+ }
578
+
579
+ function resolveActorRecord(sessionBundle, actorName) {
580
+ if (!sessionBundle?.actors || !actorName) return null;
581
+ return sessionBundle.actors[actorName] || null;
582
+ }
583
+
584
+ function buildAuthHeaders({ actorRecord, auth }) {
585
+ if (!auth || !actorRecord) return {};
586
+ const source = auth.source || {};
587
+ const value = actorRecord[source.key];
588
+ if (!value) return {};
589
+
590
+ const header = auth.header || "Authorization";
591
+ const prefix = auth.prefix === undefined ? "Bearer " : String(auth.prefix);
592
+ return {
593
+ [header]: `${prefix}${value}`,
594
+ };
595
+ }
596
+
597
+ function createCompositeHeaderBuilder({ defaultActor, headers, resolveActorRecord }) {
598
+ const normalized = normalizeHeaderOptions(headers);
599
+
600
+ return function buildHeaders(sessionBundle, env, actorOverride = null) {
601
+ const actorName = actorOverride || defaultActor || null;
602
+ const actorRecord = resolveActorRecord(sessionBundle, actorName);
603
+ const staticValues = resolveFactoryValue(normalized.values, {
604
+ actor: actorName,
605
+ env,
606
+ session: actorRecord,
607
+ }) || {};
608
+ const builtHeaders = {
609
+ ...(normalized.contentTypeJson ? { "Content-Type": "application/json" } : {}),
610
+ ...staticValues,
611
+ };
612
+
613
+ const forwardedFor = buildForwardedForHeader(normalized.forwardedFor, env, actorName);
614
+ if (forwardedFor) {
615
+ builtHeaders[forwardedFor.header] = forwardedFor.value;
616
+ }
617
+
618
+ for (const entry of normalized.fromSession || []) {
619
+ const record = resolveActorRecord(sessionBundle, entry.actor || actorName);
620
+ const value = record ? readValueAtPath(record, entry.field) : null;
621
+ if (value != null && value !== "") {
622
+ builtHeaders[entry.header] = String(value);
623
+ }
624
+ }
625
+
626
+ return builtHeaders;
627
+ };
628
+ }
629
+
630
+ function buildForwardedForHeader(config, env, actorName) {
631
+ if (!config) return null;
632
+ const header = typeof config === "object" && config.header ? config.header : "X-Forwarded-For";
633
+ const seed =
634
+ typeof config === "object" && config.seed
635
+ ? config.seed
636
+ : `${env?.BASE || "testkit"}:${actorName || "raw"}`;
637
+ return {
638
+ header,
639
+ value: deriveDeterministicIp(seed),
640
+ };
641
+ }
642
+
643
+ function deriveDeterministicIp(seed) {
644
+ const text = String(seed || "testkit");
645
+ const octets = [0, 0, 0, 0];
646
+ for (let index = 0; index < text.length; index += 1) {
647
+ octets[index % 4] = (octets[index % 4] * 31 + text.charCodeAt(index)) % 250;
648
+ }
649
+ return `10.${octets[0] + 1}.${octets[1] + 1}.${octets[2] + 1}`;
650
+ }
651
+
652
+ function runProfileRequest({ requestConfig, context, label }) {
653
+ const method = normalizeMethod(requestConfig.method);
654
+ const path = normalizeRequiredString(requestConfig.path, `${label} path`);
655
+ const bodyValue = resolveFactoryValue(requestConfig.body, context);
656
+ const headersValue = resolveFactoryValue(requestConfig.headers, context) || {};
657
+ const headers = {
658
+ ...(requestConfig.contentTypeJson === false ? {} : { "Content-Type": "application/json" }),
659
+ ...(context.env?.routeParams || {}),
660
+ ...headersValue,
661
+ };
662
+ const expectedStatuses = normalizeExpectedStatuses(requestConfig.expect);
663
+ const url = `${context.env.BASE}${path}`;
664
+ const response = dispatchProfileRequest(method, url, bodyValue, headers);
665
+
666
+ if (!expectedStatuses.includes(Number(response?.status))) {
667
+ throw new Error(
668
+ `${label} failed (${response?.status ?? "unknown"}): ${response?.body ?? "no response body"}`
669
+ );
670
+ }
671
+
672
+ return response;
673
+ }
674
+
675
+ function dispatchProfileRequest(method, url, body, headers) {
676
+ if (method === "GET") return runtimeHttp.get(url, { headers });
677
+ if (method === "DELETE") return runtimeHttp.del(url, body == null ? null : JSON.stringify(body), { headers });
678
+ if (method === "PUT") return runtimeHttp.put(url, JSON.stringify(body ?? null), { headers });
679
+ if (method === "PATCH") return runtimeHttp.patch(url, JSON.stringify(body ?? null), { headers });
680
+ return runtimeHttp.post(url, JSON.stringify(body ?? null), { headers });
681
+ }
682
+
683
+ function normalizeMethod(value) {
684
+ const method = String(value || "POST").trim().toUpperCase();
685
+ if (!SUPPORTED_METHODS.has(method)) {
686
+ throw new Error(`Unsupported auth request method "${method}"`);
687
+ }
688
+ return method;
689
+ }
690
+
691
+ function normalizeExpectedStatuses(value) {
692
+ if (value == null) return [200];
693
+ return Array.isArray(value) ? value.map((entry) => Number(entry)) : [Number(value)];
694
+ }
695
+
696
+ function resolveFactoryValue(value, context) {
697
+ if (typeof value === "function") {
698
+ return value(context);
699
+ }
700
+ return value;
701
+ }
702
+
703
+ function extractSessionData(response, sessionConfig) {
704
+ const data = {};
705
+ const body = safeRuntimeJson(response);
706
+
707
+ const cookies = {
708
+ ...(sessionConfig.auth ? { [sessionConfig.auth.source.key]: sessionConfig.auth.cookie } : {}),
709
+ ...sessionConfig.cookies,
710
+ };
711
+ for (const [fieldName, cookieName] of Object.entries(cookies)) {
712
+ data[fieldName] = extractCookieValue(response, cookieName);
713
+ }
714
+
715
+ for (const [fieldName, fieldPath] of Object.entries(sessionConfig.fields || {})) {
716
+ data[fieldName] = readValueAtPath(body, fieldPath);
717
+ }
718
+
719
+ return data;
720
+ }
721
+
722
+ function extractCookieValue(response, cookieName) {
723
+ const cookies = response?.cookies?.[cookieName];
724
+ if (Array.isArray(cookies) && cookies[0]?.value) {
725
+ return cookies[0].value;
726
+ }
727
+
728
+ const headerValues = [];
729
+ for (const [headerName, headerValue] of Object.entries(response?.headers || {})) {
730
+ if (String(headerName).toLowerCase() !== "set-cookie") continue;
731
+ if (Array.isArray(headerValue)) headerValues.push(...headerValue);
732
+ else if (headerValue) headerValues.push(headerValue);
733
+ }
734
+
735
+ const pattern = new RegExp(`(?:^|,\\s*)${cookieName}=([^;,]+)`);
736
+ for (const value of headerValues) {
737
+ const match = pattern.exec(String(value));
738
+ if (match?.[1]) return match[1];
739
+ }
740
+
741
+ return null;
742
+ }
743
+
744
+ function safeRuntimeJson(response) {
745
+ try {
746
+ return runtimeJson(response);
747
+ } catch {
748
+ return null;
749
+ }
750
+ }
751
+
752
+ function readValueAtPath(value, path) {
753
+ if (value == null || typeof path !== "string" || path.trim().length === 0) {
754
+ return null;
755
+ }
756
+ const parts = String(path)
757
+ .replace(/\[(\d+)\]/g, ".$1")
758
+ .split(".")
759
+ .filter(Boolean);
760
+
761
+ let current = value;
762
+ for (const part of parts) {
763
+ if (current == null) return null;
764
+ current = current[part];
765
+ }
766
+ return current ?? null;
767
+ }