apitestcenter 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/THIRD-PARTY-NOTICES-UI.txt +1 -1
  2. package/dist/THIRD-PARTY-NOTICES.txt +1 -1
  3. package/dist/cli.mjs +722 -388
  4. package/dist/ui/404/index.html +1 -1
  5. package/dist/ui/404.html +1 -1
  6. package/dist/ui/__next.__PAGE__.txt +11 -11
  7. package/dist/ui/__next._full.txt +12 -12
  8. package/dist/ui/__next._tree.txt +2 -2
  9. package/dist/ui/_next/static/chunks/0gi0-hmzkqhl_.js +9 -0
  10. package/dist/ui/_next/static/chunks/0k57pe238zzo_.js +119 -0
  11. package/dist/ui/_next/static/chunks/39zwadip857oa.css +3 -0
  12. package/dist/ui/_next/static/chunks/3nzrtm85qus90.js +1 -0
  13. package/dist/ui/_next/static/chunks/{388t7tbteqx_j.js → 3rlhnherpqpp5.js} +73 -73
  14. package/dist/ui/_next/static/chunks/406kf8_89ve9v.js +19 -0
  15. package/dist/ui/_not-found/__next._full.txt +11 -11
  16. package/dist/ui/_not-found/__next._not-found.__PAGE__.txt +10 -10
  17. package/dist/ui/_not-found/__next._tree.txt +2 -2
  18. package/dist/ui/_not-found/index.html +1 -1
  19. package/dist/ui/_not-found/index.txt +11 -11
  20. package/dist/ui/compare/__next._full.txt +12 -12
  21. package/dist/ui/compare/__next._tree.txt +2 -2
  22. package/dist/ui/compare/__next.compare.__PAGE__.txt +11 -11
  23. package/dist/ui/compare/index.html +1 -1
  24. package/dist/ui/compare/index.txt +12 -12
  25. package/dist/ui/history/__next._full.txt +13 -13
  26. package/dist/ui/history/__next._tree.txt +2 -2
  27. package/dist/ui/history/__next.history.__PAGE__.txt +12 -12
  28. package/dist/ui/history/index.html +1 -1
  29. package/dist/ui/history/index.txt +13 -13
  30. package/dist/ui/index.html +1 -1
  31. package/dist/ui/index.txt +12 -12
  32. package/dist/ui/preview/__next._full.txt +13 -13
  33. package/dist/ui/preview/__next._tree.txt +2 -2
  34. package/dist/ui/preview/__next.preview.__PAGE__.txt +12 -12
  35. package/dist/ui/preview/index.html +1 -1
  36. package/dist/ui/preview/index.txt +13 -13
  37. package/dist/ui/suite/__next._full.txt +12 -12
  38. package/dist/ui/suite/__next._tree.txt +2 -2
  39. package/dist/ui/suite/__next.suite.__PAGE__.txt +11 -11
  40. package/dist/ui/suite/index.html +1 -1
  41. package/dist/ui/suite/index.txt +12 -12
  42. package/dist/ui/tests/__next._full.txt +11 -11
  43. package/dist/ui/tests/__next._tree.txt +2 -2
  44. package/dist/ui/tests/__next.tests.__PAGE__.txt +10 -10
  45. package/dist/ui/tests/index.html +1 -1
  46. package/dist/ui/tests/index.txt +11 -11
  47. package/package.json +1 -1
  48. package/dist/ui/_next/static/chunks/1-6_oo-9egb_t.js +0 -1
  49. package/dist/ui/_next/static/chunks/1_537szc_614o.js +0 -1
  50. package/dist/ui/_next/static/chunks/1_q4bnjjia0y5.js +0 -19
  51. package/dist/ui/_next/static/chunks/3_q1wiq483s-m.css +0 -3
  52. package/dist/ui/_next/static/chunks/3jywp_61ylz0z.js +0 -119
  53. /package/dist/ui/_next/static/{Tiehe_bgMIxGzSdltdXXB → wqNU0rhQA3VV8Z_uJQsWF}/_buildManifest.js +0 -0
  54. /package/dist/ui/_next/static/{Tiehe_bgMIxGzSdltdXXB → wqNU0rhQA3VV8Z_uJQsWF}/_clientMiddlewareManifest.js +0 -0
  55. /package/dist/ui/_next/static/{Tiehe_bgMIxGzSdltdXXB → wqNU0rhQA3VV8Z_uJQsWF}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -16866,7 +16866,7 @@ function loadFromLabKnobs(knobs) {
16866
16866
  ...extras
16867
16867
  };
16868
16868
  }
