lacspace-http 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.js ADDED
@@ -0,0 +1,862 @@
1
+ // src/jsonpath.ts
2
+ var IDENT = /[A-Za-z0-9_$-]/;
3
+ function parseJsonPath(path) {
4
+ const s = path.trim();
5
+ const segs = [];
6
+ let i = 0;
7
+ if (s[i] === "$") i++;
8
+ if (segs.length === 0 && i < s.length && s[i] !== "." && s[i] !== "[") {
9
+ let j = i;
10
+ while (j < s.length && IDENT.test(s[j])) j++;
11
+ if (j === i) throw new Error(`Invalid JSON path: "${path}"`);
12
+ segs.push({ type: "key", key: s.slice(i, j) });
13
+ i = j;
14
+ }
15
+ while (i < s.length) {
16
+ const ch = s[i];
17
+ if (ch === ".") {
18
+ i++;
19
+ let j = i;
20
+ while (j < s.length && IDENT.test(s[j])) j++;
21
+ if (j === i) throw new Error(`Invalid JSON path: "${path}"`);
22
+ segs.push({ type: "key", key: s.slice(i, j) });
23
+ i = j;
24
+ } else if (ch === "[") {
25
+ i++;
26
+ const quote = s[i];
27
+ if (quote === "'" || quote === '"') {
28
+ i++;
29
+ let j = i;
30
+ while (j < s.length && s[j] !== quote) j++;
31
+ if (j >= s.length) throw new Error(`Unterminated quote in JSON path: "${path}"`);
32
+ segs.push({ type: "key", key: s.slice(i, j) });
33
+ i = j + 1;
34
+ } else {
35
+ let j = i;
36
+ while (j < s.length && s[j] !== "]") j++;
37
+ const raw = s.slice(i, j).trim();
38
+ const num = Number(raw);
39
+ if (raw === "" || !Number.isInteger(num)) {
40
+ throw new Error(`Invalid array index "${raw}" in JSON path: "${path}"`);
41
+ }
42
+ segs.push({ type: "index", index: num });
43
+ i = j;
44
+ }
45
+ if (s[i] !== "]") throw new Error(`Expected "]" in JSON path: "${path}"`);
46
+ i++;
47
+ } else {
48
+ throw new Error(`Unexpected "${ch}" in JSON path: "${path}"`);
49
+ }
50
+ }
51
+ return segs;
52
+ }
53
+ function evalPath(root, path) {
54
+ const segs = parseJsonPath(path);
55
+ let cur = root;
56
+ for (const seg of segs) {
57
+ if (cur === null || cur === void 0) return void 0;
58
+ if (seg.type === "key") {
59
+ if (typeof cur !== "object") return void 0;
60
+ cur = cur[seg.key];
61
+ } else {
62
+ if (!Array.isArray(cur)) return void 0;
63
+ const idx = seg.index < 0 ? cur.length + seg.index : seg.index;
64
+ cur = cur[idx];
65
+ }
66
+ }
67
+ return cur;
68
+ }
69
+
70
+ // src/vars.ts
71
+ import { randomUUID } from "crypto";
72
+ function makeScope(...sources) {
73
+ const scope = /* @__PURE__ */ new Map();
74
+ for (const src of sources) {
75
+ if (!src) continue;
76
+ for (const [k, v] of Object.entries(src)) scope.set(k, v);
77
+ }
78
+ return scope;
79
+ }
80
+ function resolveOne(expr, scope) {
81
+ if (scope.has(expr)) return scope.get(expr);
82
+ if (expr.startsWith("$")) {
83
+ const [name, ...rest] = expr.slice(1).split(/\s+/);
84
+ switch (name) {
85
+ case "timestamp":
86
+ return String(Math.floor(Date.now() / 1e3));
87
+ case "isoTimestamp":
88
+ case "datetime":
89
+ return (/* @__PURE__ */ new Date()).toISOString();
90
+ case "guid":
91
+ case "uuid":
92
+ return randomUUID();
93
+ case "randomInt": {
94
+ const min = Number(rest[0] ?? 0);
95
+ const max = Number(rest[1] ?? 100);
96
+ if (Number.isNaN(min) || Number.isNaN(max) || max < min) return void 0;
97
+ return String(min + Math.floor(Math.random() * (max - min + 1)));
98
+ }
99
+ case "processEnv":
100
+ case "env": {
101
+ const key = rest[0];
102
+ return key ? process.env[key] : void 0;
103
+ }
104
+ default:
105
+ return void 0;
106
+ }
107
+ }
108
+ return void 0;
109
+ }
110
+ function resolveVars(text, scope) {
111
+ const missing = [];
112
+ const out = text.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (whole, rawExpr) => {
113
+ const expr = rawExpr.trim();
114
+ const val = resolveOne(expr, scope);
115
+ if (val === void 0) {
116
+ missing.push(expr);
117
+ return whole;
118
+ }
119
+ return val;
120
+ });
121
+ return { text: out, missing };
122
+ }
123
+ function parseEnvJson(json, envName) {
124
+ let doc;
125
+ try {
126
+ doc = JSON.parse(json);
127
+ } catch (err) {
128
+ throw new Error(`Invalid env JSON: ${err.message}`);
129
+ }
130
+ if (typeof doc !== "object" || doc === null) {
131
+ throw new Error("Env JSON must be an object of { envName: { key: value } }.");
132
+ }
133
+ const record = doc;
134
+ const flatten = (block) => {
135
+ const out = {};
136
+ if (typeof block !== "object" || block === null) return out;
137
+ for (const [k, v] of Object.entries(block)) {
138
+ out[k] = typeof v === "string" ? v : JSON.stringify(v);
139
+ }
140
+ return out;
141
+ };
142
+ const shared = flatten(record["$shared"]);
143
+ if (!(envName in record)) {
144
+ const names = Object.keys(record).filter((n) => n !== "$shared");
145
+ throw new Error(`Environment "${envName}" not found. Available: ${names.join(", ") || "(none)"}`);
146
+ }
147
+ return { ...shared, ...flatten(record[envName]) };
148
+ }
149
+ function parseDotenv(text) {
150
+ const out = {};
151
+ for (const line of text.split(/\r?\n/)) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed || trimmed.startsWith("#")) continue;
154
+ const eq = trimmed.indexOf("=");
155
+ if (eq === -1) continue;
156
+ const key = trimmed.slice(0, eq).trim().replace(/^export\s+/, "");
157
+ let val = trimmed.slice(eq + 1).trim();
158
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
159
+ val = val.slice(1, -1);
160
+ }
161
+ if (key) out[key] = val;
162
+ }
163
+ return out;
164
+ }
165
+ function parseKvPairs(pairs) {
166
+ const out = {};
167
+ for (const p of pairs) {
168
+ const eq = p.indexOf("=");
169
+ if (eq === -1) continue;
170
+ out[p.slice(0, eq).trim()] = p.slice(eq + 1);
171
+ }
172
+ return out;
173
+ }
174
+
175
+ // src/httpfile.ts
176
+ var METHODS = /* @__PURE__ */ new Set([
177
+ "GET",
178
+ "HEAD",
179
+ "POST",
180
+ "PUT",
181
+ "PATCH",
182
+ "DELETE",
183
+ "OPTIONS",
184
+ "TRACE",
185
+ "CONNECT"
186
+ ]);
187
+ function isSeparator(line) {
188
+ return /^###/.test(line.trim());
189
+ }
190
+ function isComment(line) {
191
+ const t = line.trim();
192
+ return t.startsWith("#") || t.startsWith("//");
193
+ }
194
+ function commentBody(line) {
195
+ const t = line.trim();
196
+ if (t.startsWith("//")) return t.slice(2).trim();
197
+ return t.replace(/^#+/, "").trim();
198
+ }
199
+ function parseRequestLine(line) {
200
+ const parts = line.trim().split(/\s+/);
201
+ let method = "GET";
202
+ let rest = parts;
203
+ const first = (parts[0] ?? "").toUpperCase();
204
+ if (METHODS.has(first)) {
205
+ method = first;
206
+ rest = parts.slice(1);
207
+ }
208
+ if (rest.length > 1 && /^HTTP\/\d/i.test(rest[rest.length - 1])) rest = rest.slice(0, -1);
209
+ const url = rest.join(" ").trim();
210
+ return { method, url };
211
+ }
212
+ function parseHttpFile(source) {
213
+ const rawLines = source.split(/\r?\n/);
214
+ const blocks = [];
215
+ let current = { start: 0, lines: [] };
216
+ rawLines.forEach((line, idx) => {
217
+ if (isSeparator(line)) {
218
+ blocks.push(current);
219
+ current = { start: idx + 1, lines: [] };
220
+ } else {
221
+ current.lines.push(line);
222
+ }
223
+ });
224
+ blocks.push(current);
225
+ const requests = [];
226
+ for (const block of blocks) {
227
+ const req = parseBlock(block.lines, block.start);
228
+ if (req) requests.push(req);
229
+ }
230
+ return requests;
231
+ }
232
+ function parseBlock(lines, startLine) {
233
+ let name;
234
+ const captures = [];
235
+ const assertions = [];
236
+ let i = 0;
237
+ for (; i < lines.length; i++) {
238
+ const line = lines[i];
239
+ if (line.trim() === "") continue;
240
+ if (!isComment(line)) break;
241
+ const body2 = commentBody(line);
242
+ const at = /^@(\w+)\s*(.*)$/.exec(body2);
243
+ if (at) {
244
+ const directive2 = at[1].toLowerCase();
245
+ const arg = at[2].trim();
246
+ if (directive2 === "name") name = arg.replace(/^=?\s*/, "").trim() || void 0;
247
+ else if (directive2 === "capture") {
248
+ const eq = arg.indexOf("=");
249
+ if (eq !== -1) {
250
+ const cname = arg.slice(0, eq).trim();
251
+ const source = arg.slice(eq + 1).trim();
252
+ if (cname && source) captures.push({ name: cname, source });
253
+ }
254
+ } else if (directive2 === "assert") {
255
+ if (arg) assertions.push({ expr: arg });
256
+ }
257
+ }
258
+ }
259
+ for (; i < lines.length; i++) {
260
+ if (lines[i].trim() === "") continue;
261
+ break;
262
+ }
263
+ if (i >= lines.length) return void 0;
264
+ const requestLineIdx = i;
265
+ const { method, url: firstUrl } = parseRequestLine(lines[i]);
266
+ i++;
267
+ let url = firstUrl;
268
+ while (i < lines.length) {
269
+ const raw = lines[i];
270
+ const t = raw.trim();
271
+ if (t !== "" && /^[?&]/.test(t) && /^\s/.test(raw)) {
272
+ url += t;
273
+ i++;
274
+ } else break;
275
+ }
276
+ const headers = [];
277
+ for (; i < lines.length; i++) {
278
+ const line = lines[i];
279
+ if (line.trim() === "") {
280
+ i++;
281
+ break;
282
+ }
283
+ if (isComment(line)) {
284
+ const body2 = commentBody(line);
285
+ const at = /^@(\w+)\s*(.*)$/.exec(body2);
286
+ if (at) {
287
+ const directive2 = at[1].toLowerCase();
288
+ const arg = at[2].trim();
289
+ if (directive2 === "assert" && arg) assertions.push({ expr: arg });
290
+ else if (directive2 === "capture") {
291
+ const eq = arg.indexOf("=");
292
+ if (eq !== -1) captures.push({ name: arg.slice(0, eq).trim(), source: arg.slice(eq + 1).trim() });
293
+ }
294
+ }
295
+ continue;
296
+ }
297
+ const colon = line.indexOf(":");
298
+ if (colon === -1) continue;
299
+ const key = line.slice(0, colon).trim();
300
+ const value = line.slice(colon + 1).trim();
301
+ if (key) headers.push([key, value]);
302
+ }
303
+ const bodyLines = [];
304
+ const directive = /^\s*(?:#+|\/\/)\s*@(name|capture|assert)\b\s*(.*)$/;
305
+ for (const line of lines.slice(i)) {
306
+ const m = directive.exec(line);
307
+ if (m) {
308
+ const kind = m[1].toLowerCase();
309
+ const arg = m[2].trim();
310
+ if (kind === "name") {
311
+ if (arg) name = arg.replace(/^=?\s*/, "").trim();
312
+ } else if (kind === "capture") {
313
+ const eq = arg.indexOf("=");
314
+ if (eq !== -1) {
315
+ const cname = arg.slice(0, eq).trim();
316
+ const source = arg.slice(eq + 1).trim();
317
+ if (cname && source) captures.push({ name: cname, source });
318
+ }
319
+ } else if (kind === "assert" && arg) {
320
+ assertions.push({ expr: arg });
321
+ }
322
+ continue;
323
+ }
324
+ bodyLines.push(line);
325
+ }
326
+ const bodyText = bodyLines.join("\n").replace(/^\n+/, "").replace(/\s+$/, "");
327
+ const body = bodyText.length > 0 ? bodyText : void 0;
328
+ const req = {
329
+ method,
330
+ url,
331
+ headers,
332
+ captures,
333
+ assertions,
334
+ line: startLine + requestLineIdx + 1
335
+ };
336
+ if (name !== void 0) req.name = name;
337
+ if (body !== void 0) req.body = body;
338
+ return req;
339
+ }
340
+
341
+ // src/request.ts
342
+ function parseHeaderStrings(raw) {
343
+ const out = [];
344
+ for (const h of raw) {
345
+ const colon = h.indexOf(":");
346
+ if (colon === -1) continue;
347
+ out.push([h.slice(0, colon).trim(), h.slice(colon + 1).trim()]);
348
+ }
349
+ return out;
350
+ }
351
+ function hasHeader(headers, name) {
352
+ return headers.some(([k]) => k.toLowerCase() === name.toLowerCase());
353
+ }
354
+ function coerceKvValue(raw, rawJson) {
355
+ if (rawJson) {
356
+ try {
357
+ return JSON.parse(raw);
358
+ } catch {
359
+ return raw;
360
+ }
361
+ }
362
+ return raw;
363
+ }
364
+ function base64(input) {
365
+ return Buffer.from(input, "utf8").toString("base64");
366
+ }
367
+ function assembleRequest(url, o = {}) {
368
+ const headers = parseHeaderStrings(o.headers ?? []);
369
+ let finalUrl = url;
370
+ if (o.query && o.query.length) {
371
+ const qs = [];
372
+ for (const q of o.query) {
373
+ const eq = q.indexOf("=");
374
+ const k = eq === -1 ? q : q.slice(0, eq);
375
+ const v = eq === -1 ? "" : q.slice(eq + 1);
376
+ qs.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
377
+ }
378
+ const joined = qs.join("&");
379
+ finalUrl += (url.includes("?") ? "&" : "?") + joined;
380
+ }
381
+ let body;
382
+ let contentType;
383
+ if (o.json !== void 0) {
384
+ body = o.json;
385
+ contentType = "application/json";
386
+ } else if (o.jsonKv && o.jsonKv.length) {
387
+ const obj = {};
388
+ for (const pair of o.jsonKv) {
389
+ const rawJson = pair.includes(":=");
390
+ const sep = rawJson ? ":=" : "=";
391
+ const idx = pair.indexOf(sep);
392
+ if (idx === -1) continue;
393
+ const k = pair.slice(0, idx);
394
+ const v = pair.slice(idx + sep.length);
395
+ obj[k] = coerceKvValue(v, rawJson);
396
+ }
397
+ body = JSON.stringify(obj);
398
+ contentType = "application/json";
399
+ } else if (o.form && o.form.length) {
400
+ const params = new URLSearchParams();
401
+ for (const f of o.form) {
402
+ const eq = f.indexOf("=");
403
+ params.append(eq === -1 ? f : f.slice(0, eq), eq === -1 ? "" : f.slice(eq + 1));
404
+ }
405
+ body = params.toString();
406
+ contentType = "application/x-www-form-urlencoded";
407
+ } else if (o.data !== void 0) {
408
+ body = o.data;
409
+ }
410
+ if (contentType && !hasHeader(headers, "content-type")) {
411
+ headers.push(["Content-Type", contentType]);
412
+ }
413
+ if (o.bearer && !hasHeader(headers, "authorization")) {
414
+ headers.push(["Authorization", `Bearer ${o.bearer}`]);
415
+ } else if (o.user && !hasHeader(headers, "authorization")) {
416
+ headers.push(["Authorization", `Basic ${base64(o.user)}`]);
417
+ }
418
+ const method = (o.method ?? (body !== void 0 ? "POST" : "GET")).toUpperCase();
419
+ const spec = { method, url: finalUrl, headers };
420
+ if (body !== void 0) spec.body = body;
421
+ return spec;
422
+ }
423
+ var REDIRECT_STATUS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
424
+ function headersToObject(h) {
425
+ const out = {};
426
+ if (h && typeof h.forEach === "function") {
427
+ h.forEach((v, k) => {
428
+ out[k.toLowerCase()] = v;
429
+ });
430
+ } else if (h && typeof h === "object") {
431
+ for (const [k, v] of Object.entries(h)) out[k.toLowerCase()] = String(v);
432
+ }
433
+ return out;
434
+ }
435
+ async function readCapped(res, maxSize) {
436
+ const body = res.body;
437
+ if (!body || typeof body.getReader !== "function") {
438
+ const text = await res.text();
439
+ const buf2 = Buffer.from(text, "utf8");
440
+ if (buf2.length > maxSize) {
441
+ return { text: buf2.subarray(0, maxSize).toString("utf8"), size: maxSize, truncated: true };
442
+ }
443
+ return { text, size: buf2.length, truncated: false };
444
+ }
445
+ const reader = body.getReader();
446
+ const chunks = [];
447
+ let size = 0;
448
+ let truncated = false;
449
+ for (; ; ) {
450
+ const { done, value } = await reader.read();
451
+ if (done) break;
452
+ if (value) {
453
+ chunks.push(Buffer.from(value));
454
+ size += value.byteLength;
455
+ if (size > maxSize) {
456
+ truncated = true;
457
+ try {
458
+ await reader.cancel();
459
+ } catch {
460
+ }
461
+ break;
462
+ }
463
+ }
464
+ }
465
+ let buf = Buffer.concat(chunks);
466
+ if (truncated) buf = buf.subarray(0, maxSize);
467
+ return { text: buf.toString("utf8"), size: buf.length, truncated };
468
+ }
469
+ function looksJson(contentType, text) {
470
+ if (contentType && /\bjson\b/i.test(contentType)) return true;
471
+ const t = text.trimStart();
472
+ return t.startsWith("{") || t.startsWith("[");
473
+ }
474
+ function hostOf(url) {
475
+ try {
476
+ return new URL(url).host;
477
+ } catch {
478
+ return "";
479
+ }
480
+ }
481
+ async function sendRequest(spec, opts = {}) {
482
+ const timeoutMs = opts.timeoutMs ?? 3e4;
483
+ const maxSize = opts.maxSize ?? 10 * 1024 * 1024;
484
+ const maxRedirects = opts.maxRedirects ?? 5;
485
+ const follow = opts.followRedirects ?? true;
486
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
487
+ if (typeof doFetch !== "function") {
488
+ throw new Error("global fetch is unavailable \u2014 Node 20+ (or a fetchImpl) is required.");
489
+ }
490
+ const originHost = hostOf(spec.url);
491
+ let currentUrl = spec.url;
492
+ let method = spec.method;
493
+ let body = spec.body;
494
+ const redirectChain = [];
495
+ let crossHost = false;
496
+ const start = Date.now();
497
+ for (let hop = 0; ; hop++) {
498
+ const controller = new AbortController();
499
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
500
+ let res;
501
+ try {
502
+ res = await doFetch(currentUrl, {
503
+ method,
504
+ headers: spec.headers,
505
+ ...body !== void 0 ? { body } : {},
506
+ redirect: "manual",
507
+ signal: controller.signal
508
+ });
509
+ } catch (err) {
510
+ clearTimeout(timer);
511
+ if (err?.name === "AbortError") {
512
+ throw new Error(`Request timed out after ${timeoutMs}ms: ${currentUrl}`);
513
+ }
514
+ throw err;
515
+ }
516
+ clearTimeout(timer);
517
+ const location = res.headers.get?.("location") ?? void 0;
518
+ if (follow && REDIRECT_STATUS.has(res.status) && location && hop < maxRedirects) {
519
+ const nextUrl = new URL(location, currentUrl).toString();
520
+ if (hostOf(nextUrl) !== originHost) crossHost = true;
521
+ redirectChain.push(currentUrl);
522
+ if (res.status === 303 || (res.status === 301 || res.status === 302) && method === "POST") {
523
+ method = "GET";
524
+ body = void 0;
525
+ }
526
+ currentUrl = nextUrl;
527
+ try {
528
+ await res.text();
529
+ } catch {
530
+ }
531
+ continue;
532
+ }
533
+ const headers = headersToObject(res.headers);
534
+ const { text, size, truncated } = await readCapped(res, maxSize);
535
+ const timeMs = Date.now() - start;
536
+ const record = {
537
+ status: res.status,
538
+ statusText: res.statusText || "",
539
+ headers,
540
+ timeMs,
541
+ size,
542
+ body: text,
543
+ url: currentUrl,
544
+ redirected: redirectChain.length > 0,
545
+ redirectChain,
546
+ ok: res.status >= 200 && res.status < 300,
547
+ truncated,
548
+ crossHostRedirect: crossHost
549
+ };
550
+ if (looksJson(headers["content-type"], text)) {
551
+ try {
552
+ record.json = JSON.parse(text);
553
+ } catch {
554
+ }
555
+ }
556
+ return record;
557
+ }
558
+ }
559
+ function shq(s) {
560
+ return `'${s.replace(/'/g, `'\\''`)}'`;
561
+ }
562
+ function toCurl(spec, opts = {}) {
563
+ const parts = ["curl"];
564
+ if (spec.method !== "GET") parts.push("-X", spec.method);
565
+ for (const [k, v] of spec.headers) {
566
+ let value = v;
567
+ if (!opts.showSecrets && k.toLowerCase() === "authorization") {
568
+ const scheme = v.split(/\s+/)[0] ?? "";
569
+ value = /^(Bearer|Basic)$/i.test(scheme) ? `${scheme} ***` : "***";
570
+ }
571
+ parts.push("-H", shq(`${k}: ${value}`));
572
+ }
573
+ if (spec.body !== void 0) parts.push("--data-raw", shq(spec.body));
574
+ if (opts.followRedirects === false) {
575
+ } else {
576
+ parts.push("-L");
577
+ if (opts.maxRedirects !== void 0) parts.push("--max-redirs", String(opts.maxRedirects));
578
+ }
579
+ parts.push(shq(spec.url));
580
+ return parts.join(" ");
581
+ }
582
+
583
+ // src/assert.ts
584
+ var BINARY_OPS = ["==", "!=", "<=", ">=", "<", ">", "contains", "matches"];
585
+ var UNARY_OPS = ["exists", "empty"];
586
+ function parseAssertion(expr) {
587
+ const raw = expr.trim();
588
+ const tokens = raw.split(/\s+/);
589
+ const last = tokens[tokens.length - 1] ?? "";
590
+ if (UNARY_OPS.includes(last) && tokens.length === 2) {
591
+ return { lhs: tokens[0], op: last, raw };
592
+ }
593
+ for (let i = 1; i < tokens.length - 1; i++) {
594
+ const t = tokens[i];
595
+ if (BINARY_OPS.includes(t)) {
596
+ const lhs = tokens.slice(0, i).join(" ");
597
+ const rhs = tokens.slice(i + 1).join(" ");
598
+ return { lhs, op: t, rhs, raw };
599
+ }
600
+ }
601
+ throw new Error(`Cannot parse assertion: "${raw}"`);
602
+ }
603
+ function resolveLhs(lhs, rec) {
604
+ const l = lhs.trim();
605
+ if (l === "status") return rec.status;
606
+ if (l === "time" || l === "duration") return rec.timeMs;
607
+ if (l === "size") return rec.size;
608
+ if (l === "body") return rec.body;
609
+ if (l === "url") return rec.url;
610
+ if (l.startsWith("body.")) {
611
+ const path = l.slice(5);
612
+ const root = rec.json !== void 0 ? rec.json : tryParse(rec.body);
613
+ return evalPath(root, path);
614
+ }
615
+ if (l.startsWith("header.")) {
616
+ return rec.headers[l.slice(7).toLowerCase()];
617
+ }
618
+ return void 0;
619
+ }
620
+ function tryParse(text) {
621
+ try {
622
+ return JSON.parse(text);
623
+ } catch {
624
+ return void 0;
625
+ }
626
+ }
627
+ function parseLiteral(raw) {
628
+ const s = raw.trim();
629
+ if (s.startsWith('"') && s.endsWith('"') && s.length >= 2 || s.startsWith("'") && s.endsWith("'") && s.length >= 2) {
630
+ return s.slice(1, -1);
631
+ }
632
+ if (s === "true") return true;
633
+ if (s === "false") return false;
634
+ if (s === "null") return null;
635
+ if (s !== "" && !Number.isNaN(Number(s))) return Number(s);
636
+ return s;
637
+ }
638
+ function isEmpty(v) {
639
+ if (v === void 0 || v === null) return true;
640
+ if (typeof v === "string") return v.length === 0;
641
+ if (Array.isArray(v)) return v.length === 0;
642
+ if (typeof v === "object") return Object.keys(v).length === 0;
643
+ return false;
644
+ }
645
+ function looseEqual(actual, expected) {
646
+ if (typeof expected === "number") return Number(actual) === expected;
647
+ if (typeof expected === "boolean") {
648
+ if (typeof actual === "boolean") return actual === expected;
649
+ return String(actual) === String(expected);
650
+ }
651
+ if (expected === null) return actual === null || actual === void 0;
652
+ return String(actual) === String(expected);
653
+ }
654
+ function evalAssertion(assertion, rec) {
655
+ const actual = resolveLhs(assertion.lhs, rec);
656
+ const expected = assertion.rhs !== void 0 ? parseLiteral(assertion.rhs) : void 0;
657
+ let ok = false;
658
+ switch (assertion.op) {
659
+ case "==":
660
+ ok = looseEqual(actual, expected);
661
+ break;
662
+ case "!=":
663
+ ok = !looseEqual(actual, expected);
664
+ break;
665
+ case "<":
666
+ ok = Number(actual) < Number(expected);
667
+ break;
668
+ case "<=":
669
+ ok = Number(actual) <= Number(expected);
670
+ break;
671
+ case ">":
672
+ ok = Number(actual) > Number(expected);
673
+ break;
674
+ case ">=":
675
+ ok = Number(actual) >= Number(expected);
676
+ break;
677
+ case "contains": {
678
+ if (Array.isArray(actual)) ok = actual.map((x) => String(x)).includes(String(expected));
679
+ else ok = String(actual).toLowerCase().includes(String(expected).toLowerCase());
680
+ break;
681
+ }
682
+ case "matches": {
683
+ try {
684
+ ok = new RegExp(String(expected)).test(String(actual));
685
+ } catch {
686
+ ok = false;
687
+ }
688
+ break;
689
+ }
690
+ case "exists":
691
+ ok = actual !== void 0 && actual !== null;
692
+ break;
693
+ case "empty":
694
+ ok = isEmpty(actual);
695
+ break;
696
+ }
697
+ const shownActual = typeof actual === "object" ? JSON.stringify(actual) : String(actual);
698
+ const message = assertion.rhs !== void 0 ? `${assertion.lhs} ${assertion.op} ${assertion.rhs} (actual: ${shownActual})` : `${assertion.lhs} ${assertion.op} (actual: ${shownActual})`;
699
+ const result = { ok, assertion, actual, message };
700
+ if (expected !== void 0) result.expected = expected;
701
+ return result;
702
+ }
703
+ function runAssertion(expr, rec) {
704
+ return evalAssertion(parseAssertion(expr), rec);
705
+ }
706
+ function captureValue(source, rec) {
707
+ const v = resolveLhs(source, rec);
708
+ if (v === void 0 || v === null) return void 0;
709
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
710
+ }
711
+
712
+ // src/runner.ts
713
+ function resolveSpec(req, scope) {
714
+ const missing = [];
715
+ const collect = (r) => {
716
+ missing.push(...r.missing);
717
+ return r.text;
718
+ };
719
+ const url = collect(resolveVars(req.url, scope));
720
+ const headers = req.headers.map(([k, v]) => [
721
+ collect(resolveVars(k, scope)),
722
+ collect(resolveVars(v, scope))
723
+ ]);
724
+ const spec = { method: req.method, url, headers };
725
+ if (req.body !== void 0) spec.body = collect(resolveVars(req.body, scope));
726
+ return { spec, missing };
727
+ }
728
+ async function runHttpFile(source, opts = {}) {
729
+ const requests = parseHttpFile(source);
730
+ const scope = makeScope(opts.env, opts.vars);
731
+ const results = [];
732
+ const selected = opts.name ? requests.filter((r) => r.name === opts.name) : requests;
733
+ const sendOpts = {};
734
+ if (opts.timeoutMs !== void 0) sendOpts.timeoutMs = opts.timeoutMs;
735
+ if (opts.maxSize !== void 0) sendOpts.maxSize = opts.maxSize;
736
+ if (opts.maxRedirects !== void 0) sendOpts.maxRedirects = opts.maxRedirects;
737
+ if (opts.followRedirects !== void 0) sendOpts.followRedirects = opts.followRedirects;
738
+ if (opts.fetchImpl !== void 0) sendOpts.fetchImpl = opts.fetchImpl;
739
+ for (const req of selected) {
740
+ const { spec, missing } = resolveSpec(req, scope);
741
+ const result = {
742
+ method: spec.method,
743
+ url: spec.url,
744
+ line: req.line,
745
+ missingVars: missing,
746
+ assertions: [],
747
+ captured: {},
748
+ ok: false
749
+ };
750
+ if (req.name !== void 0) result.name = req.name;
751
+ try {
752
+ const rec = await sendRequest(spec, sendOpts);
753
+ result.response = rec;
754
+ for (const cap of req.captures) {
755
+ const value = captureValue(cap.source, rec);
756
+ if (value !== void 0) {
757
+ result.captured[cap.name] = value;
758
+ scope.set(cap.name, value);
759
+ }
760
+ }
761
+ let allPass = true;
762
+ for (const a of req.assertions) {
763
+ try {
764
+ const ar = evalAssertion(parseAssertion(a.expr), rec);
765
+ result.assertions.push(ar);
766
+ if (!ar.ok) allPass = false;
767
+ } catch (err) {
768
+ result.assertions.push({
769
+ ok: false,
770
+ assertion: { lhs: a.expr, op: "==", raw: a.expr },
771
+ actual: void 0,
772
+ message: `invalid assertion "${a.expr}": ${err.message}`
773
+ });
774
+ allPass = false;
775
+ }
776
+ }
777
+ result.ok = allPass && missing.length === 0;
778
+ } catch (err) {
779
+ result.error = err.message;
780
+ result.ok = false;
781
+ }
782
+ results.push(result);
783
+ opts.onResult?.(result);
784
+ }
785
+ const passed = results.filter((r) => r.ok).length;
786
+ const failed = results.length - passed;
787
+ return { results, passed, failed, ok: failed === 0 && results.length > 0 };
788
+ }
789
+
790
+ // src/format.ts
791
+ var ANSI = {
792
+ reset: "\x1B[0m",
793
+ dim: "\x1B[2m",
794
+ green: "\x1B[32m",
795
+ red: "\x1B[31m",
796
+ yellow: "\x1B[33m",
797
+ cyan: "\x1B[36m",
798
+ magenta: "\x1B[35m",
799
+ blue: "\x1B[34m"
800
+ };
801
+ function humanSize(bytes) {
802
+ if (bytes < 1024) return `${bytes} B`;
803
+ const units = ["KB", "MB", "GB", "TB"];
804
+ let n = bytes / 1024;
805
+ let i = 0;
806
+ while (n >= 1024 && i < units.length - 1) {
807
+ n /= 1024;
808
+ i++;
809
+ }
810
+ return `${n.toFixed(1)} ${units[i]}`;
811
+ }
812
+ function statusColor(status) {
813
+ if (status >= 200 && status < 300) return "green";
814
+ if (status >= 300 && status < 400) return "cyan";
815
+ if (status >= 400 && status < 500) return "yellow";
816
+ return "red";
817
+ }
818
+ function paint(color, s, enabled) {
819
+ return enabled ? `${ANSI[color]}${s}${ANSI.reset}` : s;
820
+ }
821
+ function prettyJson(value, color = false) {
822
+ const plain = JSON.stringify(value, null, 2);
823
+ if (plain === void 0) return String(value);
824
+ if (!color) return plain;
825
+ return plain.replace(
826
+ /("(?:\\.|[^"\\])*"(\s*:)?)|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
827
+ (match, str, colon, lit) => {
828
+ if (str !== void 0) {
829
+ if (colon) return paint("blue", str.slice(0, str.length - colon.length), true) + colon;
830
+ return paint("green", str, true);
831
+ }
832
+ if (lit !== void 0) return paint("yellow", lit, true);
833
+ return paint("cyan", match, true);
834
+ }
835
+ );
836
+ }
837
+ function isJsonContentType(ct) {
838
+ return !!ct && /\bjson\b/i.test(ct);
839
+ }
840
+ export {
841
+ assembleRequest,
842
+ captureValue,
843
+ evalAssertion,
844
+ evalPath,
845
+ humanSize,
846
+ isJsonContentType,
847
+ makeScope,
848
+ parseAssertion,
849
+ parseDotenv,
850
+ parseEnvJson,
851
+ parseHttpFile,
852
+ parseJsonPath,
853
+ parseKvPairs,
854
+ prettyJson,
855
+ resolveLhs,
856
+ resolveVars,
857
+ runAssertion,
858
+ runHttpFile,
859
+ sendRequest,
860
+ statusColor,
861
+ toCurl
862
+ };