16869
- var httpMethodSchema, durationSchema, headerNameSchema, keyValueSchema, requestHeaderSchema, bodySchema, httpRequestSchema, loadStageSchema, LOAD_EXECUTORS, loadConfigSchema, runTargetsSchema, thresholdSchema, httpClientOptionsSchema, oauthGrants, oauthConfigSchema, sessionAuthSchema, VARIABLE_NAME, variableNameSchema, extractRuleSchema, setupStepSchema, testDataSchema, testDefinitionSchema, requestScriptsSchema, labRequestSchema, labRunSchema, loadExtras, LOAD_PRESETS, LOAD_KNOB_PRESETS;
16869
+ var httpMethodSchema, durationSchema, headerNameSchema, keyValueSchema, requestHeaderSchema, fileRefSchema, multipartPartSchema, bodySchema, httpRequestSchema, loadStageSchema, LOAD_EXECUTORS, loadConfigSchema, runTargetsSchema, thresholdSchema, httpClientOptionsSchema, oauthGrants, oauthConfigSchema, sessionAuthSchema, VARIABLE_NAME, variableNameSchema, extractRuleSchema, setupStepSchema, testDataSchema, testDefinitionSchema, requestScriptsSchema, labRequestSchema, labRunSchema, loadExtras, LOAD_PRESETS, LOAD_KNOB_PRESETS;
16870
16870
  var init_definition = __esm({
16871
16871
  "../shared/src/definition.ts"() {
16872
16872
  "use strict";
@@ -16894,6 +16894,16 @@ var init_definition = __esm({
16894
16894
  key: headerNameSchema,
16895
16895
  value: external_exports.string()
16896
16896
  });
16897
+ fileRefSchema = external_exports.object({
16898
+ id: external_exports.string().regex(/^[a-z0-9-]{8,64}$/, "not a file id"),
16899
+ name: external_exports.string().min(1).max(255),
16900
+ size: external_exports.number().int().nonnegative(),
16901
+ contentType: external_exports.string().max(200).default("application/octet-stream")
16902
+ });
16903
+ multipartPartSchema = external_exports.discriminatedUnion("kind", [
16904
+ external_exports.object({ kind: external_exports.literal("text"), key: external_exports.string().min(1), value: external_exports.string() }).strict(),
16905
+ external_exports.object({ kind: external_exports.literal("file"), key: external_exports.string().min(1), file: fileRefSchema }).strict()
16906
+ ]);
16897
16907
  bodySchema = external_exports.discriminatedUnion("mode", [
16898
16908
  external_exports.object({ mode: external_exports.literal("none") }).strict(),
16899
16909
  external_exports.object({ mode: external_exports.literal("json"), json: external_exports.unknown() }).strict(),
@@ -16906,6 +16916,17 @@ var init_definition = __esm({
16906
16916
  mode: external_exports.literal("raw"),
16907
16917
  raw: external_exports.string(),
16908
16918
  contentType: external_exports.string().min(1)
16919
+ }).strict(),
16920
+ // A form with files (multipart/form-data): text fields and files, in order.
16921
+ external_exports.object({
16922
+ mode: external_exports.literal("multipart"),
16923
+ parts: external_exports.array(multipartPartSchema).max(100)
16924
+ }).strict(),
16925
+ // One file as the whole body (an image, a PDF, a ZIP…).
16926
+ external_exports.object({
16927
+ mode: external_exports.literal("file"),
16928
+ file: fileRefSchema,
16929
+ contentType: external_exports.string().min(1)
16909
16930
  }).strict()
16910
16931
  ]);
16911
16932
  httpRequestSchema = external_exports.object({
@@ -17203,7 +17224,7 @@ function fillLab(lab, vars2, { builtins = true } = {}) {
17203
17224
  const deep = (v) => fillDeep(v, vars2, builtins);
17204
17225
  const { request: request3, auth } = lab;
17205
17226
  const body = request3.body;
17206
- const filledBody = body.mode === "json" ? { ...body, json: deep(body.json) } : body.mode === "text" ? { ...body, text: raw(body.text) } : body.mode === "form" ? { ...body, fields: body.fields.map((f) => ({ key: raw(f.key), value: raw(f.value) })) } : body.mode === "raw" ? { ...body, raw: raw(body.raw) } : body;
17227
+ const filledBody = body.mode === "json" ? { ...body, json: deep(body.json) } : body.mode === "text" ? { ...body, text: raw(body.text) } : body.mode === "form" ? { ...body, fields: body.fields.map((f) => ({ key: raw(f.key), value: raw(f.value) })) } : body.mode === "raw" ? { ...body, raw: raw(body.raw) } : body.mode === "multipart" ? { ...body, parts: body.parts.map((p) => p.kind === "text" ? { ...p, key: raw(p.key), value: raw(p.value) } : { ...p, key: raw(p.key) }) } : body;
17207
17228
  const filledAuth = auth.mode === "bearer" ? { ...auth, token: raw(auth.token) } : auth.mode === "basic" ? { ...auth, username: raw(auth.username), password: raw(auth.password) } : auth.mode === "api_key" ? { ...auth, name: raw(auth.name), value: raw(auth.value) } : auth;
17208
17229
  return {
17209
17230
  ...lab,
@@ -20740,9 +20761,9 @@ var require_stringifyDocument = __commonJS({
20740
20761
  const lines = [];
20741
20762
  let hasDirectives = options.directives === true;
20742
20763
  if (options.directives !== false && doc.directives) {
20743
- const dir2 = doc.directives.toString(doc);
20744
- if (dir2) {
20745
- lines.push(dir2);
20764
+ const dir3 = doc.directives.toString(doc);
20765
+ if (dir3) {
20766
+ lines.push(dir3);
20746
20767
  hasDirectives = true;
20747
20768
  } else if (doc.directives.docStart)
20748
20769
  hasDirectives = true;
@@ -24856,6 +24877,9 @@ function defaultRequestDraft() {
24856
24877
  graphqlOperation: "",
24857
24878
  graphqlErrorsFail: true,
24858
24879
  bodyText: "",
24880
+ formRows: [],
24881
+ bodyFile: null,
24882
+ bodyFileType: "",
24859
24883
  authMode: "inherit",
24860
24884
  oauth: defaultOAuth(),
24861
24885
  oauth1: defaultOAuth1(),
@@ -24905,6 +24929,17 @@ function checksFrom(value) {
24905
24929
  return parsed.success ? [parsed.data] : [];
24906
24930
  });
24907
24931
  }
24932
+ function fileFrom(value) {
24933
+ const parsed = fileRefSchema.safeParse(value);
24934
+ return parsed.success ? parsed.data : null;
24935
+ }
24936
+ function formRowsFrom(value) {
24937
+ if (!Array.isArray(value)) return [];
24938
+ return value.slice(0, 100).map((row) => {
24939
+ const item = row ?? {};
24940
+ return { key: typeof item.key === "string" ? item.key : "", value: typeof item.value === "string" ? item.value : "", kind: item.kind === "file" ? "file" : "text", file: fileFrom(item.file) };
24941
+ });
24942
+ }
24908
24943
  function rows(value) {
24909
24944
  if (!Array.isArray(value)) return [];
24910
24945
  return value.map((row) => {
@@ -24919,7 +24954,7 @@ function requestFromUnknown(value) {
24919
24954
  const base = defaultRequestDraft();
24920
24955
  if (!value || typeof value !== "object") return base;
24921
24956
  const item = value;
24922
- const bodyMode = item.bodyMode === "json" || item.bodyMode === "text" || item.bodyMode === "graphql" ? item.bodyMode : "none";
24957
+ const bodyMode = BODY_MODES.includes(item.bodyMode) ? item.bodyMode : "none";
24923
24958
  const authMode2 = item.authMode === "bearer" || item.authMode === "basic" || item.authMode === "api_key" || item.authMode === "inherit" || item.authMode === "oauth2" || item.authMode === "digest" || item.authMode === "ntlm" || item.authMode === "aws_sigv4" || item.authMode === "jwt" || item.authMode === "oauth1" ? item.authMode : item.authMode === void 0 ? "inherit" : "none";
24924
24959
  return {
24925
24960
  method: METHODS.includes(item.method) ? item.method : "GET",
@@ -24930,6 +24965,9 @@ function requestFromUnknown(value) {
24930
24965
  headers: rows(item.headers),
24931
24966
  bodyMode,
24932
24967
  bodyText: str(item.bodyText, ""),
24968
+ formRows: formRowsFrom(item.formRows),
24969
+ bodyFile: fileFrom(item.bodyFile),
24970
+ bodyFileType: str(item.bodyFileType, ""),
24933
24971
  graphqlQuery: str(item.graphqlQuery, ""),
24934
24972
  graphqlVariables: str(item.graphqlVariables, ""),
24935
24973
  graphqlOperation: str(item.graphqlOperation, ""),
@@ -25085,6 +25123,20 @@ function requestParts(draft) {
25085
25123
  body = { mode: "json", json: { query: draft.graphqlQuery, variables, ...draft.graphqlOperation.trim() ? { operationName: draft.graphqlOperation.trim() } : {} } };
25086
25124
  } else if (hasBody(draft.method) && draft.bodyMode === "text") {
25087
25125
  body = { mode: "text", text: draft.bodyText };
25126
+ } else if (hasBody(draft.method) && draft.bodyMode === "form") {
25127
+ body = { mode: "form", fields: draft.formRows.filter((r) => r.kind === "text" && r.key.trim()).map((r) => ({ key: r.key, value: r.value })) };
25128
+ } else if (hasBody(draft.method) && draft.bodyMode === "multipart") {
25129
+ const parts2 = [];
25130
+ for (const r of draft.formRows) {
25131
+ if (!r.key.trim()) continue;
25132
+ if (r.kind === "text") parts2.push({ kind: "text", key: r.key, value: r.value });
25133
+ else if (r.file) parts2.push({ kind: "file", key: r.key, file: r.file });
25134
+ else throw new Error(`Choose a file for "${r.key}" in the body`);
25135
+ }
25136
+ body = { mode: "multipart", parts: parts2 };
25137
+ } else if (hasBody(draft.method) && draft.bodyMode === "file") {
25138
+ if (!draft.bodyFile) throw new Error("Choose the file to send as the body");
25139
+ body = { mode: "file", file: draft.bodyFile, contentType: draft.bodyFileType.trim() || draft.bodyFile.contentType || "application/octet-stream" };
25088
25140
  }
25089
25141
  return {
25090
25142
  request: {
@@ -25250,7 +25302,7 @@ function oauthWire(o) {
25250
25302
  assertion: o.assertion.trim()
25251
25303
  };
25252
25304
  }
25253
- var METHODS, DEFAULT_LOAD, str, num, TEMPLATED, DURATION;
25305
+ var METHODS, BODY_MODES, DEFAULT_LOAD, str, num, TEMPLATED, DURATION;
25254
25306
  var init_draft = __esm({
25255
25307
  "../shared/src/draft.ts"() {
25256
25308
  "use strict";
@@ -25263,6 +25315,7 @@ var init_draft = __esm({
25263
25315
  init_url_parts();
25264
25316
  init_workspace();
25265
25317
  METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
25318
+ BODY_MODES = ["none", "json", "text", "graphql", "form", "multipart", "file"];
25266
25319
  DEFAULT_LOAD = { preset: "smoke", ...LOAD_KNOB_PRESETS.smoke };
25267
25320
  str = (v, fallback) => typeof v === "string" ? v : fallback;
25268
25321
  num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
@@ -26500,6 +26553,91 @@ var init_graphql = __esm({
26500
26553
  }
26501
26554
  });
26502
26555
 
26556
+ // ../shared/src/multipart.ts
26557
+ function multipartBoundary() {
26558
+ let s = "";
26559
+ for (let i = 0; i < 24; i++) s += "0123456789abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 36)];
26560
+ return `----ApiTestCenter${s}`;
26561
+ }
26562
+ function quoted(name) {
26563
+ return `"${name.replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A")}"`;
26564
+ }
26565
+ function partHead(part, boundary) {
26566
+ if (part.kind === "text") return `--${boundary}\r
26567
+ Content-Disposition: form-data; name=${quoted(part.key)}\r
26568
+ \r
26569
+ `;
26570
+ return `--${boundary}\r
26571
+ Content-Disposition: form-data; name=${quoted(part.key)}; filename=${quoted(part.file.name)}\r
26572
+ Content-Type: ${part.file.contentType || "application/octet-stream"}\r
26573
+ \r
26574
+ `;
26575
+ }
26576
+ function multipartContentType(boundary) {
26577
+ return `multipart/form-data; boundary=${boundary}`;
26578
+ }
26579
+ function buildMultipart(parts, boundary) {
26580
+ const enc = new TextEncoder();
26581
+ const chunks = [];
26582
+ for (const part of parts) {
26583
+ chunks.push(enc.encode(partHead(part, boundary)));
26584
+ chunks.push(part.kind === "text" ? enc.encode(part.value) : part.bytes);
26585
+ chunks.push(enc.encode("\r\n"));
26586
+ }
26587
+ chunks.push(enc.encode(`--${boundary}--\r
26588
+ `));
26589
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
26590
+ let at = 0;
26591
+ for (const c of chunks) {
26592
+ out.set(c, at);
26593
+ at += c.length;
26594
+ }
26595
+ return out;
26596
+ }
26597
+ function formatBytes(n) {
26598
+ if (n < 1024) return `${n} B`;
26599
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
26600
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
26601
+ }
26602
+ function multipartPreview(parts, boundary) {
26603
+ let out = "";
26604
+ for (const part of parts) out += partHead(part, boundary) + (part.kind === "text" ? part.value : `<file: ${part.file.name}, ${formatBytes(part.file.size)}>`) + "\r\n";
26605
+ return `${out}--${boundary}--\r
26606
+ `;
26607
+ }
26608
+ function guessContentType(name) {
26609
+ const ext = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase() ?? "";
26610
+ const types = {
26611
+ json: "application/json",
26612
+ xml: "application/xml",
26613
+ pdf: "application/pdf",
26614
+ zip: "application/zip",
26615
+ gz: "application/gzip",
26616
+ csv: "text/csv",
26617
+ txt: "text/plain",
26618
+ html: "text/html",
26619
+ png: "image/png",
26620
+ jpg: "image/jpeg",
26621
+ jpeg: "image/jpeg",
26622
+ gif: "image/gif",
26623
+ webp: "image/webp",
26624
+ svg: "image/svg+xml",
26625
+ mp4: "video/mp4",
26626
+ mp3: "audio/mpeg",
26627
+ wav: "audio/wav",
26628
+ doc: "application/msword",
26629
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
26630
+ xls: "application/vnd.ms-excel",
26631
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
26632
+ };
26633
+ return types[ext] ?? "application/octet-stream";
26634
+ }
26635
+ var init_multipart = __esm({
26636
+ "../shared/src/multipart.ts"() {
26637
+ "use strict";
26638
+ }
26639
+ });
26640
+
26503
26641
  // ../shared/src/export-fields.ts
26504
26642
  function roundN(n, digits = 2) {
26505
26643
  if (!Number.isFinite(n)) return 0;
@@ -27462,6 +27600,7 @@ var init_src = __esm({
27462
27600
  init_sigv4();
27463
27601
  init_oauth1();
27464
27602
  init_graphql();
27603
+ init_multipart();
27465
27604
  init_latency_bins();
27466
27605
  init_checks3();
27467
27606
  init_definition();
@@ -27527,8 +27666,8 @@ token, password, or key.
27527
27666
  entries = /* @__PURE__ */ new Map();
27528
27667
  loaded = false;
27529
27668
  queue = Promise.resolve();
27530
- constructor(dir2 = resolveDataDir()) {
27531
- this.dir = dir2;
27669
+ constructor(dir3 = resolveDataDir()) {
27670
+ this.dir = dir3;
27532
27671
  }
27533
27672
  /** Run one mutation at a time, so two saves never race on index.json or a baseline flip. */
27534
27673
  serial(task) {
@@ -27855,6 +27994,61 @@ var init_k6 = __esm({
27855
27994
  }
27856
27995
  });
27857
27996
 
27997
+ // ../../apps/engine/src/file-store.ts
27998
+ import { randomUUID as randomUUID2 } from "node:crypto";
27999
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
28000
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
28001
+ import { join as join4 } from "node:path";
28002
+ function useFileStore(dataDir) {
28003
+ dir = join4(dataDir, "files");
28004
+ }
28005
+ function filesDir() {
28006
+ return dir || join4(resolveDataDir(), "files");
28007
+ }
28008
+ async function saveFile(name, bytes2, contentType) {
28009
+ if (bytes2.length > FILE_MAX) throw Object.assign(new Error("A file can be up to 50 MB"), { statusCode: 413 });
28010
+ const id = randomUUID2();
28011
+ await mkdir2(filesDir(), { recursive: true });
28012
+ await writeFile2(join4(filesDir(), id), bytes2);
28013
+ const clean = name.replace(/[\\/]/g, "_").trim().slice(0, 255) || "file";
28014
+ const type = contentType && contentType !== "application/octet-stream" ? contentType : guessContentType(clean);
28015
+ return { id, name: clean, size: bytes2.length, contentType: type };
28016
+ }
28017
+ function fileBytes(file2) {
28018
+ const path = join4(filesDir(), file2.id);
28019
+ if (!/^[a-z0-9-]{8,64}$/.test(file2.id) || !existsSync2(path)) {
28020
+ throw new Error(`The file "${file2.name}" isn't in this data folder (${filesDir()}) \u2014 choose it again in the Body tab`);
28021
+ }
28022
+ return readFileSync(path);
28023
+ }
28024
+ function encodeMultipart(parts) {
28025
+ const boundary = multipartBoundary();
28026
+ const filled = parts.map((p) => p.kind === "text" ? p : { ...p, bytes: fileBytes(p.file) });
28027
+ return { bytes: Buffer.from(buildMultipart(filled, boundary)), contentType: multipartContentType(boundary), preview: multipartPreview(parts, boundary) };
28028
+ }
28029
+ function registerFileRoutes(api) {
28030
+ api.addContentTypeParser("*", { parseAs: "buffer", bodyLimit: FILE_MAX }, (_req, body, done) => done(null, body));
28031
+ api.post("/files", { bodyLimit: FILE_MAX }, async (request3, reply) => {
28032
+ const body = request3.body;
28033
+ if (!Buffer.isBuffer(body)) return reply.code(400).send({ error: "Send the file's bytes as the body" });
28034
+ try {
28035
+ return await saveFile(String(request3.query.name ?? "file"), body, request3.query.type || void 0);
28036
+ } catch (err) {
28037
+ return reply.code(err.statusCode ?? 500).send({ error: err instanceof Error ? err.message : "Could not keep the file" });
28038
+ }
28039
+ });
28040
+ }
28041
+ var FILE_MAX, dir;
28042
+ var init_file_store = __esm({
28043
+ "../../apps/engine/src/file-store.ts"() {
28044
+ "use strict";
28045
+ init_src();
28046
+ init_history_store();
28047
+ FILE_MAX = 50 * 1024 * 1024;
28048
+ dir = "";
28049
+ }
28050
+ });
28051
+
27858
28052
  // ../shared/src/k6-script.ts
27859
28053
  function targetUrl(run2) {
27860
28054
  const url2 = new URL(run2.request.url);
@@ -27874,18 +28068,22 @@ function publicHeaders(run2) {
27874
28068
  if (pair.key) headers[pair.key] = pair.value;
27875
28069
  }
27876
28070
  const body = run2.request.body;
28071
+ if (body.mode === "multipart") {
28072
+ for (const k of Object.keys(headers)) if (k.toLowerCase() === "content-type") delete headers[k];
28073
+ }
27877
28074
  const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
27878
28075
  if (!hasContentType) {
27879
28076
  if (body.mode === "json") headers["content-type"] = "application/json";
27880
28077
  else if (body.mode === "text") headers["content-type"] = "text/plain";
27881
28078
  else if (body.mode === "form") headers["content-type"] = "application/x-www-form-urlencoded";
27882
28079
  else if (body.mode === "raw") headers["content-type"] = body.contentType;
28080
+ else if (body.mode === "file") headers["content-type"] = body.contentType;
27883
28081
  }
27884
28082
  return headers;
27885
28083
  }
27886
28084
  function encodeBody(run2) {
27887
28085
  const { body } = run2.request;
27888
- if (body.mode === "none") return "";
28086
+ if (body.mode === "none" || body.mode === "multipart" || body.mode === "file") return "";
27889
28087
  if (body.mode === "json") {
27890
28088
  return typeof body.json === "string" ? body.json : JSON.stringify(body.json ?? {});
27891
28089
  }
@@ -28017,6 +28215,35 @@ function generateK6Script(run2, extra2 = {}) {
28017
28215
  }
28018
28216
  const bodyMode = run2.request.body.mode;
28019
28217
  env.ATC_BODY_FILL = bodyMode === "json" ? "json" : bodyMode === "form" ? "url" : "raw";
28218
+ const reqBody = run2.request.body;
28219
+ const bytesOf = (id, name) => {
28220
+ const bytes2 = extra2.fileBytes?.[id];
28221
+ if (!bytes2) throw new Error(`The file "${name}" isn't available for the load test`);
28222
+ return bytes2;
28223
+ };
28224
+ const partFiles = [];
28225
+ let bodyBin = false;
28226
+ if (reqBody.mode === "file") {
28227
+ files["body.bin"] = bytesOf(reqBody.file.id, reqBody.file.name);
28228
+ bodyBin = true;
28229
+ } else if (reqBody.mode === "multipart" && run2.auth.mode === "aws_sigv4") {
28230
+ const boundary = multipartBoundary();
28231
+ const parts = reqBody.parts.map((p) => p.kind === "text" ? p : { ...p, bytes: bytesOf(p.file.id, p.file.name) });
28232
+ files["body.bin"] = buildMultipart(parts, boundary);
28233
+ const headers = JSON.parse(env.ATC_HEADERS);
28234
+ headers["content-type"] = multipartContentType(boundary);
28235
+ env.ATC_HEADERS = JSON.stringify(headers);
28236
+ bodyBin = true;
28237
+ } else if (reqBody.mode === "multipart") {
28238
+ const parts = reqBody.parts.map((p, i) => {
28239
+ if (p.kind === "text") return { key: p.key, value: p.value };
28240
+ const name = `file-${i}.bin`;
28241
+ files[name] = bytesOf(p.file.id, p.file.name);
28242
+ partFiles.push(name);
28243
+ return { key: p.key, file: name, name: p.file.name, type: p.file.contentType };
28244
+ });
28245
+ files["parts.json"] = JSON.stringify(parts);
28246
+ }
28020
28247
  if (data) {
28021
28248
  files["data.json"] = JSON.stringify(data.rows);
28022
28249
  env.ATC_DATA_COLUMNS = JSON.stringify(data.columns);
@@ -28070,7 +28297,13 @@ function __atcKeep(out) {
28070
28297
  }
28071
28298
  ` : ""}${run2.auth.mode === "aws_sigv4" ? `
28072
28299
  const AWS = JSON.parse(__ENV.ATC_AWS);
28073
- ${SIGV4_JS}` : ""}${run2.auth.mode === "oauth1" ? `
28300
+ ${SIGV4_JS}` : ""}${bodyBin ? `
28301
+ // The body: a file, read once.
28302
+ const __atcBodyBin = open("./body.bin", "b");` : ""}${files["parts.json"] ? `
28303
+ // A form with files: the fields, and the files, read once.
28304
+ const __atcParts = JSON.parse(open("./parts.json"));
28305
+ const __atcPartFiles = {};
28306
+ ${partFiles.map((n) => `__atcPartFiles[${JSON.stringify(n)}] = open(${JSON.stringify(`./${n}`)}, "b");`).join("\n")}` : ""}${run2.auth.mode === "oauth1" ? `
28074
28307
  const OAUTH1 = JSON.parse(__ENV.ATC_OAUTH1);
28075
28308
  ${OAUTH1_JS}` : ""}
28076
28309
  const CHECKS = __ENV.ATC_CHECKS_FILE ? JSON.parse(open("./checks.json")) : JSON.parse(__ENV.ATC_CHECKS || "[]");
@@ -28127,7 +28360,11 @@ export default ${scripts ? "async " : ""}function () {
28127
28360
  body = out.request.body === undefined ? null : f(out.request.body, __ENV.ATC_BODY_FILL);
28128
28361
  if (out.error) check(null, { "pre-request script ran": () => false });
28129
28362
  }` : ""}
28130
- if (__ENV.ATC_COOKIE) headers.Cookie = __ENV.ATC_COOKIE;
28363
+ ${bodyBin ? ` body = __atcBodyBin;
28364
+ ` : files["parts.json"] ? ` // Test data and built-in values fill the text fields, for each request.
28365
+ body = {};
28366
+ for (const p of __atcParts) body[f(p.key)] = p.file ? http.file(__atcPartFiles[p.file], p.name, p.type) : f(p.value);
28367
+ ` : ""} if (__ENV.ATC_COOKIE) headers.Cookie = __ENV.ATC_COOKIE;
28131
28368
  if (__ENV.ATC_TOKEN) headers.Authorization = "Bearer " + f(__atcFreshToken());
28132
28369
  if (__ENV.ATC_BASIC_USER !== undefined) headers.Authorization = "Basic " + encoding.b64encode(f(__ENV.ATC_BASIC_USER) + ":" + f(__ENV.ATC_BASIC_PASS || ""));
28133
28370
  if (__ENV.ATC_API_NAME && __ENV.ATC_API_KEY) headers[f(__ENV.ATC_API_NAME)] = f(__ENV.ATC_API_KEY);
@@ -28149,7 +28386,7 @@ export default ${scripts ? "async " : ""}function () {
28149
28386
  const named = {};
28150
28387
  for (const item of CHECKS) named[item.label] = () => __atcCheck(item.check, view);${scripts ? `
28151
28388
  if (__atcPost) {
28152
- const out = await __atcScriptRunAsync(__atcPost, { phase: "post", name: __ENV.ATC_NAME, iteration: __atcIteration, request: { method: method, url: target, headers: [], body: body === null ? undefined : body }, response: { code: res.status, headers: lower, body: view.body || "", responseTime: res.timings.duration }, vars: vars }, __atcHost);
28389
+ const out = await __atcScriptRunAsync(__atcPost, { phase: "post", name: __ENV.ATC_NAME, iteration: __atcIteration, request: { method: method, url: target, headers: [], body: typeof body === "string" ? body : undefined }, response: { code: res.status, headers: lower, body: view.body || "", responseTime: res.timings.duration }, vars: vars }, __atcHost);
28153
28390
  __atcKeep(out);
28154
28391
  // Each pm.test is a check; a test that throws in one request fails in that one only.
28155
28392
  for (const t of out.tests) named[t.name] = () => t.ok;
@@ -28178,6 +28415,7 @@ var init_k6_script = __esm({
28178
28415
  init_scripts();
28179
28416
  init_oauth1();
28180
28417
  init_sigv4();
28418
+ init_multipart();
28181
28419
  init_definition();
28182
28420
  }
28183
28421
  });
@@ -32588,9 +32826,9 @@ var require_sha256 = __commonJS({
32588
32826
  var forge2 = require_forge();
32589
32827
  require_md();
32590
32828
  require_util();
32591
- var sha256 = module.exports = forge2.sha256 = forge2.sha256 || {};
32592
- forge2.md.sha256 = forge2.md.algorithms.sha256 = sha256;
32593
- sha256.create = function() {
32829
+ var sha2562 = module.exports = forge2.sha256 = forge2.sha256 || {};
32830
+ forge2.md.sha256 = forge2.md.algorithms.sha256 = sha2562;
32831
+ sha2562.create = function() {
32594
32832
  if (!_initialized) {
32595
32833
  _init();
32596
32834
  }
@@ -46008,14 +46246,14 @@ var require_lib = __commonJS({
46008
46246
  });
46009
46247
 
46010
46248
  // ../../apps/engine/src/certificates.ts
46011
- import { randomUUID as randomUUID2 } from "node:crypto";
46012
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
46013
- import { join as join4 } from "node:path";
46249
+ import { randomUUID as randomUUID3 } from "node:crypto";
46250
+ import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
46251
+ import { join as join5 } from "node:path";
46014
46252
  import { X509Certificate } from "node:crypto";
46015
46253
  import * as tlsModule from "node:tls";
46016
46254
  import { connect as tlsSocket, createSecureContext, rootCertificates } from "node:tls";
46017
46255
  function useCertificateStore(dataDir, store3) {
46018
- dir = dataDir;
46256
+ dir2 = dataDir;
46019
46257
  secrets = store3;
46020
46258
  }
46021
46259
  function addTemporaryCertificate(host2, material) {
@@ -46039,9 +46277,9 @@ function hostMatches(pattern, url2) {
46039
46277
  return name === host2;
46040
46278
  }
46041
46279
  async function readStore() {
46042
- if (!dir) return { certificates: [], cas: [] };
46280
+ if (!dir2) return { certificates: [], cas: [] };
46043
46281
  try {
46044
- const raw = JSON.parse(await readFile2(join4(dir, "certificates.json"), "utf8"));
46282
+ const raw = JSON.parse(await readFile2(join5(dir2, "certificates.json"), "utf8"));
46045
46283
  return {
46046
46284
  certificates: Array.isArray(raw.certificates) ? raw.certificates.filter((c) => c && typeof c.id === "string" && typeof c.host === "string") : [],
46047
46285
  cas: Array.isArray(raw.cas) ? raw.cas.filter((c) => c && typeof c.id === "string" && typeof c.pem === "string") : []
@@ -46054,10 +46292,10 @@ async function readAll() {
46054
46292
  return (await readStore()).certificates;
46055
46293
  }
46056
46294
  async function writeStore(next) {
46057
- await mkdir2(dir, { recursive: true });
46058
- const file2 = join4(dir, "certificates.json");
46295
+ await mkdir3(dir2, { recursive: true });
46296
+ const file2 = join5(dir2, "certificates.json");
46059
46297
  const tmp = `${file2}.${process.pid}.tmp`;
46060
- await writeFile2(tmp, `${JSON.stringify({ version: 1, ...next }, null, 2)}
46298
+ await writeFile3(tmp, `${JSON.stringify({ version: 1, ...next }, null, 2)}
46061
46299
  `, "utf8");
46062
46300
  await rename2(tmp, file2);
46063
46301
  }
@@ -46081,7 +46319,7 @@ async function addCas(pemText) {
46081
46319
  if (!x.ca) throw new Error(`${x.subject.split("\n").find((l) => l.startsWith("CN="))?.slice(3) ?? "That certificate"} is not a certificate authority \u2014 add the CA that issued the server's certificate`);
46082
46320
  if (store3.cas.some((c) => c.pem.replace(/\s/g, "") === pem.replace(/\s/g, ""))) continue;
46083
46321
  const cn = x.subject.split("\n").find((l) => l.startsWith("CN="))?.slice(3) ?? x.subject.replace(/\n/g, ", ");
46084
- store3.cas.push({ id: randomUUID2(), subject: cn, expires: new Date(x.validTo).toISOString(), pem });
46322
+ store3.cas.push({ id: randomUUID3(), subject: cn, expires: new Date(x.validTo).toISOString(), pem });
46085
46323
  }
46086
46324
  await writeStore(store3);
46087
46325
  casCache = null;
@@ -46169,7 +46407,7 @@ async function saveCertificate(input) {
46169
46407
  if (input.kind === "pem" && (!material.cert || !material.key)) throw new Error("Choose the certificate file and its private key file");
46170
46408
  checkMaterial(material);
46171
46409
  const all = await readAll();
46172
- const id = input.id && all.some((c) => c.id === input.id) ? input.id : randomUUID2();
46410
+ const id = input.id && all.some((c) => c.id === input.id) ? input.id : randomUUID3();
46173
46411
  await secrets.deletePrefix(`cert:${id}:`);
46174
46412
  if (material.key) await secrets.set(`cert:${id}:key`, material.key);
46175
46413
  if (material.pfx) await secrets.set(`cert:${id}:pfx`, material.pfx.toString("base64"));
@@ -46288,13 +46526,13 @@ function registerCertificateRoutes(app2) {
46288
46526
  return { host: m?.host ?? null };
46289
46527
  });
46290
46528
  }
46291
- var import_node_forge, dir, secrets, extra, casCache, systemCache, saveBody;
46529
+ var import_node_forge, dir2, secrets, extra, casCache, systemCache, saveBody;
46292
46530
  var init_certificates = __esm({
46293
46531
  "../../apps/engine/src/certificates.ts"() {
46294
46532
  "use strict";
46295
46533
  import_node_forge = __toESM(require_lib(), 1);
46296
46534
  init_zod();
46297
- dir = "";
46535
+ dir2 = "";
46298
46536
  secrets = null;
46299
46537
  extra = [];
46300
46538
  casCache = null;
@@ -61284,7 +61522,7 @@ var require_snapshot_utils = __commonJS({
61284
61522
  var require_snapshot_recorder = __commonJS({
61285
61523
  "../../node_modules/.pnpm/undici@8.10.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
61286
61524
  "use strict";
61287
- var { writeFile: writeFile11, readFile: readFile12, mkdir: mkdir8 } = __require("node:fs/promises");
61525
+ var { writeFile: writeFile12, readFile: readFile12, mkdir: mkdir9 } = __require("node:fs/promises");
61288
61526
  var { dirname: dirname4, resolve: resolve5 } = __require("node:path");
61289
61527
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
61290
61528
  var { InvalidArgumentError, UndiciError } = require_errors2();
@@ -61531,12 +61769,12 @@ var require_snapshot_recorder = __commonJS({
61531
61769
  throw new InvalidArgumentError("Snapshot path is required");
61532
61770
  }
61533
61771
  const resolvedPath = resolve5(path);
61534
- await mkdir8(dirname4(resolvedPath), { recursive: true });
61772
+ await mkdir9(dirname4(resolvedPath), { recursive: true });
61535
61773
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
61536
61774
  hash: hash2,
61537
61775
  snapshot: snapshot2
61538
61776
  }));
61539
- await writeFile11(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
61777
+ await writeFile12(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
61540
61778
  }
61541
61779
  /**
61542
61780
  * Clears all recorded snapshots
@@ -73430,7 +73668,7 @@ ${captureLines}` : capture.stack;
73430
73668
  });
73431
73669
 
73432
73670
  // ../../apps/engine/src/oauth.ts
73433
- import { X509Certificate as X509Certificate2, createHash as createHash2, randomBytes, randomUUID as randomUUID3 } from "node:crypto";
73671
+ import { X509Certificate as X509Certificate2, createHash as createHash2, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
73434
73672
  import { createServer } from "node:http";
73435
73673
  function useOAuthStore(secrets2) {
73436
73674
  store2 = secrets2;
@@ -73473,7 +73711,7 @@ function signedAssertion(c, claims, now) {
73473
73711
  const x5t = thumbprint(c.certificate);
73474
73712
  try {
73475
73713
  return signJwt(
73476
- { algorithm: c.assertionAlgorithm, key: c.privateKey, payload: JSON.stringify({ jti: randomUUID3(), ...claims }), lifetimeSec: 300, kid: c.kid, headerName: "", prefix: "" },
73714
+ { algorithm: c.assertionAlgorithm, key: c.privateKey, payload: JSON.stringify({ jti: randomUUID4(), ...claims }), lifetimeSec: 300, kid: c.kid, headerName: "", prefix: "" },
73477
73715
  now,
73478
73716
  x5t ? { x5t } : {}
73479
73717
  );
@@ -73627,7 +73865,7 @@ async function startAuthorization(c) {
73627
73865
  }
73628
73866
  const port2 = Number(redirect.port || 80);
73629
73867
  for (const [id2, p] of pending) if (p.port === port2 && p.status === "waiting") stopPending(id2, "replaced by a new sign-in");
73630
- const id = randomUUID3();
73868
+ const id = randomUUID4();
73631
73869
  const state = b64url2(randomBytes(24));
73632
73870
  const verifier = b64url2(randomBytes(48));
73633
73871
  const url2 = new URL(c.authUrl);
@@ -77972,7 +78210,7 @@ var init_script_sandbox = __esm({
77972
78210
  });
77973
78211
 
77974
78212
  // ../../apps/engine/src/realtime.ts
77975
- import { randomUUID as randomUUID4 } from "node:crypto";
78213
+ import { randomUUID as randomUUID5 } from "node:crypto";
77976
78214
  function add(s, e) {
77977
78215
  s.events.push({ at: Date.now() - s.started, ...e, text: e.text.length > 1e5 ? `${e.text.slice(0, 1e5)}\u2026 (cut)` : e.text });
77978
78216
  if (s.events.length > MAX_EVENTS) {
@@ -77982,7 +78220,7 @@ function add(s, e) {
77982
78220
  }
77983
78221
  function newSession(kind, url2) {
77984
78222
  if (sessions.size >= MAX_SESSIONS) [...sessions.values()].sort((a, b) => a.started - b.started)[0]?.close();
77985
- const s = { id: randomUUID4(), kind, url: url2, started: Date.now(), status: "connecting", events: [], dropped: 0, lastRead: Date.now(), close: () => void 0 };
78223
+ const s = { id: randomUUID5(), kind, url: url2, started: Date.now(), status: "connecting", events: [], dropped: 0, lastRead: Date.now(), close: () => void 0 };
77986
78224
  sessions.set(s.id, s);
77987
78225
  return s;
77988
78226
  }
@@ -78199,6 +78437,11 @@ function applyAuth(headers, url2, auth) {
78199
78437
  }
78200
78438
  function encodeBody2(lab) {
78201
78439
  const { body } = lab.request;
78440
+ if (body.mode === "multipart") {
78441
+ const encoded = encodeMultipart(body.parts);
78442
+ return { body: encoded.bytes, contentType: encoded.contentType, preview: encoded.preview, ownType: true };
78443
+ }
78444
+ if (body.mode === "file") return { body: fileBytes(body.file), contentType: body.contentType, preview: `<file: ${body.file.name}, ${formatBytes(body.file.size)}>` };
78202
78445
  if (body.mode === "none") return {};
78203
78446
  if (body.mode === "json") {
78204
78447
  return {
@@ -78319,8 +78562,21 @@ function scriptSender(lab) {
78319
78562
  }
78320
78563
  function scriptRequest(lab) {
78321
78564
  const url2 = fullUrl(lab);
78565
+ const text = bodyText(lab);
78566
+ return { method: lab.request.method, url: url2.toString(), headers: lab.request.headers.map((h) => [h.key, h.value]), ...text !== void 0 ? { body: text } : {} };
78567
+ }
78568
+ function bodyShape(lab) {
78569
+ const { body } = lab.request;
78570
+ if (body.mode === "multipart") return { form: body.parts.map((p) => p.kind === "text" ? { name: p.key, value: p.value } : { name: p.key, file: p.file.name, contentType: p.file.contentType }) };
78571
+ if (body.mode === "file") return { file: { name: body.file.name, contentType: body.contentType } };
78572
+ return void 0;
78573
+ }
78574
+ function bodyText(lab) {
78575
+ const { body } = lab.request;
78576
+ if (body.mode === "multipart") return multipartPreview(body.parts, "----ApiTestCenter");
78577
+ if (body.mode === "file") return `<file: ${body.file.name}, ${formatBytes(body.file.size)}>`;
78322
78578
  const encoded = encodeBody2(lab);
78323
- return { method: lab.request.method, url: url2.toString(), headers: lab.request.headers.map((h) => [h.key, h.value]), ...encoded.body !== void 0 ? { body: encoded.body } : {} };
78579
+ return typeof encoded.body === "string" ? encoded.body : void 0;
78324
78580
  }
78325
78581
  function applyScriptRequest(lab, out, vars2) {
78326
78582
  const before = scriptRequest(lab);
@@ -78330,7 +78586,7 @@ function applyScriptRequest(lab, out, vars2) {
78330
78586
  let body = lab.request.body;
78331
78587
  const newBody = r.body === void 0 ? void 0 : fill(r.body);
78332
78588
  if (newBody !== before.body || newBody !== r.body) {
78333
- const contentType = headers.find((h) => h.key.toLowerCase() === "content-type")?.value ?? encodeBody2(lab).contentType ?? "text/plain";
78589
+ const contentType = headers.find((h) => h.key.toLowerCase() === "content-type")?.value ?? (lab.request.body.mode === "multipart" || lab.request.body.mode === "file" ? void 0 : encodeBody2(lab).contentType) ?? "text/plain";
78334
78590
  body = newBody === void 0 ? { mode: "none" } : { mode: "raw", raw: newBody, contentType };
78335
78591
  }
78336
78592
  const urlChanged = r.url !== before.url;
@@ -78459,7 +78715,16 @@ async function sendRaw(lab, extra2 = {}) {
78459
78715
  headers.authorization = extra2.authorization;
78460
78716
  secretNames.push("authorization");
78461
78717
  }
78462
- const encoded = encodeBody2(lab);
78718
+ let encoded;
78719
+ try {
78720
+ encoded = encodeBody2(lab);
78721
+ } catch (err) {
78722
+ const error51 = err instanceof Error ? err.message : String(err);
78723
+ return { raw: "", lowered: {}, result: { ok: false, status: 0, statusText: "error", headers: {}, body: "", truncated: false, durationMs: 0, url: url2.toString(), error: error51 } };
78724
+ }
78725
+ if (encoded.ownType) {
78726
+ for (const k of Object.keys(headers)) if (k.toLowerCase() === "content-type") delete headers[k];
78727
+ }
78463
78728
  if (encoded.contentType && !Object.keys(headers).some((k) => k.toLowerCase() === "content-type")) {
78464
78729
  headers["content-type"] = encoded.contentType;
78465
78730
  }
@@ -78467,7 +78732,7 @@ async function sendRaw(lab, extra2 = {}) {
78467
78732
  if (lab.auth.mode === "oauth1") {
78468
78733
  const { mode: _mode, ...creds } = lab.auth;
78469
78734
  const contentType = Object.entries(headers).find(([k]) => k.toLowerCase() === "content-type")?.[1];
78470
- const signed = signOAuth1(creds, { method: lab.request.method, url: url2.toString(), contentType, body: encoded.body });
78735
+ const signed = signOAuth1(creds, { method: lab.request.method, url: url2.toString(), contentType, body: typeof encoded.body === "string" ? encoded.body : void 0 });
78471
78736
  if (signed.authorization) {
78472
78737
  headers.authorization = signed.authorization;
78473
78738
  secretNames.push("authorization");
@@ -78502,7 +78767,7 @@ async function sendRaw(lab, extra2 = {}) {
78502
78767
  const jar2 = await cookieHeader(current);
78503
78768
  if (jar2) sendHeaders.cookie = jar2;
78504
78769
  }
78505
- if (!sent) sent = describeSent(method, current, sendHeaders, body, [...secretNames, ...ALWAYS_SECRET], secretQuery, options.tlsSkipVerify);
78770
+ if (!sent) sent = describeSent(method, current, sendHeaders, body, [...secretNames, ...ALWAYS_SECRET], secretQuery, options.tlsSkipVerify, encoded.preview, bodyShape(lab));
78506
78771
  else (sent.redirects ??= []).push(`${method} ${current.toString()}`);
78507
78772
  response = await (0, import_undici3.request)(current, { method, headers: sendHeaders, body, headersTimeout: timeout, bodyTimeout: timeout, dispatcher });
78508
78773
  const setCookie = response.headers["set-cookie"];
@@ -78594,14 +78859,16 @@ async function sendRaw(lab, extra2 = {}) {
78594
78859
  if (!extra2.dispatcher && !keepOpen) await dispatcher.close();
78595
78860
  }
78596
78861
  }
78597
- function describeSent(method, url2, headers, body, secretHeaders, secretQuery, insecure) {
78862
+ function describeSent(method, url2, headers, bytes2, secretHeaders, secretQuery, insecure, preview, shape) {
78598
78863
  const list = [["host", url2.host], ...Object.entries(headers)];
78599
- if (body !== void 0) list.push(["content-length", String(Buffer.byteLength(body))]);
78864
+ if (bytes2 !== void 0) list.push(["content-length", String(typeof bytes2 === "string" ? Buffer.byteLength(bytes2) : bytes2.length)]);
78865
+ const body = bytes2 === void 0 ? void 0 : preview ?? (typeof bytes2 === "string" ? bytes2 : `<${bytes2.length} bytes>`);
78600
78866
  return {
78601
78867
  method,
78602
78868
  url: url2.toString(),
78603
78869
  headers: list,
78604
78870
  ...body !== void 0 ? { body: body.length > BODY_LIMIT ? body.slice(0, BODY_LIMIT) : body, bodyTruncated: body.length > BODY_LIMIT || void 0 } : {},
78871
+ ...shape ?? {},
78605
78872
  secretHeaders: [...new Set(secretHeaders.map((h) => h.toLowerCase()))],
78606
78873
  secretQuery,
78607
78874
  // Only https has a certificate to skip.
@@ -78623,6 +78890,7 @@ var init_probe = __esm({
78623
78890
  init_oauth();
78624
78891
  init_script_sandbox();
78625
78892
  init_realtime();
78893
+ init_file_store();
78626
78894
  BODY_LIMIT = 64 * 1024;
78627
78895
  IMAGE_LIMIT = 5 * 1024 * 1024;
78628
78896
  ALWAYS_SECRET = ["authorization", "proxy-authorization", "cookie", "x-api-key", "api-key", "x-amz-security-token"];
@@ -78896,9 +79164,9 @@ var init_k6_metrics = __esm({
78896
79164
 
78897
79165
  // ../../apps/engine/src/load-run.ts
78898
79166
  import { spawn as spawn2 } from "node:child_process";
78899
- import { mkdtemp, readFile as readFile3, rm, writeFile as writeFile3 } from "node:fs/promises";
79167
+ import { mkdtemp, readFile as readFile3, rm, writeFile as writeFile4 } from "node:fs/promises";
78900
79168
  import { tmpdir as tmpdir2 } from "node:os";
78901
- import { join as join5 } from "node:path";
79169
+ import { join as join6 } from "node:path";
78902
79170
  function onRunFinished(listener) {
78903
79171
  finishedListeners.push(listener);
78904
79172
  }
@@ -79197,9 +79465,9 @@ async function startRun(lab, repoRoot2) {
79197
79465
  filled = fillLab(lab, step.vars, { builtins: false });
79198
79466
  }
79199
79467
  const id = crypto.randomUUID();
79200
- const dir2 = await mkdtemp(join5(tmpdir2(), "atc-run-"));
79201
- const scriptPath = join5(dir2, "script.js");
79202
- const summaryPath = join5(dir2, "summary.json");
79468
+ const dir3 = await mkdtemp(join6(tmpdir2(), "atc-run-"));
79469
+ const scriptPath = join6(dir3, "script.js");
79470
+ const summaryPath = join6(dir3, "summary.json");
79203
79471
  let refresh = null;
79204
79472
  if (filled.auth.mode === "oauth2") {
79205
79473
  const config2 = filled.auth.oauth;
@@ -79233,17 +79501,24 @@ async function startRun(lab, repoRoot2) {
79233
79501
  if (bundle && process.platform !== "linux" && !filled.options.tlsSkipVerify && await needsTrustedCa(filled.request.url)) {
79234
79502
  filled = { ...filled, options: { ...filled.options, tlsSkipVerify: true } };
79235
79503
  }
79236
- const generated = generateK6Script(filled, { oauthRefresh: refresh, tlsAuth, jwt: jwt2 });
79504
+ const fileBytesById = {};
79505
+ const body = filled.request.body;
79506
+ try {
79507
+ for (const file2 of body.mode === "file" ? [body.file] : body.mode === "multipart" ? body.parts.flatMap((p) => p.kind === "file" ? [p.file] : []) : []) fileBytesById[file2.id] = fileBytes(file2);
79508
+ } catch (err) {
79509
+ throw Object.assign(err instanceof Error ? err : new Error(String(err)), { statusCode: 422 });
79510
+ }
79511
+ const generated = generateK6Script(filled, { oauthRefresh: refresh, tlsAuth, jwt: jwt2, fileBytes: fileBytesById });
79237
79512
  if (filled.options.cookies !== false && !filled.request.headers.some((h) => h.key.toLowerCase() === "cookie")) {
79238
79513
  const jar2 = await cookieHeader(filled.request.url);
79239
79514
  if (jar2) generated.env.ATC_COOKIE = jar2;
79240
79515
  }
79241
79516
  if (bundle && process.platform === "linux") {
79242
- await writeFile3(join5(dir2, "ca-bundle.pem"), bundle, "utf8");
79243
- generated.env.SSL_CERT_FILE = join5(dir2, "ca-bundle.pem");
79517
+ await writeFile4(join6(dir3, "ca-bundle.pem"), bundle, "utf8");
79518
+ generated.env.SSL_CERT_FILE = join6(dir3, "ca-bundle.pem");
79244
79519
  }
79245
- await writeFile3(scriptPath, generated.script, "utf8");
79246
- for (const [name, text] of Object.entries(generated.files)) await writeFile3(join5(dir2, name), text, "utf8");
79520
+ await writeFile4(scriptPath, generated.script, "utf8");
79521
+ for (const [name, content] of Object.entries(generated.files)) await writeFile4(join6(dir3, name), content);
79247
79522
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
79248
79523
  const run2 = {
79249
79524
  id,
@@ -79251,7 +79526,7 @@ async function startRun(lab, repoRoot2) {
79251
79526
  status: "running",
79252
79527
  startedAt,
79253
79528
  child: null,
79254
- dir: dir2,
79529
+ dir: dir3,
79255
79530
  summaryPath,
79256
79531
  aggregator: Object.assign(new LiveAggregator(Date.parse(startedAt)), { skipChallenges: lab.auth.mode === "digest" || lab.auth.mode === "ntlm" }),
79257
79532
  report: {
@@ -79306,7 +79581,7 @@ async function startRun(lab, repoRoot2) {
79306
79581
  scriptPath
79307
79582
  ],
79308
79583
  {
79309
- cwd: dir2,
79584
+ cwd: dir3,
79310
79585
  env: { ...process.env, ...generated.env },
79311
79586
  windowsHide: true
79312
79587
  }
@@ -79385,6 +79660,7 @@ var init_load_run = __esm({
79385
79660
  "../../apps/engine/src/load-run.ts"() {
79386
79661
  "use strict";
79387
79662
  init_src();
79663
+ init_file_store();
79388
79664
  init_k6_script();
79389
79665
  init_src();
79390
79666
  init_certificates();
@@ -79403,8 +79679,8 @@ var init_load_run = __esm({
79403
79679
 
79404
79680
  // ../../apps/engine/src/secret-store.ts
79405
79681
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes4 } from "node:crypto";
79406
- import { readFile as readFile4, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
79407
- import { join as join6 } from "node:path";
79682
+ import { readFile as readFile4, rename as rename3, writeFile as writeFile5 } from "node:fs/promises";
79683
+ import { join as join7 } from "node:path";
79408
79684
  var SecretStore;
79409
79685
  var init_secret_store = __esm({
79410
79686
  "../../apps/engine/src/secret-store.ts"() {
@@ -79414,8 +79690,8 @@ var init_secret_store = __esm({
79414
79690
  loaded = false;
79415
79691
  key;
79416
79692
  file;
79417
- constructor(dir2, keyBase64 = process.env.ATC_SECRET_KEY) {
79418
- this.file = join6(dir2, "secrets.enc");
79693
+ constructor(dir3, keyBase64 = process.env.ATC_SECRET_KEY) {
79694
+ this.file = join7(dir3, "secrets.enc");
79419
79695
  const key = keyBase64 ? Buffer.from(keyBase64, "base64") : null;
79420
79696
  this.key = key && key.length === 32 ? key : null;
79421
79697
  }
@@ -79443,7 +79719,7 @@ var init_secret_store = __esm({
79443
79719
  const cipher = createCipheriv("aes-256-gcm", this.key, iv);
79444
79720
  const body = Buffer.concat([cipher.update(JSON.stringify(Object.fromEntries(this.values)), "utf8"), cipher.final()]);
79445
79721
  const tmp = `${this.file}.${process.pid}.tmp`;
79446
- await writeFile4(tmp, Buffer.concat([iv, cipher.getAuthTag(), body]));
79722
+ await writeFile5(tmp, Buffer.concat([iv, cipher.getAuthTag(), body]));
79447
79723
  await rename3(tmp, this.file);
79448
79724
  }
79449
79725
  async get(id) {
@@ -79466,9 +79742,9 @@ var init_secret_store = __esm({
79466
79742
  });
79467
79743
 
79468
79744
  // ../../apps/engine/src/suite-run.ts
79469
- import { randomUUID as randomUUID5 } from "node:crypto";
79470
- import { mkdir as mkdir3, readFile as readFile5, rename as rename4, writeFile as writeFile5 } from "node:fs/promises";
79471
- import { join as join7 } from "node:path";
79745
+ import { randomUUID as randomUUID6 } from "node:crypto";
79746
+ import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile6 } from "node:fs/promises";
79747
+ import { join as join8 } from "node:path";
79472
79748
  async function resolveSaved(workspace, saved, vars2, envName, patch = {}) {
79473
79749
  const applied = applyFolders({ ...requestFromUnknown(saved.request), ...patch }, await workspace.folderChain(saved.collection));
79474
79750
  return resolveDraft(applied.draft, { ...applied.vars, ...vars2 }, envName);
@@ -79483,7 +79759,7 @@ async function runSuite(workspace, plans, opts, onChange = () => {
79483
79759
  if (!inCollection.length) throw new StepProblem(opts.collection ? `No saved requests in "${opts.collection}"` : "No saved requests outside a collection");
79484
79760
  const ordered = orderSuite(inCollection, await plans.get(opts.collection));
79485
79761
  const run2 = {
79486
- id: randomUUID5(),
79762
+ id: randomUUID6(),
79487
79763
  collection: opts.collection,
79488
79764
  environment: env?.name ?? null,
79489
79765
  status: "running",
@@ -79674,8 +79950,8 @@ var init_suite_run = __esm({
79674
79950
  SuitePlans = class {
79675
79951
  file;
79676
79952
  queue = Promise.resolve();
79677
- constructor(dir2) {
79678
- this.file = join7(dir2, "suites.json");
79953
+ constructor(dir3) {
79954
+ this.file = join8(dir3, "suites.json");
79679
79955
  }
79680
79956
  async readAll() {
79681
79957
  try {
@@ -79693,9 +79969,9 @@ var init_suite_run = __esm({
79693
79969
  const task = async () => {
79694
79970
  const all = await this.readAll();
79695
79971
  const next2 = Object.fromEntries(Object.entries(all).map(([k, v]) => [k === from || k.startsWith(`${from}/`) ? to + k.slice(from.length) : k, v]));
79696
- await mkdir3(join7(this.file, ".."), { recursive: true });
79972
+ await mkdir4(join8(this.file, ".."), { recursive: true });
79697
79973
  const tmp = `${this.file}.tmp`;
79698
- await writeFile5(tmp, `${JSON.stringify({ version: 1, collections: next2 }, null, 2)}
79974
+ await writeFile6(tmp, `${JSON.stringify({ version: 1, collections: next2 }, null, 2)}
79699
79975
  `, "utf8");
79700
79976
  await rename4(tmp, this.file);
79701
79977
  };
@@ -79707,9 +79983,9 @@ var init_suite_run = __esm({
79707
79983
  const task = async () => {
79708
79984
  const all = await this.readAll();
79709
79985
  all[collection] = normalizeSuitePlan(plan);
79710
- await mkdir3(join7(this.file, ".."), { recursive: true });
79986
+ await mkdir4(join8(this.file, ".."), { recursive: true });
79711
79987
  const tmp = `${this.file}.tmp`;
79712
- await writeFile5(tmp, `${JSON.stringify({ version: 1, collections: all }, null, 2)}
79988
+ await writeFile6(tmp, `${JSON.stringify({ version: 1, collections: all }, null, 2)}
79713
79989
  `, "utf8");
79714
79990
  await rename4(tmp, this.file);
79715
79991
  return all[collection];
@@ -79733,9 +80009,9 @@ var init_suite_run = __esm({
79733
80009
  });
79734
80010
 
79735
80011
  // ../../apps/engine/src/workspace-store.ts
79736
- import { randomUUID as randomUUID6 } from "node:crypto";
79737
- import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, rename as rename5, rm as rm2, rmdir, stat as stat2, unlink as unlink2, writeFile as writeFile6 } from "node:fs/promises";
79738
- import { dirname, join as join8, relative, sep } from "node:path";
80012
+ import { randomUUID as randomUUID7 } from "node:crypto";
80013
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile6, rename as rename5, rm as rm2, rmdir, stat as stat2, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
80014
+ import { dirname, join as join9, relative, sep } from "node:path";
79739
80015
  function putSecret(target, field, value) {
79740
80016
  const dot = field.indexOf(".");
79741
80017
  if (dot > 0) {
@@ -79774,11 +80050,11 @@ var init_workspace_store = __esm({
79774
80050
  root;
79775
80051
  envFile;
79776
80052
  queue = Promise.resolve();
79777
- constructor(dir2, secrets2) {
79778
- this.dir = dir2;
80053
+ constructor(dir3, secrets2) {
80054
+ this.dir = dir3;
79779
80055
  this.secrets = secrets2;
79780
- this.root = join8(dir2, "requests");
79781
- this.envFile = join8(dir2, "environments.json");
80056
+ this.root = join9(dir3, "requests");
80057
+ this.envFile = join9(dir3, "environments.json");
79782
80058
  }
79783
80059
  serial(task) {
79784
80060
  const next = this.queue.then(task, task);
@@ -79797,7 +80073,7 @@ var init_workspace_store = __esm({
79797
80073
  return;
79798
80074
  }
79799
80075
  for (const e of entries) {
79800
- const path = join8(folder, e.name);
80076
+ const path = join9(folder, e.name);
79801
80077
  if (e.isDirectory() && depth < MAX_FOLDER_DEPTH) {
79802
80078
  folders.push(this.pathOf(path));
79803
80079
  await walk(path, depth + 1);
@@ -79811,12 +80087,12 @@ var init_workspace_store = __esm({
79811
80087
  return (await this.scan()).files;
79812
80088
  }
79813
80089
  /** A folder on disk → its path, "Orders/Admin". */
79814
- pathOf(dir2) {
79815
- const rel = relative(this.root, dir2);
80090
+ pathOf(dir3) {
80091
+ const rel = relative(this.root, dir3);
79816
80092
  return rel && rel !== "." ? rel.split(sep).join("/") : "";
79817
80093
  }
79818
80094
  dirOf(path) {
79819
- return path ? join8(this.root, ...path.split("/")) : this.root;
80095
+ return path ? join9(this.root, ...path.split("/")) : this.root;
79820
80096
  }
79821
80097
  async readRequest(path) {
79822
80098
  try {
@@ -79887,7 +80163,7 @@ var init_workspace_store = __esm({
79887
80163
  const secrets2 = [];
79888
80164
  const folders = new Set([...picked.values()].flatMap((f) => folderChain(f.file.collection)));
79889
80165
  for (const path of [...folders].sort()) {
79890
- const file2 = join8(this.dirOf(path), FOLDER_FILE);
80166
+ const file2 = join9(this.dirOf(path), FOLDER_FILE);
79891
80167
  try {
79892
80168
  const text = await readFile6(file2, "utf8");
79893
80169
  files.push({ path: relative(this.dir, file2).split(sep).join("/"), text });
@@ -79918,7 +80194,7 @@ var init_workspace_store = __esm({
79918
80194
  }
79919
80195
  saveRequest(input) {
79920
80196
  return this.serial(async () => {
79921
- const id = input.id || randomUUID6();
80197
+ const id = input.id || randomUUID7();
79922
80198
  const existing = input.id ? await this.find(input.id) : null;
79923
80199
  const now = (/* @__PURE__ */ new Date()).toISOString();
79924
80200
  const request3 = { ...input.request };
@@ -79935,10 +80211,10 @@ var init_workspace_store = __esm({
79935
80211
  secretFields: secretFields.length ? secretFields : void 0
79936
80212
  };
79937
80213
  const folder = this.dirOf(collection);
79938
- await mkdir4(folder, { recursive: true });
79939
- const path = join8(folder, `${safeSegment(file2.name)}_${id.slice(0, 8)}.json`);
80214
+ await mkdir5(folder, { recursive: true });
80215
+ const path = join9(folder, `${safeSegment(file2.name)}_${id.slice(0, 8)}.json`);
79940
80216
  const tmp = `${path}.${process.pid}.tmp`;
79941
- await writeFile6(tmp, `${JSON.stringify(file2, null, 2)}
80217
+ await writeFile7(tmp, `${JSON.stringify(file2, null, 2)}
79942
80218
  `, "utf8");
79943
80219
  await rename5(tmp, path);
79944
80220
  if (existing && existing.path !== path) await unlink2(existing.path).catch(() => void 0);
@@ -79957,7 +80233,7 @@ var init_workspace_store = __esm({
79957
80233
  }
79958
80234
  async readFolder(path) {
79959
80235
  try {
79960
- const raw = JSON.parse(await readFile6(join8(this.dirOf(path), FOLDER_FILE), "utf8"));
80236
+ const raw = JSON.parse(await readFile6(join9(this.dirOf(path), FOLDER_FILE), "utf8"));
79961
80237
  return { ...folderFromUnknown(raw), id: typeof raw.id === "string" ? raw.id : "", secretFields: Array.isArray(raw.secretFields) ? raw.secretFields : void 0 };
79962
80238
  } catch {
79963
80239
  return null;
@@ -79991,13 +80267,13 @@ var init_workspace_store = __esm({
79991
80267
  return this.serial(async () => {
79992
80268
  const path = folderPath(input);
79993
80269
  if (!path) throw Object.assign(new Error("The top level has no folder settings"), { statusCode: 400 });
79994
- const dir2 = this.dirOf(path);
79995
- await mkdir4(dir2, { recursive: true });
80270
+ const dir3 = this.dirOf(path);
80271
+ await mkdir5(dir3, { recursive: true });
79996
80272
  const settings = folderFromUnknown(raw);
79997
80273
  const old = await this.readFolder(path);
79998
- const id = old?.id || randomUUID6();
80274
+ const id = old?.id || randomUUID7();
79999
80275
  await this.secrets.deletePrefix(`folder:${id}:`);
80000
- const file2 = join8(dir2, FOLDER_FILE);
80276
+ const file2 = join9(dir3, FOLDER_FILE);
80001
80277
  if (isEmptyFolder(settings)) {
80002
80278
  await unlink2(file2).catch(() => void 0);
80003
80279
  return { path, settings };
@@ -80013,7 +80289,7 @@ var init_workspace_store = __esm({
80013
80289
  }
80014
80290
  if (secretFields.length) onDisk.secretFields = secretFields;
80015
80291
  const tmp = `${file2}.${process.pid}.tmp`;
80016
- await writeFile6(tmp, `${JSON.stringify(onDisk, null, 2)}
80292
+ await writeFile7(tmp, `${JSON.stringify(onDisk, null, 2)}
80017
80293
  `, "utf8");
80018
80294
  await rename5(tmp, file2);
80019
80295
  return await this.getFolder(path);
@@ -80030,7 +80306,7 @@ var init_workspace_store = __esm({
80030
80306
  if (inFolder(to, from)) throw bad("A folder can't move inside itself");
80031
80307
  if (await stat2(this.dirOf(to)).then(() => true, () => false)) throw bad(`There is already a folder "${to}"`);
80032
80308
  if (!await stat2(this.dirOf(from)).then(() => true, () => false)) throw Object.assign(new Error(`No folder "${from}"`), { statusCode: 404 });
80033
- await mkdir4(dirname(this.dirOf(to)), { recursive: true });
80309
+ await mkdir5(dirname(this.dirOf(to)), { recursive: true });
80034
80310
  await rename5(this.dirOf(from), this.dirOf(to));
80035
80311
  return { from, to };
80036
80312
  });
@@ -80085,9 +80361,9 @@ var init_workspace_store = __esm({
80085
80361
  }
80086
80362
  onDisk.push({ id: env.id, name: env.name, variables });
80087
80363
  }
80088
- await mkdir4(this.dir, { recursive: true });
80364
+ await mkdir5(this.dir, { recursive: true });
80089
80365
  const tmp = `${this.envFile}.${process.pid}.tmp`;
80090
- await writeFile6(tmp, `${JSON.stringify(onDisk, null, 2)}
80366
+ await writeFile7(tmp, `${JSON.stringify(onDisk, null, 2)}
80091
80367
  `, "utf8");
80092
80368
  await rename5(tmp, this.envFile);
80093
80369
  return this.environments();
@@ -80097,6 +80373,247 @@ var init_workspace_store = __esm({
80097
80373
  }
80098
80374
  });
80099
80375
 
80376
+ // ../../apps/engine/src/practice-live.ts
80377
+ import { createHash as createHash5 } from "node:crypto";
80378
+ function frame(opcode, data) {
80379
+ const len = data.length;
80380
+ const head = len < 126 ? Buffer.from([128 | opcode, len]) : len < 65536 ? Buffer.from([128 | opcode, 126, len >> 8, len & 255]) : Buffer.concat([Buffer.from([128 | opcode, 127]), (() => {
80381
+ const b = Buffer.alloc(8);
80382
+ b.writeBigUInt64BE(BigInt(len));
80383
+ return b;
80384
+ })()]);
80385
+ return Buffer.concat([head, data]);
80386
+ }
80387
+ function echo(req, socket) {
80388
+ const key = req.headers["sec-websocket-key"];
80389
+ if (typeof key !== "string") return socket.destroy();
80390
+ const accept = createHash5("sha1").update(key + GUID).digest("base64");
80391
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
80392
+ Upgrade: websocket\r
80393
+ Connection: Upgrade\r
80394
+ Sec-WebSocket-Accept: ${accept}\r
80395
+ \r
80396
+ `);
80397
+ const sendText = (value) => socket.write(frame(1, Buffer.from(JSON.stringify(value))));
80398
+ sendText({ type: "welcome", message: "Practice WebSocket: send a message and it comes back." });
80399
+ let buf = Buffer.alloc(0);
80400
+ socket.on("data", (chunk) => {
80401
+ buf = Buffer.concat([buf, chunk]);
80402
+ for (; ; ) {
80403
+ if (buf.length < 2) return;
80404
+ const opcode = buf[0] & 15;
80405
+ const masked = (buf[1] & 128) !== 0;
80406
+ let len = buf[1] & 127;
80407
+ let at = 2;
80408
+ if (len === 126) {
80409
+ if (buf.length < 4) return;
80410
+ len = buf.readUInt16BE(2);
80411
+ at = 4;
80412
+ } else if (len === 127) {
80413
+ if (buf.length < 10) return;
80414
+ len = Number(buf.readBigUInt64BE(2));
80415
+ at = 10;
80416
+ }
80417
+ if (len > 1e6) return socket.destroy();
80418
+ const mask = masked ? buf.subarray(at, at + 4) : null;
80419
+ at += masked ? 4 : 0;
80420
+ if (buf.length < at + len) return;
80421
+ const payload = Buffer.from(buf.subarray(at, at + len));
80422
+ if (mask) for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
80423
+ buf = buf.subarray(at + len);
80424
+ if (opcode === 8) {
80425
+ socket.end(frame(8, payload.subarray(0, 2)));
80426
+ return;
80427
+ }
80428
+ if (opcode === 9) socket.write(frame(10, payload));
80429
+ else if (opcode === 1) {
80430
+ const text = payload.toString("utf8");
80431
+ let parsed = text;
80432
+ try {
80433
+ parsed = JSON.parse(text);
80434
+ } catch {
80435
+ }
80436
+ sendText({ type: "echo", you: parsed, at: (/* @__PURE__ */ new Date()).toISOString() });
80437
+ }
80438
+ }
80439
+ });
80440
+ socket.on("error", () => socket.destroy());
80441
+ }
80442
+ function attachPracticeWebSocket(server, path) {
80443
+ server.on("upgrade", (req, socket) => {
80444
+ if ((req.url ?? "").split("?")[0] !== path) return;
80445
+ echo(req, socket);
80446
+ });
80447
+ }
80448
+ function practiceEvents(res, onGone, count = 10, everyMs = 1e3) {
80449
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
80450
+ res.write(": practice event stream \u2014 a tick a second\n\n");
80451
+ let n = 0;
80452
+ const timer = setInterval(() => {
80453
+ n++;
80454
+ res.write(`event: tick
80455
+ id: ${n}
80456
+ data: ${JSON.stringify({ n, at: (/* @__PURE__ */ new Date()).toISOString(), price: Math.round((40 + Math.random() * 5) * 100) / 100 })}
80457
+
80458
+ `);
80459
+ if (n >= count) {
80460
+ clearInterval(timer);
80461
+ res.end();
80462
+ }
80463
+ }, everyMs);
80464
+ onGone(() => clearInterval(timer));
80465
+ }
80466
+ var GUID;
80467
+ var init_practice_live = __esm({
80468
+ "../../apps/engine/src/practice-live.ts"() {
80469
+ "use strict";
80470
+ GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
80471
+ }
80472
+ });
80473
+
80474
+ // ../../apps/engine/src/demo-api.ts
80475
+ import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
80476
+ function parseMultipart(body, boundary) {
80477
+ const parts = [];
80478
+ const delimiter = Buffer.from(`--${boundary}`);
80479
+ let at = body.indexOf(delimiter);
80480
+ while (at >= 0) {
80481
+ const start = at + delimiter.length;
80482
+ if (body.subarray(start, start + 2).toString() === "--") break;
80483
+ const headEnd = body.indexOf("\r\n\r\n", start);
80484
+ const next = body.indexOf(delimiter, start);
80485
+ if (headEnd < 0 || next < 0) break;
80486
+ const head = body.subarray(start, headEnd).toString("utf8");
80487
+ const param = (key) => {
80488
+ const m = new RegExp(`;\\s*${key}=(?:"([^"]*)"|([^;\\s]+))`, "i").exec(head);
80489
+ return m ? m[1] ?? m[2] : void 0;
80490
+ };
80491
+ const name = param("name");
80492
+ const filename = param("filename");
80493
+ const type = /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim();
80494
+ if (name !== void 0) parts.push({ name, ...filename !== void 0 ? { filename } : {}, ...type ? { type } : {}, data: body.subarray(headEnd + 4, next - 2) });
80495
+ at = next;
80496
+ }
80497
+ return parts;
80498
+ }
80499
+ function registerDemoApi(api) {
80500
+ const tokens = /* @__PURE__ */ new Map();
80501
+ const created = /* @__PURE__ */ new Map();
80502
+ let nextOrder = 2001;
80503
+ const signedIn = (auth) => {
80504
+ const header = auth ?? "";
80505
+ return header.startsWith("Bearer ") ? tokens.get(header.slice(7)) : void 0;
80506
+ };
80507
+ api.get("/products", async () => {
80508
+ await jitter(5, 25);
80509
+ return { items: PRODUCTS, total: PRODUCTS.length };
80510
+ });
80511
+ api.get("/products/:id", async (request3, reply) => {
80512
+ await jitter(5, 20);
80513
+ const product = PRODUCTS.find((p) => String(p.id) === request3.params.id);
80514
+ if (!product) return reply.code(404).send({ error: `No product ${request3.params.id}. Try 1 to ${PRODUCTS.length}.` });
80515
+ return product;
80516
+ });
80517
+ api.post("/login", async (request3, reply) => {
80518
+ await jitter(30, 40);
80519
+ const username = typeof request3.body?.username === "string" ? request3.body.username.trim() : "";
80520
+ if (!username || request3.body?.password !== "demo") {
80521
+ return reply.code(401).send({ error: 'Use any username with the password "demo".' });
80522
+ }
80523
+ if (tokens.size > 5e3) tokens.clear();
80524
+ const token = `demo-${randomBytes5(12).toString("hex")}`;
80525
+ tokens.set(token, username);
80526
+ return { data: { token, user: username, expiresIn: 3600 } };
80527
+ });
80528
+ api.get("/orders", async (request3, reply) => {
80529
+ await jitter(10, 30);
80530
+ const auth = request3.headers.authorization ?? "";
80531
+ const user = auth.startsWith("Bearer ") ? tokens.get(auth.slice(7)) : void 0;
80532
+ if (!user) return reply.code(401).send({ error: "Sign in first: POST /login, then send the token as a Bearer token." });
80533
+ return {
80534
+ user,
80535
+ orders: [
80536
+ { id: "A-1001", items: [PRODUCTS[0].name, PRODUCTS[1].name], total: PRODUCTS[0].price + PRODUCTS[1].price },
80537
+ { id: "A-1002", items: [PRODUCTS[3].name], total: PRODUCTS[3].price }
80538
+ ]
80539
+ };
80540
+ });
80541
+ api.post("/orders", async (request3, reply) => {
80542
+ await jitter(15, 30);
80543
+ const user = signedIn(request3.headers.authorization);
80544
+ if (!user) return reply.code(401).send({ error: "Sign in first: POST /login, then send the token as a Bearer token." });
80545
+ const product = PRODUCTS.find((p) => p.id === Number(request3.body?.productId));
80546
+ if (!product) return reply.code(400).send({ error: `Send { "productId": 1 } \u2014 a product id from 1 to ${PRODUCTS.length}.` });
80547
+ const quantity = Math.min(99, Math.max(1, Math.floor(Number(request3.body?.quantity ?? 1)) || 1));
80548
+ if (created.size > 5e3) created.clear();
80549
+ const order = { id: `A-${nextOrder++}`, user, productId: product.id, quantity, total: product.price * quantity, status: "new" };
80550
+ created.set(order.id, order);
80551
+ return reply.code(201).send(order);
80552
+ });
80553
+ api.get("/orders/:id", async (request3, reply) => {
80554
+ await jitter(5, 20);
80555
+ if (!signedIn(request3.headers.authorization)) return reply.code(401).send({ error: "Sign in first: POST /login, then send the token as a Bearer token." });
80556
+ const order = created.get(request3.params.id);
80557
+ if (!order) return reply.code(404).send({ error: `No order ${request3.params.id}. Create one with POST /orders.` });
80558
+ return order;
80559
+ });
80560
+ api.get("/slow", async (request3) => {
80561
+ const ms3 = Math.min(3e3, Math.max(0, Number(request3.query.ms ?? 300) || 0));
80562
+ await jitter(ms3, ms3 * 0.2);
80563
+ return { waitedMs: ms3 };
80564
+ });
80565
+ api.get("/flaky", async (_request, reply) => {
80566
+ await jitter(5, 30);
80567
+ if (Math.random() < 0.1) return reply.code(500).send({ error: "Something went wrong (on purpose, about 1 in 10)." });
80568
+ return { ok: true };
80569
+ });
80570
+ api.addContentTypeParser("*", { parseAs: "buffer", bodyLimit: 20 * 1024 * 1024 }, (_req, body, done) => done(null, body));
80571
+ api.post("/upload", { bodyLimit: 20 * 1024 * 1024 }, async (request3, reply) => {
80572
+ await jitter(10, 20);
80573
+ const type = String(request3.headers["content-type"] ?? "");
80574
+ const raw = Buffer.isBuffer(request3.body) ? request3.body : Buffer.from(typeof request3.body === "string" ? request3.body : JSON.stringify(request3.body ?? ""));
80575
+ const boundary = /^multipart\/form-data;.*boundary="?([^";]+)"?/i.exec(type)?.[1];
80576
+ if (/^multipart\/form-data/i.test(type) && !boundary) return reply.code(400).send({ error: "A multipart/form-data body needs a boundary in its Content-Type \u2014 leave Content-Type out and the app sets it." });
80577
+ if (!boundary) return { kind: "file", contentType: type || null, size: raw.length, sha256: sha256(raw) };
80578
+ const parts = parseMultipart(raw, boundary);
80579
+ if (!parts.length) return reply.code(400).send({ error: "No fields found in the form." });
80580
+ return {
80581
+ kind: "form",
80582
+ fields: Object.fromEntries(parts.filter((p) => p.filename === void 0).map((p) => [p.name, p.data.toString("utf8")])),
80583
+ files: parts.filter((p) => p.filename !== void 0).map((p) => ({ field: p.name, name: p.filename, contentType: p.type ?? null, size: p.data.length, sha256: sha256(p.data) }))
80584
+ };
80585
+ });
80586
+ api.get("/events", async (request3, reply) => {
80587
+ reply.hijack();
80588
+ practiceEvents(reply.raw, (stop) => request3.raw.on("close", stop));
80589
+ });
80590
+ attachPracticeWebSocket(api.server, `${api.prefix}/ws`);
80591
+ }
80592
+ var PRODUCTS, sha256, sleep, jitter;
80593
+ var init_demo_api = __esm({
80594
+ "../../apps/engine/src/demo-api.ts"() {
80595
+ "use strict";
80596
+ init_practice_live();
80597
+ PRODUCTS = [
80598
+ ["Desk lamp", 39],
80599
+ ["Notebook", 6],
80600
+ ["Coffee mug", 12],
80601
+ ["Headphones", 129],
80602
+ ["Backpack", 74],
80603
+ ["Water bottle", 18],
80604
+ ["Keyboard", 89],
80605
+ ["Plant pot", 15],
80606
+ ["Phone stand", 22],
80607
+ ["Sunglasses", 58],
80608
+ ["Umbrella", 27],
80609
+ ["Wall clock", 34]
80610
+ ].map(([name, price], i) => ({ id: i + 1, name, price, inStock: i % 4 !== 3 }));
80611
+ sha256 = (data) => createHash6("sha256").update(data).digest("hex");
80612
+ sleep = (ms3) => new Promise((r) => setTimeout(r, ms3));
80613
+ jitter = (base, spread) => sleep(base + Math.random() * spread);
80614
+ }
80615
+ });
80616
+
80100
80617
  // ../../apps/engine/src/mock-server.ts
80101
80618
  import { createServer as createServer2 } from "node:http";
80102
80619
  var MAX_ROUTES, MockServer;
@@ -88049,7 +88566,7 @@ var require_sonic_boom = __commonJS({
88049
88566
  if (!(this instanceof SonicBoom)) {
88050
88567
  return new SonicBoom(opts);
88051
88568
  }
88052
- let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir8, retryEAGAIN, fsync, contentMode, mode } = opts || {};
88569
+ let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir9, retryEAGAIN, fsync, contentMode, mode } = opts || {};
88053
88570
  fd = fd || dest;
88054
88571
  this._len = 0;
88055
88572
  this.fd = -1;
@@ -88074,7 +88591,7 @@ var require_sonic_boom = __commonJS({
88074
88591
  this.append = append || false;
88075
88592
  this.mode = mode;
88076
88593
  this.retryEAGAIN = retryEAGAIN || (() => true);
88077
- this.mkdir = mkdir8 || false;
88594
+ this.mkdir = mkdir9 || false;
88078
88595
  let fsWriteSync;
88079
88596
  let fsWrite;
88080
88597
  if (contentMode === kContentModeBuffer) {
@@ -88786,7 +89303,7 @@ var require_thread_stream = __commonJS({
88786
89303
  var { version: version2 } = require_package();
88787
89304
  var { EventEmitter } = __require("events");
88788
89305
  var { Worker: Worker2 } = __require("worker_threads");
88789
- var { join: join13 } = __require("path");
89306
+ var { join: join14 } = __require("path");
88790
89307
  var { pathToFileURL } = __require("url");
88791
89308
  var { wait } = require_wait();
88792
89309
  var {
@@ -88837,7 +89354,7 @@ var require_thread_stream = __commonJS({
88837
89354
  function createWorker(stream, opts) {
88838
89355
  const { filename, workerData } = opts;
88839
89356
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
88840
- const toExecute = bundlerOverrides["thread-stream-worker"] || join13(__dirname, "lib", "worker.js");
89357
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join14(__dirname, "lib", "worker.js");
88841
89358
  const worker = new Worker2(toExecute, {
88842
89359
  ...opts.workerOpts,
88843
89360
  name: opts.workerOpts?.name || "thread-stream",
@@ -89303,9 +89820,9 @@ var require_transport = __commonJS({
89303
89820
  "../../node_modules/.pnpm/pino@10.3.1/node_modules/pino/lib/transport.js"(exports, module) {
89304
89821
  "use strict";
89305
89822
  var { createRequire } = __require("module");
89306
- var { existsSync: existsSync3 } = __require("node:fs");
89823
+ var { existsSync: existsSync4 } = __require("node:fs");
89307
89824
  var getCallers = require_caller();
89308
- var { join: join13, isAbsolute, sep: sep3 } = __require("node:path");
89825
+ var { join: join14, isAbsolute, sep: sep3 } = __require("node:path");
89309
89826
  var { fileURLToPath: fileURLToPath3 } = __require("node:url");
89310
89827
  var sleep2 = require_atomic_sleep();
89311
89828
  var onExit = require_on_exit_leak_free();
@@ -89377,7 +89894,7 @@ var require_transport = __commonJS({
89377
89894
  return false;
89378
89895
  }
89379
89896
  }
89380
- return isAbsolute(path) && !existsSync3(path);
89897
+ return isAbsolute(path) && !existsSync4(path);
89381
89898
  }
89382
89899
  function stripQuotes(value) {
89383
89900
  const first = value[0];
@@ -89458,7 +89975,7 @@ var require_transport = __commonJS({
89458
89975
  throw new Error("only one of target or targets can be specified");
89459
89976
  }
89460
89977
  if (targets) {
89461
- target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
89978
+ target = bundlerOverrides["pino-worker"] || join14(__dirname, "worker.js");
89462
89979
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
89463
89980
  return {
89464
89981
  ...dest,
@@ -89476,7 +89993,7 @@ var require_transport = __commonJS({
89476
89993
  });
89477
89994
  });
89478
89995
  } else if (pipeline) {
89479
- target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
89996
+ target = bundlerOverrides["pino-worker"] || join14(__dirname, "worker.js");
89480
89997
  options.pipelines = [pipeline.map((dest) => {
89481
89998
  return {
89482
89999
  ...dest,
@@ -89499,7 +90016,7 @@ var require_transport = __commonJS({
89499
90016
  return origin;
89500
90017
  }
89501
90018
  if (origin === "pino/file") {
89502
- return join13(__dirname, "..", "file.js");
90019
+ return join14(__dirname, "..", "file.js");
89503
90020
  }
89504
90021
  let fixTarget2;
89505
90022
  for (const filePath of callers) {
@@ -90479,7 +90996,7 @@ var require_safe_stable_stringify = __commonJS({
90479
90996
  return circularValue;
90480
90997
  }
90481
90998
  let res = "";
90482
- let join13 = ",";
90999
+ let join14 = ",";
90483
91000
  const originalIndentation = indentation;
90484
91001
  if (Array.isArray(value)) {
90485
91002
  if (value.length === 0) {
@@ -90493,7 +91010,7 @@ var require_safe_stable_stringify = __commonJS({
90493
91010
  indentation += spacer;
90494
91011
  res += `
90495
91012
  ${indentation}`;
90496
- join13 = `,
91013
+ join14 = `,
90497
91014
  ${indentation}`;
90498
91015
  }
90499
91016
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -90501,13 +91018,13 @@ ${indentation}`;
90501
91018
  for (; i < maximumValuesToStringify - 1; i++) {
90502
91019
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
90503
91020
  res += tmp2 !== void 0 ? tmp2 : "null";
90504
- res += join13;
91021
+ res += join14;
90505
91022
  }
90506
91023
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
90507
91024
  res += tmp !== void 0 ? tmp : "null";
90508
91025
  if (value.length - 1 > maximumBreadth) {
90509
91026
  const removedKeys = value.length - maximumBreadth - 1;
90510
- res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
91027
+ res += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
90511
91028
  }
90512
91029
  if (spacer !== "") {
90513
91030
  res += `
@@ -90528,7 +91045,7 @@ ${originalIndentation}`;
90528
91045
  let separator = "";
90529
91046
  if (spacer !== "") {
90530
91047
  indentation += spacer;
90531
- join13 = `,
91048
+ join14 = `,
90532
91049
  ${indentation}`;
90533
91050
  whitespace = " ";
90534
91051
  }
@@ -90542,13 +91059,13 @@ ${indentation}`;
90542
91059
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
90543
91060
  if (tmp !== void 0) {
90544
91061
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
90545
- separator = join13;
91062
+ separator = join14;
90546
91063
  }
90547
91064
  }
90548
91065
  if (keyLength > maximumBreadth) {
90549
91066
  const removedKeys = keyLength - maximumBreadth;
90550
91067
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
90551
- separator = join13;
91068
+ separator = join14;
90552
91069
  }
90553
91070
  if (spacer !== "" && separator.length > 1) {
90554
91071
  res = `
@@ -90589,7 +91106,7 @@ ${originalIndentation}`;
90589
91106
  }
90590
91107
  const originalIndentation = indentation;
90591
91108
  let res = "";
90592
- let join13 = ",";
91109
+ let join14 = ",";
90593
91110
  if (Array.isArray(value)) {
90594
91111
  if (value.length === 0) {
90595
91112
  return "[]";
@@ -90602,7 +91119,7 @@ ${originalIndentation}`;
90602
91119
  indentation += spacer;
90603
91120
  res += `
90604
91121
  ${indentation}`;
90605
- join13 = `,
91122
+ join14 = `,
90606
91123
  ${indentation}`;
90607
91124
  }
90608
91125
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -90610,13 +91127,13 @@ ${indentation}`;
90610
91127
  for (; i < maximumValuesToStringify - 1; i++) {
90611
91128
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
90612
91129
  res += tmp2 !== void 0 ? tmp2 : "null";
90613
- res += join13;
91130
+ res += join14;
90614
91131
  }
90615
91132
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
90616
91133
  res += tmp !== void 0 ? tmp : "null";
90617
91134
  if (value.length - 1 > maximumBreadth) {
90618
91135
  const removedKeys = value.length - maximumBreadth - 1;
90619
- res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
91136
+ res += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
90620
91137
  }
90621
91138
  if (spacer !== "") {
90622
91139
  res += `
@@ -90629,7 +91146,7 @@ ${originalIndentation}`;
90629
91146
  let whitespace = "";
90630
91147
  if (spacer !== "") {
90631
91148
  indentation += spacer;
90632
- join13 = `,
91149
+ join14 = `,
90633
91150
  ${indentation}`;
90634
91151
  whitespace = " ";
90635
91152
  }
@@ -90638,7 +91155,7 @@ ${indentation}`;
90638
91155
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
90639
91156
  if (tmp !== void 0) {
90640
91157
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
90641
- separator = join13;
91158
+ separator = join14;
90642
91159
  }
90643
91160
  }
90644
91161
  if (spacer !== "" && separator.length > 1) {
@@ -90696,20 +91213,20 @@ ${originalIndentation}`;
90696
91213
  indentation += spacer;
90697
91214
  let res2 = `
90698
91215
  ${indentation}`;
90699
- const join14 = `,
91216
+ const join15 = `,
90700
91217
  ${indentation}`;
90701
91218
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
90702
91219
  let i = 0;
90703
91220
  for (; i < maximumValuesToStringify - 1; i++) {
90704
91221
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
90705
91222
  res2 += tmp2 !== void 0 ? tmp2 : "null";
90706
- res2 += join14;
91223
+ res2 += join15;
90707
91224
  }
90708
91225
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
90709
91226
  res2 += tmp !== void 0 ? tmp : "null";
90710
91227
  if (value.length - 1 > maximumBreadth) {
90711
91228
  const removedKeys = value.length - maximumBreadth - 1;
90712
- res2 += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
91229
+ res2 += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
90713
91230
  }
90714
91231
  res2 += `
90715
91232
  ${originalIndentation}`;
@@ -90725,16 +91242,16 @@ ${originalIndentation}`;
90725
91242
  return '"[Object]"';
90726
91243
  }
90727
91244
  indentation += spacer;
90728
- const join13 = `,
91245
+ const join14 = `,
90729
91246
  ${indentation}`;
90730
91247
  let res = "";
90731
91248
  let separator = "";
90732
91249
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
90733
91250
  if (isTypedArrayWithEntries(value)) {
90734
- res += stringifyTypedArray(value, join13, maximumBreadth);
91251
+ res += stringifyTypedArray(value, join14, maximumBreadth);
90735
91252
  keys = keys.slice(value.length);
90736
91253
  maximumPropertiesToStringify -= value.length;
90737
- separator = join13;
91254
+ separator = join14;
90738
91255
  }
90739
91256
  if (deterministic) {
90740
91257
  keys = sort(keys, comparator);
@@ -90745,13 +91262,13 @@ ${indentation}`;
90745
91262
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
90746
91263
  if (tmp !== void 0) {
90747
91264
  res += `${separator}${strEscape(key2)}: ${tmp}`;
90748
- separator = join13;
91265
+ separator = join14;
90749
91266
  }
90750
91267
  }
90751
91268
  if (keyLength > maximumBreadth) {
90752
91269
  const removedKeys = keyLength - maximumBreadth;
90753
91270
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
90754
- separator = join13;
91271
+ separator = join14;
90755
91272
  }
90756
91273
  if (separator !== "") {
90757
91274
  res = `
@@ -114475,7 +114992,7 @@ var require_parse_url = __commonJS({
114475
114992
  var require_form_data = __commonJS({
114476
114993
  "../../node_modules/.pnpm/light-my-request@6.6.0/node_modules/light-my-request/lib/form-data.js"(exports, module) {
114477
114994
  "use strict";
114478
- var { randomUUID: randomUUID9 } = __require("node:crypto");
114995
+ var { randomUUID: randomUUID10 } = __require("node:crypto");
114479
114996
  var { Readable } = __require("node:stream");
114480
114997
  var textEncoder;
114481
114998
  function isFormDataLike(payload) {
@@ -114483,7 +115000,7 @@ var require_form_data = __commonJS({
114483
115000
  }
114484
115001
  function formDataToStream(formdata) {
114485
115002
  textEncoder = textEncoder ?? new TextEncoder();
114486
- const boundary = `----formdata-${randomUUID9()}`;
115003
+ const boundary = `----formdata-${randomUUID10()}`;
114487
115004
  const prefix = `--${boundary}\r
114488
115005
  Content-Disposition: form-data`;
114489
115006
  const escape2 = (str3) => str3.replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22");
@@ -116918,7 +117435,7 @@ ${body}`);
116918
117435
 
116919
117436
  // ../../apps/engine/src/serve-ui.ts
116920
117437
  import { readFile as readFile7, stat as stat3 } from "node:fs/promises";
116921
- import { extname, join as join9, normalize, resolve as resolve2, sep as sep2 } from "node:path";
117438
+ import { extname, join as join10, normalize, resolve as resolve2, sep as sep2 } from "node:path";
116922
117439
  async function isFile(path) {
116923
117440
  try {
116924
117441
  return (await stat3(path)).isFile();
@@ -116926,20 +117443,20 @@ async function isFile(path) {
116926
117443
  return false;
116927
117444
  }
116928
117445
  }
116929
- function serveUi(app2, dir2) {
116930
- const root2 = resolve2(dir2);
117446
+ function serveUi(app2, dir3) {
117447
+ const root2 = resolve2(dir3);
116931
117448
  app2.get("/*", async (request3, reply) => {
116932
117449
  const urlPath = decodeURIComponent((request3.url.split("?")[0] ?? "/").replace(/\/+$/, "") || "/");
116933
- const target = normalize(join9(root2, urlPath));
117450
+ const target = normalize(join10(root2, urlPath));
116934
117451
  if (target !== root2 && !target.startsWith(root2 + sep2)) return reply.code(403).send("Forbidden");
116935
- const candidates = extname(target) ? [target] : [join9(target, "index.html"), `${target}.html`];
117452
+ const candidates = extname(target) ? [target] : [join10(target, "index.html"), `${target}.html`];
116936
117453
  for (const file2 of candidates) {
116937
117454
  if (!await isFile(file2)) continue;
116938
117455
  const type = TYPES[extname(file2).toLowerCase()] ?? "application/octet-stream";
116939
117456
  const immutable = file2.includes(`${sep2}_next${sep2}static${sep2}`);
116940
117457
  return reply.header("content-type", type).header("cache-control", immutable ? "public, max-age=31536000, immutable" : "no-cache").send(await readFile7(file2));
116941
117458
  }
116942
- const notFound = join9(root2, "404.html");
117459
+ const notFound = join10(root2, "404.html");
116943
117460
  return reply.code(404).header("content-type", TYPES[".html"]).send(await isFile(notFound) ? await readFile7(notFound) : "Not found");
116944
117461
  });
116945
117462
  }
@@ -117049,10 +117566,10 @@ var init_history_routes = __esm({
117049
117566
  });
117050
117567
 
117051
117568
  // ../../apps/engine/src/settings-routes.ts
117052
- import { mkdir as mkdir5, readFile as readFile8, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
117053
- import { join as join10 } from "node:path";
117569
+ import { mkdir as mkdir6, readFile as readFile8, rename as rename6, writeFile as writeFile8 } from "node:fs/promises";
117570
+ import { join as join11 } from "node:path";
117054
117571
  function registerSettingsRoutes(app2, dataDir) {
117055
- const file2 = join10(dataDir, "views.json");
117572
+ const file2 = join11(dataDir, "views.json");
117056
117573
  async function read() {
117057
117574
  try {
117058
117575
  return normalizeViewsFile(JSON.parse(await readFile8(file2, "utf8")));
@@ -117064,9 +117581,9 @@ function registerSettingsRoutes(app2, dataDir) {
117064
117581
  app2.put("/settings/views", async (request3, reply) => {
117065
117582
  const next = normalizeViewsFile(request3.body);
117066
117583
  if (next.views.length > 100) return reply.code(400).send({ error: "At most 100 saved views" });
117067
- await mkdir5(dataDir, { recursive: true });
117584
+ await mkdir6(dataDir, { recursive: true });
117068
117585
  const tmp = `${file2}.${process.pid}.tmp`;
117069
- await writeFile7(tmp, `${JSON.stringify(next, null, 2)}
117586
+ await writeFile8(tmp, `${JSON.stringify(next, null, 2)}
117070
117587
  `, "utf8");
117071
117588
  await rename6(tmp, file2);
117072
117589
  return next;
@@ -117168,212 +117685,11 @@ var init_workspace_routes = __esm({
117168
117685
  }
117169
117686
  });
117170
117687
 
117171
- // ../../apps/engine/src/practice-live.ts
117172
- import { createHash as createHash5 } from "node:crypto";
117173
- function frame(opcode, data) {
117174
- const len = data.length;
117175
- const head = len < 126 ? Buffer.from([128 | opcode, len]) : len < 65536 ? Buffer.from([128 | opcode, 126, len >> 8, len & 255]) : Buffer.concat([Buffer.from([128 | opcode, 127]), (() => {
117176
- const b = Buffer.alloc(8);
117177
- b.writeBigUInt64BE(BigInt(len));
117178
- return b;
117179
- })()]);
117180
- return Buffer.concat([head, data]);
117181
- }
117182
- function echo(req, socket) {
117183
- const key = req.headers["sec-websocket-key"];
117184
- if (typeof key !== "string") return socket.destroy();
117185
- const accept = createHash5("sha1").update(key + GUID).digest("base64");
117186
- socket.write(`HTTP/1.1 101 Switching Protocols\r
117187
- Upgrade: websocket\r
117188
- Connection: Upgrade\r
117189
- Sec-WebSocket-Accept: ${accept}\r
117190
- \r
117191
- `);
117192
- const sendText = (value) => socket.write(frame(1, Buffer.from(JSON.stringify(value))));
117193
- sendText({ type: "welcome", message: "Practice WebSocket: send a message and it comes back." });
117194
- let buf = Buffer.alloc(0);
117195
- socket.on("data", (chunk) => {
117196
- buf = Buffer.concat([buf, chunk]);
117197
- for (; ; ) {
117198
- if (buf.length < 2) return;
117199
- const opcode = buf[0] & 15;
117200
- const masked = (buf[1] & 128) !== 0;
117201
- let len = buf[1] & 127;
117202
- let at = 2;
117203
- if (len === 126) {
117204
- if (buf.length < 4) return;
117205
- len = buf.readUInt16BE(2);
117206
- at = 4;
117207
- } else if (len === 127) {
117208
- if (buf.length < 10) return;
117209
- len = Number(buf.readBigUInt64BE(2));
117210
- at = 10;
117211
- }
117212
- if (len > 1e6) return socket.destroy();
117213
- const mask = masked ? buf.subarray(at, at + 4) : null;
117214
- at += masked ? 4 : 0;
117215
- if (buf.length < at + len) return;
117216
- const payload = Buffer.from(buf.subarray(at, at + len));
117217
- if (mask) for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
117218
- buf = buf.subarray(at + len);
117219
- if (opcode === 8) {
117220
- socket.end(frame(8, payload.subarray(0, 2)));
117221
- return;
117222
- }
117223
- if (opcode === 9) socket.write(frame(10, payload));
117224
- else if (opcode === 1) {
117225
- const text = payload.toString("utf8");
117226
- let parsed = text;
117227
- try {
117228
- parsed = JSON.parse(text);
117229
- } catch {
117230
- }
117231
- sendText({ type: "echo", you: parsed, at: (/* @__PURE__ */ new Date()).toISOString() });
117232
- }
117233
- }
117234
- });
117235
- socket.on("error", () => socket.destroy());
117236
- }
117237
- function attachPracticeWebSocket(server, path) {
117238
- server.on("upgrade", (req, socket) => {
117239
- if ((req.url ?? "").split("?")[0] !== path) return;
117240
- echo(req, socket);
117241
- });
117242
- }
117243
- function practiceEvents(res, onGone, count = 10, everyMs = 1e3) {
117244
- res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
117245
- res.write(": practice event stream \u2014 a tick a second\n\n");
117246
- let n = 0;
117247
- const timer = setInterval(() => {
117248
- n++;
117249
- res.write(`event: tick
117250
- id: ${n}
117251
- data: ${JSON.stringify({ n, at: (/* @__PURE__ */ new Date()).toISOString(), price: Math.round((40 + Math.random() * 5) * 100) / 100 })}
117252
-
117253
- `);
117254
- if (n >= count) {
117255
- clearInterval(timer);
117256
- res.end();
117257
- }
117258
- }, everyMs);
117259
- onGone(() => clearInterval(timer));
117260
- }
117261
- var GUID;
117262
- var init_practice_live = __esm({
117263
- "../../apps/engine/src/practice-live.ts"() {
117264
- "use strict";
117265
- GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
117266
- }
117267
- });
117268
-
117269
- // ../../apps/engine/src/demo-api.ts
117270
- import { randomBytes as randomBytes5 } from "node:crypto";
117271
- function registerDemoApi(api) {
117272
- const tokens = /* @__PURE__ */ new Map();
117273
- const created = /* @__PURE__ */ new Map();
117274
- let nextOrder = 2001;
117275
- const signedIn = (auth) => {
117276
- const header = auth ?? "";
117277
- return header.startsWith("Bearer ") ? tokens.get(header.slice(7)) : void 0;
117278
- };
117279
- api.get("/products", async () => {
117280
- await jitter(5, 25);
117281
- return { items: PRODUCTS, total: PRODUCTS.length };
117282
- });
117283
- api.get("/products/:id", async (request3, reply) => {
117284
- await jitter(5, 20);
117285
- const product = PRODUCTS.find((p) => String(p.id) === request3.params.id);
117286
- if (!product) return reply.code(404).send({ error: `No product ${request3.params.id}. Try 1 to ${PRODUCTS.length}.` });
117287
- return product;
117288
- });
117289
- api.post("/login", async (request3, reply) => {
117290
- await jitter(30, 40);
117291
- const username = typeof request3.body?.username === "string" ? request3.body.username.trim() : "";
117292
- if (!username || request3.body?.password !== "demo") {
117293
- return reply.code(401).send({ error: 'Use any username with the password "demo".' });
117294
- }
117295
- if (tokens.size > 5e3) tokens.clear();
117296
- const token = `demo-${randomBytes5(12).toString("hex")}`;
117297
- tokens.set(token, username);
117298
- return { data: { token, user: username, expiresIn: 3600 } };
117299
- });
117300
- api.get("/orders", async (request3, reply) => {
117301
- await jitter(10, 30);
117302
- const auth = request3.headers.authorization ?? "";
117303
- const user = auth.startsWith("Bearer ") ? tokens.get(auth.slice(7)) : void 0;
117304
- if (!user) return reply.code(401).send({ error: "Sign in first: POST /login, then send the token as a Bearer token." });
117305
- return {
117306
- user,
117307
- orders: [
117308
- { id: "A-1001", items: [PRODUCTS[0].name, PRODUCTS[1].name], total: PRODUCTS[0].price + PRODUCTS[1].price },
117309
- { id: "A-1002", items: [PRODUCTS[3].name], total: PRODUCTS[3].price }
117310
- ]
117311
- };
117312
- });
117313
- api.post("/orders", async (request3, reply) => {
117314
- await jitter(15, 30);
117315
- const user = signedIn(request3.headers.authorization);
117316
- if (!user) return reply.code(401).send({ error: "Sign in first: POST /login, then send the token as a Bearer token." });
117317
- const product = PRODUCTS.find((p) => p.id === Number(request3.body?.productId));
117318
- if (!product) return reply.code(400).send({ error: `Send { "productId": 1 } \u2014 a product id from 1 to ${PRODUCTS.length}.` });
117319
- const quantity = Math.min(99, Math.max(1, Math.floor(Number(request3.body?.quantity ?? 1)) || 1));
117320
- if (created.size > 5e3) created.clear();
117321
- const order = { id: `A-${nextOrder++}`, user, productId: product.id, quantity, total: product.price * quantity, status: "new" };
117322
- created.set(order.id, order);
117323
- return reply.code(201).send(order);
117324
- });
117325
- api.get("/orders/:id", async (request3, reply) => {
117326
- await jitter(5, 20);
117327
- if (!signedIn(request3.headers.authorization)) return reply.code(401).send({ error: "Sign in first: POST /login, then send the token as a Bearer token." });
117328
- const order = created.get(request3.params.id);
117329
- if (!order) return reply.code(404).send({ error: `No order ${request3.params.id}. Create one with POST /orders.` });
117330
- return order;
117331
- });
117332
- api.get("/slow", async (request3) => {
117333
- const ms3 = Math.min(3e3, Math.max(0, Number(request3.query.ms ?? 300) || 0));
117334
- await jitter(ms3, ms3 * 0.2);
117335
- return { waitedMs: ms3 };
117336
- });
117337
- api.get("/flaky", async (_request, reply) => {
117338
- await jitter(5, 30);
117339
- if (Math.random() < 0.1) return reply.code(500).send({ error: "Something went wrong (on purpose, about 1 in 10)." });
117340
- return { ok: true };
117341
- });
117342
- api.get("/events", async (request3, reply) => {
117343
- reply.hijack();
117344
- practiceEvents(reply.raw, (stop) => request3.raw.on("close", stop));
117345
- });
117346
- attachPracticeWebSocket(api.server, `${api.prefix}/ws`);
117347
- }
117348
- var PRODUCTS, sleep, jitter;
117349
- var init_demo_api = __esm({
117350
- "../../apps/engine/src/demo-api.ts"() {
117351
- "use strict";
117352
- init_practice_live();
117353
- PRODUCTS = [
117354
- ["Desk lamp", 39],
117355
- ["Notebook", 6],
117356
- ["Coffee mug", 12],
117357
- ["Headphones", 129],
117358
- ["Backpack", 74],
117359
- ["Water bottle", 18],
117360
- ["Keyboard", 89],
117361
- ["Plant pot", 15],
117362
- ["Phone stand", 22],
117363
- ["Sunglasses", 58],
117364
- ["Umbrella", 27],
117365
- ["Wall clock", 34]
117366
- ].map(([name, price], i) => ({ id: i + 1, name, price, inStock: i % 4 !== 3 }));
117367
- sleep = (ms3) => new Promise((r) => setTimeout(r, ms3));
117368
- jitter = (base, spread) => sleep(base + Math.random() * spread);
117369
- }
117370
- });
117371
-
117372
117688
  // ../../apps/engine/src/usage.ts
117373
- import { randomUUID as randomUUID7 } from "node:crypto";
117374
- import { mkdir as mkdir6, readFile as readFile9, rename as rename7, writeFile as writeFile8 } from "node:fs/promises";
117689
+ import { randomUUID as randomUUID8 } from "node:crypto";
117690
+ import { mkdir as mkdir7, readFile as readFile9, rename as rename7, writeFile as writeFile9 } from "node:fs/promises";
117375
117691
  import { arch, platform } from "node:os";
117376
- import { join as join11 } from "node:path";
117692
+ import { join as join12 } from "node:path";
117377
117693
  var EMPTY, KEEP_DAYS, UsageStats;
117378
117694
  var init_usage = __esm({
117379
117695
  "../../apps/engine/src/usage.ts"() {
@@ -117385,8 +117701,8 @@ var init_usage = __esm({
117385
117701
  opts;
117386
117702
  state = null;
117387
117703
  writing = Promise.resolve();
117388
- constructor(dir2, opts) {
117389
- this.file = join11(dir2, "usage.json");
117704
+ constructor(dir3, opts) {
117705
+ this.file = join12(dir3, "usage.json");
117390
117706
  this.opts = opts;
117391
117707
  }
117392
117708
  async load() {
@@ -117395,20 +117711,20 @@ var init_usage = __esm({
117395
117711
  const raw = JSON.parse(await readFile9(this.file, "utf8"));
117396
117712
  this.state = {
117397
117713
  enabled: typeof raw.enabled === "boolean" ? raw.enabled : null,
117398
- installId: typeof raw.installId === "string" && raw.installId ? raw.installId : randomUUID7(),
117714
+ installId: typeof raw.installId === "string" && raw.installId ? raw.installId : randomUUID8(),
117399
117715
  days: raw.days && typeof raw.days === "object" ? raw.days : {}
117400
117716
  };
117401
117717
  } catch {
117402
- this.state = { enabled: null, installId: randomUUID7(), days: {} };
117718
+ this.state = { enabled: null, installId: randomUUID8(), days: {} };
117403
117719
  }
117404
117720
  return this.state;
117405
117721
  }
117406
117722
  save() {
117407
117723
  this.writing = this.writing.then(async () => {
117408
117724
  if (!this.state) return;
117409
- await mkdir6(join11(this.file, ".."), { recursive: true });
117725
+ await mkdir7(join12(this.file, ".."), { recursive: true });
117410
117726
  const tmp = `${this.file}.${process.pid}.tmp`;
117411
- await writeFile8(tmp, `${JSON.stringify(this.state, null, 2)}
117727
+ await writeFile9(tmp, `${JSON.stringify(this.state, null, 2)}
117412
117728
  `, "utf8");
117413
117729
  await rename7(tmp, this.file);
117414
117730
  }).catch(() => void 0);
@@ -117448,7 +117764,7 @@ var init_usage = __esm({
117448
117764
  const s = await this.load();
117449
117765
  s.enabled = enabled;
117450
117766
  if (!enabled) {
117451
- s.installId = randomUUID7();
117767
+ s.installId = randomUUID8();
117452
117768
  s.days = {};
117453
117769
  }
117454
117770
  await this.save();
@@ -117632,8 +117948,8 @@ var init_local_servers = __esm({
117632
117948
  });
117633
117949
 
117634
117950
  // ../../apps/engine/src/response-history.ts
117635
- import { mkdir as mkdir7, readFile as readFile10, rm as rm3, writeFile as writeFile9 } from "node:fs/promises";
117636
- import { join as join12 } from "node:path";
117951
+ import { mkdir as mkdir8, readFile as readFile10, rm as rm3, writeFile as writeFile10 } from "node:fs/promises";
117952
+ import { join as join13 } from "node:path";
117637
117953
  function maskSecrets(body) {
117638
117954
  const t = body.trim();
117639
117955
  if (!(t.startsWith("{") || t.startsWith("["))) return { body, masked: false };
@@ -117671,11 +117987,11 @@ var init_response_history = __esm({
117671
117987
  dir;
117672
117988
  queue = Promise.resolve();
117673
117989
  constructor(dataDir) {
117674
- this.dir = join12(dataDir, "responses");
117990
+ this.dir = join13(dataDir, "responses");
117675
117991
  }
117676
117992
  file(id) {
117677
117993
  if (!/^[\w-]{8,64}$/.test(id)) throw new Error("bad request id");
117678
- return join12(this.dir, `${id}.json`);
117994
+ return join13(this.dir, `${id}.json`);
117679
117995
  }
117680
117996
  async read(id) {
117681
117997
  try {
@@ -117703,8 +118019,8 @@ var init_response_history = __esm({
117703
118019
  ...masked ? { masked } : {}
117704
118020
  };
117705
118021
  const answers2 = [answer, ...await this.read(id)].slice(0, KEEP);
117706
- await mkdir7(this.dir, { recursive: true });
117707
- await writeFile9(this.file(id), JSON.stringify({ version: 1, answers: answers2 }, null, 1), "utf8");
118022
+ await mkdir8(this.dir, { recursive: true });
118023
+ await writeFile10(this.file(id), JSON.stringify({ version: 1, answers: answers2 }, null, 1), "utf8");
117708
118024
  });
117709
118025
  this.queue = next.catch(() => void 0);
117710
118026
  return next;
@@ -117826,6 +118142,8 @@ async function routes(api) {
117826
118142
  useOAuthStore(secrets2);
117827
118143
  registerOAuthRoutes(api);
117828
118144
  useCertificateStore(history.dir, secrets2);
118145
+ useFileStore(history.dir);
118146
+ await api.register(async (scope) => registerFileRoutes(scope));
117829
118147
  useCookieStore(secrets2);
117830
118148
  registerCookieRoutes(api);
117831
118149
  registerCertificateRoutes(api);
@@ -117941,6 +118259,7 @@ var init_index = __esm({
117941
118259
  init_suite_run();
117942
118260
  init_oauth();
117943
118261
  init_certificates();
118262
+ init_file_store();
117944
118263
  init_cookies();
117945
118264
  init_local_servers();
117946
118265
  init_mock_server();
@@ -117998,9 +118317,9 @@ init_history_store();
117998
118317
  init_k6();
117999
118318
  import { spawn as spawn3, spawnSync } from "node:child_process";
118000
118319
  import { tmpdir as tmpdir3 } from "node:os";
118001
- import { randomUUID as randomUUID8 } from "node:crypto";
118002
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
118003
- import { readFile as readFile11, writeFile as writeFile10 } from "node:fs/promises";
118320
+ import { randomUUID as randomUUID9 } from "node:crypto";
118321
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
118322
+ import { readFile as readFile11, writeFile as writeFile11 } from "node:fs/promises";
118004
118323
  import { dirname as dirname3, resolve as resolve4 } from "node:path";
118005
118324
  import { fileURLToPath as fileURLToPath2 } from "node:url";
118006
118325
  import { parseArgs } from "node:util";
@@ -118051,9 +118370,9 @@ async function downloadK6(log = () => {
118051
118370
  const tar = process.platform === "win32" && existsSync(systemTar) ? systemTar : "tar";
118052
118371
  execFileSync(tar, ["-xf", asset], { cwd: work, stdio: "ignore" });
118053
118372
  const exe = process.platform === "win32" ? "k6.exe" : "k6";
118054
- const find = (dir3) => {
118055
- for (const name of readdirSync(dir3)) {
118056
- const p = join3(dir3, name);
118373
+ const find = (dir4) => {
118374
+ for (const name of readdirSync(dir4)) {
118375
+ const p = join3(dir4, name);
118057
118376
  if (statSync(p).isDirectory()) {
118058
118377
  const hit = find(p);
118059
118378
  if (hit) return hit;
@@ -118063,9 +118382,9 @@ async function downloadK6(log = () => {
118063
118382
  };
118064
118383
  const found = find(work);
118065
118384
  if (!found) throw new Error(`${exe} was not in ${asset}`);
118066
- const dir2 = k6CacheDir();
118067
- mkdirSync(dir2, { recursive: true });
118068
- const dest = join3(dir2, exe);
118385
+ const dir3 = k6CacheDir();
118386
+ mkdirSync(dir3, { recursive: true });
118387
+ const dest = join3(dir3, exe);
118069
118388
  copyFileSync(found, dest);
118070
118389
  if (process.platform !== "win32") chmodSync(dest, 493);
118071
118390
  log(`k6 saved to ${dest}`);
@@ -118082,8 +118401,10 @@ init_secret_store();
118082
118401
  init_suite_run();
118083
118402
  init_oauth();
118084
118403
  init_certificates();
118404
+ init_file_store();
118085
118405
  init_cookies();
118086
118406
  init_workspace_store();
118407
+ init_demo_api();
118087
118408
  init_mock_server();
118088
118409
 
118089
118410
  // ../../apps/engine/src/mcp.ts
@@ -118442,7 +118763,7 @@ async function serveMcp(o) {
118442
118763
  // ../../apps/engine/src/cli.ts
118443
118764
  var repoRoot = resolve4(dirname3(fileURLToPath2(import.meta.url)), "../../..");
118444
118765
  var EXIT = { passed: 0, failed: 1, error: 2 };
118445
- var VERSION = "0.12.0";
118766
+ var VERSION = "0.13.1";
118446
118767
  var HELP = `APITestCenter CLI ${VERSION} \u2014 https://apitestcenter.com/docs/cli-and-ci.html
118447
118768
 
118448
118769
  Usage (apitestcenter, or the short name atc once installed)
@@ -118550,7 +118871,7 @@ async function main() {
118550
118871
  process.exit(values.help ? EXIT.passed : EXIT.error);
118551
118872
  }
118552
118873
  if (values.data && command !== "init") {
118553
- if (!existsSync2(values.data)) fail(`no folder "${values.data}"`);
118874
+ if (!existsSync3(values.data)) fail(`no folder "${values.data}"`);
118554
118875
  process.env.ATC_DATA_DIR = resolve4(values.data);
118555
118876
  }
118556
118877
  if (command === "init") return init(target, values.data ?? "api-tests");
@@ -118560,6 +118881,7 @@ async function main() {
118560
118881
  const workspace = new WorkspaceStore(dataDir, secrets2);
118561
118882
  useOAuthStore(secrets2);
118562
118883
  useCertificateStore(dataDir, secrets2);
118884
+ useFileStore(dataDir);
118563
118885
  useCookieStore(secrets2);
118564
118886
  for (const spec of values.cert ?? []) {
118565
118887
  const eq = spec.indexOf("=");
@@ -118604,7 +118926,7 @@ Environments (${index.environments.length})`);
118604
118926
  let raw;
118605
118927
  let name;
118606
118928
  let collection = "";
118607
- if (existsSync2(target) && target.toLowerCase().endsWith(".json")) {
118929
+ if (existsSync3(target) && target.toLowerCase().endsWith(".json")) {
118608
118930
  const file2 = JSON.parse(await readFile11(target, "utf8"));
118609
118931
  raw = file2.request ?? file2;
118610
118932
  name = file2.name ?? target;
@@ -118621,7 +118943,7 @@ Environments (${index.environments.length})`);
118621
118943
  }
118622
118944
  const env = values.env ? index.environments.find((e) => e.name === values.env || e.id === values.env) : void 0;
118623
118945
  if (values.env && !env) fail(`no environment "${values.env}". Run "atc list".`);
118624
- const vars2 = variablesOf(env);
118946
+ const vars2 = { ...JSON.stringify(raw).includes("{{practice") ? await practiceVars() : {}, ...variablesOf(env) };
118625
118947
  for (const pair of values.var ?? []) {
118626
118948
  const i = pair.indexOf("=");
118627
118949
  if (i < 1) fail(`--var needs key=value, got "${pair}"`);
@@ -118742,12 +119064,12 @@ compared with baseline \u201C${baseline.testName}\u201D (${baseline.startedAt.sl
118742
119064
  console.log(` ${compared.headline}`);
118743
119065
  console.log(` ${gate.failed ? "\u2715" : "\u2713"} ${gate.reason}`);
118744
119066
  }
118745
- if (values.json) await writeFile10(values.json, exportRunJson(report), "utf8");
118746
- if (values.html) await writeFile10(values.html, exportRunHtml(report, { slo, before: baseline }), "utf8");
119067
+ if (values.json) await writeFile11(values.json, exportRunJson(report), "utf8");
119068
+ if (values.html) await writeFile11(values.html, exportRunHtml(report, { slo, before: baseline }), "utf8");
118747
119069
  const extra2 = gate ? [{ classname: "baseline", name: `no slower than the baseline by more than ${Math.round(maxSlower * 100)}%`, failure: gate.failed ? gate.reason : void 0 }] : [];
118748
- if (values.junit) await writeFile10(values.junit, exportRunJunit(report, verdict, targets?.checksRate ?? 0.99, extra2), "utf8");
119070
+ if (values.junit) await writeFile11(values.junit, exportRunJunit(report, verdict, targets?.checksRate ?? 0.99, extra2), "utf8");
118749
119071
  if (values["save-baseline"]) {
118750
- await writeFile10(values["save-baseline"], exportRunJson(report), "utf8");
119072
+ await writeFile11(values["save-baseline"], exportRunJson(report), "utf8");
118751
119073
  log(` saved this run as a baseline \u2192 ${values["save-baseline"]}`);
118752
119074
  }
118753
119075
  for (const [flag, file2] of [["json", values.json], ["html", values.html], ["junit", values.junit]]) if (file2) log(` wrote ${flag} \u2192 ${file2}`);
@@ -118756,7 +119078,7 @@ compared with baseline \u201C${baseline.testName}\u201D (${baseline.startedAt.sl
118756
119078
  }
118757
119079
  async function ui(collection, dataDir, noOpen, inBrowserTab = false) {
118758
119080
  const here = dirname3(fileURLToPath2(import.meta.url));
118759
- const uiDir2 = [process.env.ATC_UI_DIR, resolve4(here, "ui"), resolve4(repoRoot, "apps/web/.next-desktop")].find((d) => Boolean(d) && existsSync2(resolve4(d, "index.html")));
119081
+ const uiDir2 = [process.env.ATC_UI_DIR, resolve4(here, "ui"), resolve4(repoRoot, "apps/web/.next-desktop")].find((d) => Boolean(d) && existsSync3(resolve4(d, "index.html")));
118760
119082
  if (!uiDir2) fail("this build has no app screens. Use the npm package (npx apitestcenter ui), or run: pnpm --filter @apitestcenter/desktop build:ui");
118761
119083
  Object.assign(process.env, {
118762
119084
  ATC_UI_DIR: uiDir2,
@@ -118812,7 +119134,7 @@ function findAppBrowser() {
118812
119134
  const candidates = process.platform === "win32" ? [env["ProgramFiles(x86)"], env.ProgramFiles, env.LOCALAPPDATA].flatMap(
118813
119135
  (root2) => root2 ? [`${root2}\\Microsoft\\Edge\\Application\\msedge.exe`, `${root2}\\Google\\Chrome\\Application\\chrome.exe`, `${root2}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`] : []
118814
119136
  ) : process.platform === "darwin" ? ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"] : ["/usr/bin/microsoft-edge", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/brave-browser", "/snap/bin/chromium"];
118815
- return candidates.find((p) => existsSync2(p)) ?? null;
119137
+ return candidates.find((p) => existsSync3(p)) ?? null;
118816
119138
  }
118817
119139
  function openAppWindow(url2) {
118818
119140
  const bin = findAppBrowser();
@@ -118844,24 +119166,24 @@ function openBrowser(url2) {
118844
119166
  }
118845
119167
  async function init(collection, folder) {
118846
119168
  const name = collection?.trim() || "My API";
118847
- const dir2 = resolve4(folder);
118848
- if (existsSync2(resolve4(dir2, "requests")) && readdirSync2(resolve4(dir2, "requests")).length) {
119169
+ const dir3 = resolve4(folder);
119170
+ if (existsSync3(resolve4(dir3, "requests")) && readdirSync2(resolve4(dir3, "requests")).length) {
118849
119171
  fail(`${folder} already has saved requests. Open them with: npx apitestcenter ui --data ${folder}`);
118850
119172
  }
118851
- mkdirSync2(dir2, { recursive: true });
118852
- const store3 = new WorkspaceStore(dir2, new SecretStore(dir2));
118853
- await store3.saveEnvironments([{ id: randomUUID8(), name: "Local", variables: [{ key: "baseUrl", value: "http://localhost:3000", secret: false }] }]);
119173
+ mkdirSync2(dir3, { recursive: true });
119174
+ const store3 = new WorkspaceStore(dir3, new SecretStore(dir3));
119175
+ await store3.saveEnvironments([{ id: randomUUID9(), name: "Local", variables: [{ key: "baseUrl", value: "http://localhost:3000", secret: false }] }]);
118854
119176
  await store3.saveRequest({
118855
119177
  name: "Health check",
118856
119178
  collection: name,
118857
119179
  request: { method: "GET", url: "{{baseUrl}}/health", checks: [{ type: "status", expect: "2xx" }] }
118858
119180
  });
118859
119181
  const data = folder.replace(/\\/g, "/").replace(/\/+$/, "");
118860
- writeFileSync2(resolve4(dir2, "README.md"), ciReadme({ collection: name, environment: "Local", data }));
118861
- const ignore = resolve4(dir2, ".gitignore");
118862
- if (!existsSync2(ignore)) writeFileSync2(ignore, ["# Kept by APITestCenter on this computer \u2014 not tests, so not in git", "responses/", "runs/", "compares/", "usage.json", "secrets.enc", ""].join("\n"));
119182
+ writeFileSync2(resolve4(dir3, "README.md"), ciReadme({ collection: name, environment: "Local", data }));
119183
+ const ignore = resolve4(dir3, ".gitignore");
119184
+ if (!existsSync3(ignore)) writeFileSync2(ignore, ["# Kept by APITestCenter on this computer \u2014 not tests, so not in git", "responses/", "runs/", "compares/", "usage.json", "secrets.enc", ""].join("\n"));
118863
119185
  const workflow = resolve4(".github/workflows/api-checks.yml");
118864
- const wroteWorkflow = !existsSync2(workflow);
119186
+ const wroteWorkflow = !existsSync3(workflow);
118865
119187
  if (wroteWorkflow) {
118866
119188
  mkdirSync2(dirname3(workflow), { recursive: true });
118867
119189
  writeFileSync2(workflow, ciWorkflow({ collection: name, environment: "Local", data }));
@@ -118877,6 +119199,8 @@ async function init(collection, folder) {
118877
119199
  async function suite(collection, values, workspace, dataDir) {
118878
119200
  if (collection === void 0) fail('suite needs a collection name, or "" for requests outside any collection. Run "atc list".');
118879
119201
  const vars2 = {};
119202
+ const inside = (await workspace.index()).requests.filter((r) => collection === "" ? r.collection === "" : r.collection === collection || r.collection.startsWith(`${collection}/`));
119203
+ if (inside.some((r) => r.url.includes("{{practice"))) Object.assign(vars2, await practiceVars());
118880
119204
  for (const pair of values.var ?? []) {
118881
119205
  const i = pair.indexOf("=");
118882
119206
  if (i < 1) fail(`--var needs key=value, got "${pair}"`);
@@ -118913,12 +119237,22 @@ async function suite(collection, values, workspace, dataDir) {
118913
119237
  const ok = suitePassed(run2);
118914
119238
  console.log(`
118915
119239
  ${ok ? "PASS" : "FAIL"} ${suiteHeadline(run2)}`);
118916
- if (values.json) await writeFile10(values.json, exportSuiteJson(run2), "utf8");
118917
- if (values.html) await writeFile10(values.html, exportSuiteHtml(run2), "utf8");
118918
- if (values.junit) await writeFile10(values.junit, exportSuiteJunit(run2), "utf8");
119240
+ if (values.json) await writeFile11(values.json, exportSuiteJson(run2), "utf8");
119241
+ if (values.html) await writeFile11(values.html, exportSuiteHtml(run2), "utf8");
119242
+ if (values.junit) await writeFile11(values.junit, exportSuiteJunit(run2), "utf8");
118919
119243
  for (const [flag, file2] of [["json", values.json], ["html", values.html], ["junit", values.junit]]) if (file2) log(` wrote ${flag} \u2192 ${file2}`);
118920
119244
  return ok ? EXIT.passed : EXIT.failed;
118921
119245
  }
119246
+ async function practiceVars() {
119247
+ const { default: Fastify2 } = await Promise.resolve().then(() => __toESM(require_fastify(), 1));
119248
+ const app2 = Fastify2({ logger: false });
119249
+ await app2.register(async (demo) => registerDemoApi(demo), { prefix: "/demo-api" });
119250
+ await app2.listen({ host: "127.0.0.1", port: 0 });
119251
+ app2.server.unref();
119252
+ const address = app2.server.address();
119253
+ const port2 = typeof address === "object" && address ? address.port : 0;
119254
+ return { practiceApi: `http://127.0.0.1:${port2}/demo-api`, practiceWs: `ws://127.0.0.1:${port2}/demo-api` };
119255
+ }
118922
119256
  async function mock(collection, portText, workspace) {
118923
119257
  if (collection === void 0) fail('say which collection to mock, e.g. atc mock "Orders API" (or "" for all requests)');
118924
119258
  const port2 = portText === void 0 ? 4010 : Number(portText);