hive-am 1.0.0 → 1.2.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/CHANGELOG.md +114 -1
- package/README.md +34 -4
- package/package.json +13 -2
- package/server/dist/api.js +42 -677
- package/server/dist/connections/addressing.js +87 -0
- package/server/dist/connections/channel-skill.js +62 -0
- package/server/dist/connections/fake.js +1 -0
- package/server/dist/connections/files.js +45 -1
- package/server/dist/connections/format.js +9 -1
- package/server/dist/connections/manager.js +60 -3
- package/server/dist/connections/outbound.js +2 -1
- package/server/dist/connections/prompt.js +12 -3
- package/server/dist/connections/rate.js +20 -0
- package/server/dist/connections/router.js +55 -22
- package/server/dist/connections/secrets.js +52 -0
- package/server/dist/connections/store.js +6 -43
- package/server/dist/connections/telegram.js +105 -14
- package/server/dist/cron.js +138 -0
- package/server/dist/db/json.js +16 -0
- package/server/dist/db/migrations.js +199 -0
- package/server/dist/db.js +98 -196
- package/server/dist/errors.js +11 -0
- package/server/dist/git/browse.js +99 -0
- package/server/dist/git/ops.js +136 -19
- package/server/dist/git/repo.js +2 -1
- package/server/dist/history/claude.js +33 -20
- package/server/dist/history/index.js +17 -9
- package/server/dist/history/kiro.js +17 -12
- package/server/dist/history/lines.js +12 -0
- package/server/dist/history/opencode.js +18 -1
- package/server/dist/http/body.js +46 -0
- package/server/dist/http/errors.js +21 -0
- package/server/dist/http/guards.js +44 -0
- package/server/dist/http/router.js +24 -0
- package/server/dist/http/routes/agents.js +103 -0
- package/server/dist/http/routes/channel.js +16 -0
- package/server/dist/http/routes/colonies.js +9 -0
- package/server/dist/http/routes/connections.js +36 -0
- package/server/dist/http/routes/fs.js +22 -0
- package/server/dist/http/routes/git.js +90 -0
- package/server/dist/http/routes/meta.js +11 -0
- package/server/dist/http/routes/notebook.js +34 -0
- package/server/dist/http/routes/orchestration.js +32 -0
- package/server/dist/http/routes/resources.js +68 -0
- package/server/dist/http/routes/schedules.js +40 -0
- package/server/dist/http/routes/skills.js +47 -0
- package/server/dist/http/routes/terminals.js +18 -0
- package/server/dist/http/routes/types.js +36 -0
- package/server/dist/http/validate.js +65 -0
- package/server/dist/index.js +74 -4
- package/server/dist/instructions.js +23 -11
- package/server/dist/objects/agent.js +49 -0
- package/server/dist/objects/database.js +451 -0
- package/server/dist/objects/db-history.js +79 -0
- package/server/dist/objects/docker-info.js +75 -0
- package/server/dist/objects/docker.js +285 -0
- package/server/dist/objects/driver-props.js +196 -0
- package/server/dist/objects/exec.js +16 -0
- package/server/dist/objects/http-history.js +44 -0
- package/server/dist/objects/http-script.js +644 -0
- package/server/dist/objects/http.js +735 -0
- package/server/dist/objects/index.js +445 -0
- package/server/dist/objects/model.js +250 -0
- package/server/dist/objects/runner.js +211 -0
- package/server/dist/objects/stats.js +106 -0
- package/server/dist/objects/store.js +115 -0
- package/server/dist/objects/supervisor.js +111 -0
- package/server/dist/objects/templates.js +67 -0
- package/server/dist/providers/claude.js +5 -13
- package/server/dist/providers/index.js +7 -1
- package/server/dist/providers/kiro.js +4 -5
- package/server/dist/providers/opencode-server.js +164 -0
- package/server/dist/providers/opencode.js +60 -37
- package/server/dist/providers/preamble.js +2 -0
- package/server/dist/providers/spawn.js +31 -3
- package/server/dist/runtime.js +10 -7
- package/server/dist/schedules.js +222 -0
- package/server/dist/seed.js +15 -11
- package/server/dist/services/agents.js +90 -0
- package/server/dist/services/colonies.js +38 -0
- package/server/dist/services/connections.js +76 -0
- package/server/dist/services/notebook.js +22 -0
- package/server/dist/services/providers.js +27 -0
- package/server/dist/services/schedules.js +18 -0
- package/server/dist/sessions-summary.js +51 -0
- package/server/dist/skills/defaults.js +120 -0
- package/server/dist/skills/notebook.js +3 -8
- package/server/dist/terminals.js +177 -0
- package/server/dist/wake.js +149 -0
- package/server/dist/watch.js +201 -0
- package/server/mcp/dispatch.mjs +169 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +145 -85
- package/web/.next/app-path-routes-manifest.json +5 -2
- package/web/.next/build-manifest.json +14 -14
- package/web/.next/prerender-manifest.json +85 -37
- package/web/.next/react-loadable-manifest.json +26 -1
- package/web/.next/required-server-files.json +13 -1
- package/web/.next/routes-manifest.json +32 -0
- package/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +12 -11
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +14 -13
- package/web/.next/server/app/connections/page.js +2 -2
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +14 -13
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +14 -13
- package/web/.next/server/app/page.js +2 -2
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +14 -13
- package/web/.next/server/app/resources/[id]/page.js +2 -0
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources/page.js +2 -0
- package/web/.next/server/app/resources/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources.html +1 -0
- package/web/.next/server/app/resources.meta +15 -0
- package/web/.next/server/app/resources.rsc +25 -0
- package/web/.next/server/app/schedules/page.js +2 -0
- package/web/.next/server/app/schedules/page.js.nft.json +1 -0
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/schedules.html +1 -0
- package/web/.next/server/app/schedules.meta +15 -0
- package/web/.next/server/app/schedules.rsc +25 -0
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +14 -13
- package/web/.next/server/app/skills/page.js +2 -2
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +14 -13
- package/web/.next/server/app/types/page.js +2 -2
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +14 -13
- package/web/.next/server/app-paths-manifest.json +5 -2
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/274.js +1 -0
- package/web/.next/server/chunks/306.js +1 -0
- package/web/.next/server/chunks/423.js +1 -0
- package/web/.next/server/chunks/505.js +6 -0
- package/web/.next/server/chunks/555.js +1 -0
- package/web/.next/server/chunks/612.js +1 -0
- package/web/.next/server/chunks/638.js +1 -0
- package/web/.next/server/chunks/692.js +18 -0
- package/web/.next/server/chunks/716.js +2 -0
- package/web/.next/server/chunks/790.js +11 -0
- package/web/.next/server/chunks/802.js +1 -0
- package/web/.next/server/chunks/890.js +1 -1
- package/web/.next/server/chunks/919.js +1 -0
- package/web/.next/server/chunks/950.js +6 -0
- package/web/.next/server/middleware-build-manifest.js +1 -1
- package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.json +1 -1
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
- package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
- package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
- package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
- package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
- package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
- package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
- package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
- package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
- package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
- package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
- package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
- package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
- package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
- package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
- package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
- package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
- package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
- package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
- package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
- package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
- package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
- package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
- package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
- package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
- package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
- package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
- package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
- package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
- package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
- package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
- package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
- package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
- package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
- package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
- package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
- package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
- package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
- package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
- package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
- package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
- package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
- package/web/.next/static/css/945795da867f5250.css +1 -0
- package/web/.next/static/css/dd4e32c285f75776.css +1 -0
- package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
- package/web/.next/trace +29 -23
- package/web/.next/types/app/resources/[id]/page.ts +84 -0
- package/web/.next/types/app/resources/page.ts +84 -0
- package/web/.next/types/app/schedules/page.ts +84 -0
- package/web/.next/types/routes.d.ts +7 -2
- package/web/.next/types/validator.ts +27 -0
- package/web/next.config.mjs +4 -0
- package/web/.next/server/chunks/149.js +0 -11
- package/web/.next/server/chunks/247.js +0 -1
- package/web/.next/server/chunks/851.js +0 -1
- package/web/.next/server/chunks/932.js +0 -2
- package/web/.next/static/84BpRSahg2BTwk3mX6AG8/_buildManifest.js +0 -1
- package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
- package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
- package/web/.next/static/chunks/351-65da8c1a6e25f93b.js +0 -1
- package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
- package/web/.next/static/chunks/493-92266798be872b8f.js +0 -1
- package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
- package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
- package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
- package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
- package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
- package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
- package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
- package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-5eaf094fec246f1b.js +0 -1
- package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
- package/web/.next/static/chunks/app/connections/page-c6afd90abd82d47a.js +0 -1
- package/web/.next/static/chunks/app/layout-c415d5a847cd94b3.js +0 -1
- package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
- package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
- package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
- package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
- package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
- package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
- package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
- package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
- package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
- package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
- package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
- package/web/.next/static/css/abe1a6216491f944.css +0 -1
- /package/web/.next/static/{84BpRSahg2BTwk3mX6AG8 → p4S3i9kvO359GW91DQiHu}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomInt, randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { runScript } from './http-script.js';
|
|
5
|
+
import { ObjectError } from './model.js';
|
|
6
|
+
/**
|
|
7
|
+
* HTTP requests as an object: a folder of `.http` / `.rest` files in the format of the IntelliJ and VS Code REST clients. hive-am lists the
|
|
8
|
+
* requests of every file, picks an environment from `http-client.env.json` (and the private one next to it) and runs a request with `fetch`,
|
|
9
|
+
* answering with the status, headers, body and time. No JDK and no extra package.
|
|
10
|
+
*
|
|
11
|
+
* Format that is understood: `###` separates requests (the text after it is the name; `# @name x` too); `@name = value` defines a variable;
|
|
12
|
+
* `{{name}}` uses one (and `{{$uuid}}`, `{{$timestamp}}`, `{{$isoTimestamp}}`, `{{$randomInt}}`); the request line is `METHOD URL [HTTP/1.1]`
|
|
13
|
+
* (lines that start with `?` or `&` continue the URL); header lines follow until the first blank line; the rest is the body (`< ./file`
|
|
14
|
+
* inserts a file); `> {% … %}` response handlers are ignored.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_DEPTH = 4, MAX_FILES = 300, MAX_REQUESTS = 2000;
|
|
17
|
+
const MAX_RESPONSE = 2 * 1024 * 1024, TIMEOUT_MS = 60_000, MAX_INCLUDE = 1024 * 1024, MAX_FILE_EDIT = 1024 * 1024;
|
|
18
|
+
const SKIP = new Set(['node_modules', '.git', 'target', 'dist', 'build', '.idea', '.vscode']);
|
|
19
|
+
const METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']);
|
|
20
|
+
const ENV_FILES = ['http-client.env.json', 'http-client.private.env.json'];
|
|
21
|
+
const VAR = /\{\{\s*([$A-Za-z0-9_.\[\]-]+)\s*\}\}/g;
|
|
22
|
+
const REF = /^([\w-]+)\.response\.(body|headers|status)(?:\.(.*))?$/;
|
|
23
|
+
const variablesIn = (...texts) => { const out = new Set(); for (const t of texts)
|
|
24
|
+
for (const m of (t ?? '').matchAll(VAR))
|
|
25
|
+
if (!REF.test(m[1]))
|
|
26
|
+
out.add(m[1]); return [...out]; };
|
|
27
|
+
const refsIn = (...texts) => { const out = new Set(); for (const t of texts)
|
|
28
|
+
for (const m of (t ?? '').matchAll(VAR)) {
|
|
29
|
+
const r = REF.exec(m[1]);
|
|
30
|
+
if (r)
|
|
31
|
+
out.add(r[1]);
|
|
32
|
+
} return [...out]; };
|
|
33
|
+
/** One `.http` file → its requests. */
|
|
34
|
+
export function parseHttpFile(content, relFile) {
|
|
35
|
+
const lines = content.split(/\r?\n/);
|
|
36
|
+
const fileVariables = {};
|
|
37
|
+
const requests = [];
|
|
38
|
+
let name;
|
|
39
|
+
let cur = null;
|
|
40
|
+
const finish = () => {
|
|
41
|
+
if (!cur)
|
|
42
|
+
return;
|
|
43
|
+
const body = [];
|
|
44
|
+
let inHandler = false;
|
|
45
|
+
const scripts = [];
|
|
46
|
+
let current = [];
|
|
47
|
+
let scriptFile;
|
|
48
|
+
for (const l of cur.body) {
|
|
49
|
+
const t = l.trim();
|
|
50
|
+
if (!inHandler && (t.startsWith('> {%') || t.startsWith('>! {%'))) {
|
|
51
|
+
const rest = t.replace(/^>!?\s*\{%/, '');
|
|
52
|
+
if (rest.includes('%}')) {
|
|
53
|
+
scripts.push(rest.slice(0, rest.lastIndexOf('%}')));
|
|
54
|
+
continue;
|
|
55
|
+
} // all in one line
|
|
56
|
+
inHandler = true;
|
|
57
|
+
current = rest.trim() ? [rest] : [];
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (inHandler) {
|
|
61
|
+
if (t.endsWith('%}')) {
|
|
62
|
+
current.push(l.slice(0, l.lastIndexOf('%}')));
|
|
63
|
+
scripts.push(current.join('\n'));
|
|
64
|
+
inHandler = false;
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
current.push(l);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (t.startsWith('<> '))
|
|
71
|
+
continue; // a saved response
|
|
72
|
+
if (t.startsWith('> ')) {
|
|
73
|
+
scriptFile = t.slice(2).trim();
|
|
74
|
+
continue;
|
|
75
|
+
} // a handler file
|
|
76
|
+
body.push(l);
|
|
77
|
+
}
|
|
78
|
+
while (body.length && !body[body.length - 1].trim())
|
|
79
|
+
body.pop();
|
|
80
|
+
while (body.length && !body[0].trim())
|
|
81
|
+
body.shift();
|
|
82
|
+
const text = body.length ? body.join('\n') : undefined;
|
|
83
|
+
const index = requests.length;
|
|
84
|
+
requests.push({ id: `${relFile}#${index}`, index, name: cur.name || `${cur.method} ${cur.url.replace(/^(https?:\/\/[^/]+|\{\{[^}]+\}\})/, '').split('?')[0] || '/'}`, method: cur.method, url: cur.url, headers: cur.headers, body: text, line: cur.line, variables: variablesIn(cur.url, text, ...cur.headers.map((h) => h.value)), refs: refsIn(cur.url, text, ...cur.headers.map((h) => h.value)), named: /^[\w-]+$/.test(cur.name ?? '') ? cur.name : undefined, script: scripts.length ? scripts.join('\n') : undefined, scriptFile });
|
|
85
|
+
cur = null;
|
|
86
|
+
};
|
|
87
|
+
lines.forEach((raw, i) => {
|
|
88
|
+
const line = raw.trimEnd(), t = line.trim();
|
|
89
|
+
if (/^#{3,}/.test(t)) {
|
|
90
|
+
finish();
|
|
91
|
+
name = t.replace(/^#+\s*/, '').trim() || undefined;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (cur?.inBody) {
|
|
95
|
+
cur.body.push(raw);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const tag = /^(?:#|\/\/)\s*@name\s+(\S.*)$/.exec(t);
|
|
99
|
+
if (tag) {
|
|
100
|
+
name = tag[1].trim();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!cur) {
|
|
104
|
+
if (!t || t.startsWith('#') || t.startsWith('//'))
|
|
105
|
+
return;
|
|
106
|
+
const v = /^@([\w.-]+)\s*=\s*(.*)$/.exec(t);
|
|
107
|
+
if (v) {
|
|
108
|
+
fileVariables[v[1]] = v[2].trim();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const m = /^([A-Za-z]+)\s+(\S.*?)(?:\s+HTTP\/[\d.]+)?$/.exec(t);
|
|
112
|
+
if (m && METHODS.has(m[1].toUpperCase())) {
|
|
113
|
+
cur = { method: m[1].toUpperCase(), url: m[2].trim(), headers: [], body: [], line: i + 1, inBody: false, name };
|
|
114
|
+
name = undefined;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (/^https?:\/\//i.test(t)) {
|
|
118
|
+
cur = { method: 'GET', url: t.replace(/\s+HTTP\/[\d.]+$/, ''), headers: [], body: [], line: i + 1, inBody: false, name };
|
|
119
|
+
name = undefined;
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!t) {
|
|
124
|
+
cur.inBody = true;
|
|
125
|
+
return;
|
|
126
|
+
} // the blank line that ends the headers
|
|
127
|
+
if (!cur.headers.length && /^\s+[?&]/.test(line)) {
|
|
128
|
+
cur.url += t;
|
|
129
|
+
return;
|
|
130
|
+
} // an indented line continues the URL
|
|
131
|
+
if (t.startsWith('#') || t.startsWith('//'))
|
|
132
|
+
return;
|
|
133
|
+
const h = /^([^\s:]+)\s*:\s*(.*)$/.exec(t);
|
|
134
|
+
if (h)
|
|
135
|
+
cur.headers.push({ name: h[1], value: h[2] });
|
|
136
|
+
else
|
|
137
|
+
cur.body.push(raw), cur.inBody = true;
|
|
138
|
+
});
|
|
139
|
+
finish();
|
|
140
|
+
return { relFile, requests, fileVariables };
|
|
141
|
+
}
|
|
142
|
+
/** The folder, as a real path (so a link cannot lead outside of it). */
|
|
143
|
+
function realFolder(folder) {
|
|
144
|
+
if (!isAbsolute(folder))
|
|
145
|
+
throw new ObjectError('The folder must be an absolute path');
|
|
146
|
+
if (!existsSync(folder) || !statSync(folder).isDirectory())
|
|
147
|
+
throw new ObjectError(`The folder ${folder} does not exist`);
|
|
148
|
+
return realpathSync(folder);
|
|
149
|
+
}
|
|
150
|
+
/** A path inside the folder, or an error. */
|
|
151
|
+
function inside(root, rel) {
|
|
152
|
+
const abs = resolve(root, rel);
|
|
153
|
+
const r = relative(root, abs);
|
|
154
|
+
if (r.startsWith('..') || isAbsolute(r))
|
|
155
|
+
throw new ObjectError('That path is outside the folder');
|
|
156
|
+
try {
|
|
157
|
+
const real = realpathSync(abs);
|
|
158
|
+
const rr = relative(root, real);
|
|
159
|
+
if (rr.startsWith('..') || isAbsolute(rr))
|
|
160
|
+
throw new ObjectError('That path is outside the folder');
|
|
161
|
+
return real;
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
if (e instanceof ObjectError)
|
|
165
|
+
throw e;
|
|
166
|
+
return abs;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const readJson = (file) => {
|
|
170
|
+
try {
|
|
171
|
+
const j = JSON.parse(readFileSync(file, 'utf8'));
|
|
172
|
+
return j && typeof j === 'object' && !Array.isArray(j) ? j : null;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const SECRET_NAME = /(pass(word|wd)?|secret|token|api[-_]?key|auth|credential|private)/i;
|
|
179
|
+
export const looksSecret = (name) => SECRET_NAME.test(name);
|
|
180
|
+
function envVars(root, fileAbs, env, hive) {
|
|
181
|
+
const dirs = [];
|
|
182
|
+
for (let d = dirname(fileAbs);; d = dirname(d)) {
|
|
183
|
+
dirs.push(d);
|
|
184
|
+
if (d === root || !d.startsWith(root))
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
const merged = {};
|
|
188
|
+
const priv = {};
|
|
189
|
+
const files = [];
|
|
190
|
+
for (const d of dirs.reverse())
|
|
191
|
+
for (const f of ENV_FILES) {
|
|
192
|
+
const p = join(d, f);
|
|
193
|
+
if (!existsSync(p))
|
|
194
|
+
continue;
|
|
195
|
+
const j = readJson(p);
|
|
196
|
+
if (!j)
|
|
197
|
+
continue;
|
|
198
|
+
files.push(relative(root, p));
|
|
199
|
+
for (const [k, v] of Object.entries(j))
|
|
200
|
+
if (v && typeof v === 'object') {
|
|
201
|
+
merged[k] = { ...(merged[k] ?? {}), ...v };
|
|
202
|
+
if (f.includes('private')) {
|
|
203
|
+
const s = (priv[k] ??= new Set());
|
|
204
|
+
for (const n of Object.keys(v))
|
|
205
|
+
s.add(n);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const names = [...new Set([...Object.keys(merged), ...Object.keys(hive ?? {})])].filter((k) => k !== '$shared').sort();
|
|
210
|
+
// The levels, from the weakest to the strongest: hive-am's shared values, hive-am's values of the environment, then the same two from the env files.
|
|
211
|
+
const layers = [];
|
|
212
|
+
const fromHive = (scope) => ({ source: 'hive', from: scope, vars: Object.fromEntries(Object.entries(hive?.[scope] ?? {}).map(([k, v]) => [k, { value: v.value, secret: !!v.secret, hidden: !!v.secret }])) });
|
|
213
|
+
const fromFiles = (scope) => ({ source: 'envfile', from: scope, vars: Object.fromEntries(Object.entries(merged[scope] ?? {}).map(([k, v]) => [k, { value: typeof v === 'string' ? v : JSON.stringify(v), secret: !!priv[scope]?.has(k), hidden: false }])) });
|
|
214
|
+
layers.push(fromHive('$shared'));
|
|
215
|
+
if (env)
|
|
216
|
+
layers.push(fromHive(env));
|
|
217
|
+
layers.push(fromFiles('$shared'));
|
|
218
|
+
if (env)
|
|
219
|
+
layers.push(fromFiles(env));
|
|
220
|
+
const vars = {};
|
|
221
|
+
for (const l of layers)
|
|
222
|
+
for (const [k, v] of Object.entries(l.vars))
|
|
223
|
+
vars[k] = v.value;
|
|
224
|
+
return { vars, names, files, layers };
|
|
225
|
+
}
|
|
226
|
+
export function scanHttp(folder, hive) {
|
|
227
|
+
const root = realFolder(folder);
|
|
228
|
+
const files = [];
|
|
229
|
+
const envNames = new Set(Object.keys(hive ?? {}).filter((k) => k !== '$shared'));
|
|
230
|
+
const envFiles = new Set();
|
|
231
|
+
const fileVars = {};
|
|
232
|
+
let truncated = false, total = 0;
|
|
233
|
+
const walk = (dir, depth) => {
|
|
234
|
+
let entries;
|
|
235
|
+
try {
|
|
236
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
242
|
+
for (const e of entries) {
|
|
243
|
+
const abs = join(dir, e.name);
|
|
244
|
+
if (e.isDirectory()) {
|
|
245
|
+
if (depth < MAX_DEPTH && !SKIP.has(e.name) && !e.name.startsWith('.'))
|
|
246
|
+
walk(abs, depth + 1);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ENV_FILES.includes(e.name)) {
|
|
250
|
+
envFiles.add(relative(root, abs));
|
|
251
|
+
for (const [k, vars] of Object.entries(readJson(abs) ?? {})) {
|
|
252
|
+
if (k !== '$shared')
|
|
253
|
+
envNames.add(k);
|
|
254
|
+
const set = (fileVars[k] ??= new Set());
|
|
255
|
+
for (const name of Object.keys(vars ?? {}))
|
|
256
|
+
set.add(name);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!/\.(http|rest)$/i.test(e.name))
|
|
261
|
+
continue;
|
|
262
|
+
if (files.length >= MAX_FILES || total >= MAX_REQUESTS) {
|
|
263
|
+
truncated = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const f = parseHttpFile(readFileSync(abs, 'utf8'), relative(root, abs));
|
|
268
|
+
if (f.requests.length) {
|
|
269
|
+
files.push(f);
|
|
270
|
+
total += f.requests.length;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch { /* unreadable file */ }
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
walk(root, 0);
|
|
277
|
+
const count = new Map();
|
|
278
|
+
for (const f of files)
|
|
279
|
+
for (const r of f.requests)
|
|
280
|
+
for (const v of r.variables)
|
|
281
|
+
if (!v.startsWith('$'))
|
|
282
|
+
count.set(v, (count.get(v) ?? 0) + 1);
|
|
283
|
+
const fileDefined = [...new Set(files.flatMap((f) => Object.keys(f.fileVariables)))].sort();
|
|
284
|
+
// The variables that response handlers keep, found by reading the handlers (not by running them).
|
|
285
|
+
const setBy = {};
|
|
286
|
+
for (const f of files)
|
|
287
|
+
for (const r of f.requests)
|
|
288
|
+
for (const m of (r.script ?? '').matchAll(/client\.global\.set\(\s*["'`]([\w.-]+)["'`]/g))
|
|
289
|
+
(setBy[m[1]] ??= []).push(r.named ?? r.name);
|
|
290
|
+
return { folder: root, setBy, files, environments: [...envNames].sort(), envFiles: [...envFiles].sort(), used: [...count].map(([name, n]) => ({ name, count: n })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)), fileDefined, fileVars: Object.fromEntries(Object.entries(fileVars).map(([k, v]) => [k, [...v].sort()])), truncated };
|
|
291
|
+
}
|
|
292
|
+
const MASK = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|api-key|x-auth-token|x-access-token)$/i;
|
|
293
|
+
const mask = (name, value) => (MASK.test(name) ? `${value.slice(0, Math.min(6, Math.floor(value.length / 3)))}••••` : value);
|
|
294
|
+
/** `Authorization: Basic user password` (or `user:password`) as the encoded header; an already encoded one is left alone. */
|
|
295
|
+
const basicAuth = (name, value) => {
|
|
296
|
+
if (name.toLowerCase() !== 'authorization')
|
|
297
|
+
return value;
|
|
298
|
+
const m = /^Basic\s+(\S+?)(?:\s+|:)(.+)$/i.exec(value.trim());
|
|
299
|
+
return m ? `Basic ${Buffer.from(`${m[1]}:${m[2]}`).toString('base64')}` : value;
|
|
300
|
+
};
|
|
301
|
+
const dynamic = (key) => {
|
|
302
|
+
if (key === '$uuid' || key === '$guid')
|
|
303
|
+
return randomUUID();
|
|
304
|
+
if (key === '$timestamp')
|
|
305
|
+
return String(Math.floor(Date.now() / 1000));
|
|
306
|
+
if (key === '$isoTimestamp')
|
|
307
|
+
return new Date().toISOString();
|
|
308
|
+
if (key === '$randomInt')
|
|
309
|
+
return String(randomInt(0, 1000));
|
|
310
|
+
return undefined;
|
|
311
|
+
};
|
|
312
|
+
const sessions = new Map();
|
|
313
|
+
export const sessionOf = (objectId) => { let s = sessions.get(objectId); if (!s)
|
|
314
|
+
sessions.set(objectId, s = { globals: {}, responses: new Map() }); return s; };
|
|
315
|
+
export const dropSession = (objectId) => { sessions.delete(objectId); };
|
|
316
|
+
export function sessionView(objectId) {
|
|
317
|
+
const s = sessionOf(objectId);
|
|
318
|
+
return { globals: { ...s.globals }, responses: [...s.responses].map(([name, r]) => ({ name, status: r.status, at: r.at, headers: r.headers, body: r.text.slice(0, 200_000) })) };
|
|
319
|
+
}
|
|
320
|
+
export function sessionSet(objectId, name, value) {
|
|
321
|
+
if (!/^[A-Za-z_][\w.-]{0,80}$/.test(name))
|
|
322
|
+
throw new ObjectError(`"${name}" is not a valid variable name`);
|
|
323
|
+
if (value.length > 8000)
|
|
324
|
+
throw new ObjectError('The value is too long');
|
|
325
|
+
sessionOf(objectId).globals[name] = value;
|
|
326
|
+
}
|
|
327
|
+
/** Forgets one value, or (without a name) every value and every stored answer. */
|
|
328
|
+
export function sessionClear(objectId, name) {
|
|
329
|
+
const s = sessionOf(objectId);
|
|
330
|
+
if (name)
|
|
331
|
+
delete s.globals[name];
|
|
332
|
+
else {
|
|
333
|
+
s.globals = {};
|
|
334
|
+
s.responses.clear();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** `{{login.response.body.$.token}}`, `{{login.response.headers.X-Token}}`, `{{login.response.status}}`: a piece of the last answer of a named request. */
|
|
338
|
+
function fromResponse(s, key) {
|
|
339
|
+
const m = REF.exec(key);
|
|
340
|
+
if (!m)
|
|
341
|
+
return undefined;
|
|
342
|
+
const r = s?.responses.get(m[1]);
|
|
343
|
+
if (!r)
|
|
344
|
+
return undefined;
|
|
345
|
+
if (m[2] === 'status')
|
|
346
|
+
return String(r.status);
|
|
347
|
+
if (m[2] === 'headers')
|
|
348
|
+
return r.headers[(m[3] ?? '').toLowerCase()];
|
|
349
|
+
const path = (m[3] ?? '*').replace(/^\$\.?/, '');
|
|
350
|
+
if (path === '*' || path === '')
|
|
351
|
+
return r.text;
|
|
352
|
+
let cur = r.body;
|
|
353
|
+
for (const part of path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean)) {
|
|
354
|
+
if (cur === null || typeof cur !== 'object' || ['__proto__', 'constructor', 'prototype'].includes(part))
|
|
355
|
+
return undefined;
|
|
356
|
+
cur = cur[part];
|
|
357
|
+
}
|
|
358
|
+
return cur === undefined ? undefined : typeof cur === 'string' ? cur : JSON.stringify(cur);
|
|
359
|
+
}
|
|
360
|
+
const substitute = (text, vars, unresolved, session) => {
|
|
361
|
+
let out = text;
|
|
362
|
+
for (let pass = 0; pass < 5 && VAR.test(out); pass++) {
|
|
363
|
+
VAR.lastIndex = 0;
|
|
364
|
+
out = out.replace(VAR, (whole, k) => dynamic(k) ?? vars[k] ?? fromResponse(session, k) ?? whole);
|
|
365
|
+
}
|
|
366
|
+
VAR.lastIndex = 0;
|
|
367
|
+
for (const m of out.matchAll(VAR))
|
|
368
|
+
unresolved.add(m[1]);
|
|
369
|
+
return out;
|
|
370
|
+
};
|
|
371
|
+
/** The variables a request needs and where each one stands, so the interface can show what is missing before running it. */
|
|
372
|
+
export function describeHttp(folder, relFile, index, env, hive, session) {
|
|
373
|
+
const root = realFolder(folder);
|
|
374
|
+
const abs = inside(root, relFile);
|
|
375
|
+
const file = parseHttpFile(readFileSync(abs, 'utf8'), relFile);
|
|
376
|
+
const item = file.requests[index];
|
|
377
|
+
if (!item)
|
|
378
|
+
throw new ObjectError(`Request #${index} is not in ${relFile}`);
|
|
379
|
+
const e = envVars(root, abs, env, hive);
|
|
380
|
+
const vars = { ...file.fileVariables, ...e.vars, ...(session?.globals ?? {}) };
|
|
381
|
+
// Answers of other requests this one uses: a reference to a request that has not been run in this session is missing too.
|
|
382
|
+
const refMissing = [...(item.url + (item.body ?? '') + item.headers.map((x) => x.value).join('\n')).matchAll(VAR)].map((m) => m[1]).filter((k) => REF.test(k) && fromResponse(session, k) === undefined);
|
|
383
|
+
const missing = [...item.variables.filter((v) => dynamic(v) === undefined && !(v in vars)), ...new Set(refMissing)];
|
|
384
|
+
return { item, missing, needsRun: [...new Set(refMissing.map((k) => REF.exec(k)[1]))], environments: e.names };
|
|
385
|
+
}
|
|
386
|
+
export function inspectHttp(folder, relFile, index, env, hive, session) {
|
|
387
|
+
const root = realFolder(folder);
|
|
388
|
+
const abs = relFile ? inside(root, relFile) : join(root, '_.http');
|
|
389
|
+
const files = [];
|
|
390
|
+
if (relFile)
|
|
391
|
+
files.push(parseHttpFile(readFileSync(abs, 'utf8'), relFile));
|
|
392
|
+
else
|
|
393
|
+
for (const f of scanHttp(folder, hive).files)
|
|
394
|
+
files.push(f);
|
|
395
|
+
const item = relFile && index !== undefined ? files[0].requests[index] : undefined;
|
|
396
|
+
if (relFile && index !== undefined && !item)
|
|
397
|
+
throw new ObjectError(`Request #${index} is not in ${relFile}`);
|
|
398
|
+
const e = envVars(root, abs, env, hive);
|
|
399
|
+
// The names to show, and how many requests use each.
|
|
400
|
+
const used = new Map();
|
|
401
|
+
const bump = (n) => used.set(n, (used.get(n) ?? 0) + 1);
|
|
402
|
+
const reqs = item ? [item] : files.flatMap((f) => f.requests);
|
|
403
|
+
for (const r of reqs) {
|
|
404
|
+
for (const v of r.variables)
|
|
405
|
+
bump(v);
|
|
406
|
+
for (const m of (r.url + (r.body ?? '') + r.headers.map((x) => x.value).join('\n')).matchAll(VAR))
|
|
407
|
+
if (REF.test(m[1]))
|
|
408
|
+
bump(m[1]);
|
|
409
|
+
}
|
|
410
|
+
const names = new Set(used.keys());
|
|
411
|
+
if (!item) {
|
|
412
|
+
for (const f of files)
|
|
413
|
+
for (const k of Object.keys(f.fileVariables))
|
|
414
|
+
names.add(k);
|
|
415
|
+
for (const l of e.layers)
|
|
416
|
+
for (const k of Object.keys(l.vars))
|
|
417
|
+
names.add(k);
|
|
418
|
+
for (const k of Object.keys(session?.globals ?? {}))
|
|
419
|
+
names.add(k);
|
|
420
|
+
}
|
|
421
|
+
const infoOf = (name) => {
|
|
422
|
+
const usedBy = used.get(name) ?? 0;
|
|
423
|
+
if (REF.test(name)) {
|
|
424
|
+
const v = fromResponse(session, name), ref = REF.exec(name);
|
|
425
|
+
return v === undefined
|
|
426
|
+
? { name, value: null, source: 'missing', from: ref[1], secret: false, hidden: false, overridden: [], usedBy, hint: `run:${ref[1]}` }
|
|
427
|
+
: { name, value: v, source: 'response', from: ref[1], secret: looksSecret(name), hidden: false, overridden: [], usedBy };
|
|
428
|
+
}
|
|
429
|
+
if (dynamic(name) !== undefined)
|
|
430
|
+
return { name, value: null, source: 'dynamic', from: name, secret: false, hidden: false, overridden: [], usedBy };
|
|
431
|
+
// every source that defines it, strongest first
|
|
432
|
+
const found = [];
|
|
433
|
+
if (session && name in session.globals)
|
|
434
|
+
found.push({ source: 'session', from: '', value: session.globals[name], secret: looksSecret(name), hidden: false });
|
|
435
|
+
for (const l of [...e.layers].reverse())
|
|
436
|
+
if (name in l.vars)
|
|
437
|
+
found.push({ source: l.source, from: l.from === '$shared' ? '$shared' : l.from, value: l.vars[name].value, secret: l.vars[name].secret || looksSecret(name), hidden: l.vars[name].hidden });
|
|
438
|
+
for (const f of files)
|
|
439
|
+
if (name in f.fileVariables)
|
|
440
|
+
found.push({ source: 'file', from: f.relFile, value: f.fileVariables[name], secret: looksSecret(name), hidden: false });
|
|
441
|
+
if (!found.length)
|
|
442
|
+
return { name, value: null, source: 'missing', from: '', secret: looksSecret(name), hidden: false, overridden: [], usedBy };
|
|
443
|
+
const [w, ...rest] = found;
|
|
444
|
+
return { name, value: w.hidden ? null : w.value, source: w.source, from: w.from, secret: w.secret, hidden: w.hidden, overridden: rest.map((x) => ({ source: x.source, from: x.from, value: x.hidden || x.secret ? null : x.value })), usedBy };
|
|
445
|
+
};
|
|
446
|
+
const vars = [...names].map(infoOf).sort((a, b) => (a.source === 'missing' ? 0 : 1) - (b.source === 'missing' ? 0 : 1) || a.name.localeCompare(b.name));
|
|
447
|
+
let preview;
|
|
448
|
+
if (item) {
|
|
449
|
+
const unresolved = new Set();
|
|
450
|
+
const byName = new Map(vars.map((v) => [v.name, v]));
|
|
451
|
+
const fill = (text) => text.replace(VAR, (whole, k) => { const v = byName.get(k); if (!v)
|
|
452
|
+
return whole; if (v.source === 'missing') {
|
|
453
|
+
unresolved.add(k);
|
|
454
|
+
return whole;
|
|
455
|
+
} if (v.source === 'dynamic')
|
|
456
|
+
return whole; return v.hidden ? '••••' : v.value ?? whole; });
|
|
457
|
+
preview = { method: item.method, url: fill(item.url), headers: item.headers.map((x) => ({ name: x.name, value: mask(x.name, fill(x.value)) })), body: item.body === undefined ? undefined : fill(item.body), unresolved: [...unresolved] };
|
|
458
|
+
}
|
|
459
|
+
return { env: env ?? null, environments: e.names, vars, preview };
|
|
460
|
+
}
|
|
461
|
+
/** The text of a .http file (to show and edit it). */
|
|
462
|
+
export function readHttpFile(folder, relFile) {
|
|
463
|
+
const root = realFolder(folder);
|
|
464
|
+
const abs = inside(root, relFile);
|
|
465
|
+
if (!/\.(http|rest)$/i.test(abs))
|
|
466
|
+
throw new ObjectError('That is not a .http file');
|
|
467
|
+
const st = statSync(abs);
|
|
468
|
+
if (!st.isFile())
|
|
469
|
+
throw new ObjectError('That is not a file');
|
|
470
|
+
if (st.size > MAX_FILE_EDIT)
|
|
471
|
+
throw new ObjectError('The file is too large to open here');
|
|
472
|
+
return readFileSync(abs, 'utf8');
|
|
473
|
+
}
|
|
474
|
+
/** Creates a new .http / .rest file inside the folder (it never overwrites one). */
|
|
475
|
+
export function createHttpFile(folder, relFile, content) {
|
|
476
|
+
const root = realFolder(folder);
|
|
477
|
+
if (!/^[\w .@()+/-]+\.(http|rest)$/i.test(relFile) || relFile.split('/').some((p) => p.startsWith('.') || !p))
|
|
478
|
+
throw new ObjectError('Use a name that ends in .http or .rest, with letters, numbers, spaces, - _ . and folders inside the project');
|
|
479
|
+
const abs = resolve(root, relFile);
|
|
480
|
+
const r = relative(root, abs);
|
|
481
|
+
if (r.startsWith('..') || isAbsolute(r))
|
|
482
|
+
throw new ObjectError('That path is outside the folder');
|
|
483
|
+
if (existsSync(abs))
|
|
484
|
+
throw new ObjectError('A file with that name already exists');
|
|
485
|
+
if (Buffer.byteLength(content) > MAX_FILE_EDIT)
|
|
486
|
+
throw new ObjectError('The file is too large');
|
|
487
|
+
// the deepest folder that exists must be inside the project once links are resolved (a link could lead out)
|
|
488
|
+
let anc = dirname(abs);
|
|
489
|
+
while (!existsSync(anc))
|
|
490
|
+
anc = dirname(anc);
|
|
491
|
+
const real = relative(root, realpathSync(anc));
|
|
492
|
+
if (real.startsWith('..') || isAbsolute(real))
|
|
493
|
+
throw new ObjectError('That path is outside the folder');
|
|
494
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
495
|
+
writeFileSync(abs, content, { encoding: 'utf8', flag: 'wx' });
|
|
496
|
+
return r;
|
|
497
|
+
}
|
|
498
|
+
export function writeHttpFile(folder, relFile, content) {
|
|
499
|
+
const root = realFolder(folder);
|
|
500
|
+
const abs = inside(root, relFile);
|
|
501
|
+
if (!/\.(http|rest)$/i.test(abs))
|
|
502
|
+
throw new ObjectError('Only .http and .rest files can be saved here');
|
|
503
|
+
if (!existsSync(abs) || !statSync(abs).isFile())
|
|
504
|
+
throw new ObjectError('The file does not exist: this saves changes to a file, it does not create one');
|
|
505
|
+
if (Buffer.byteLength(content) > MAX_FILE_EDIT)
|
|
506
|
+
throw new ObjectError('The file is too large');
|
|
507
|
+
writeFileSync(abs, content, 'utf8');
|
|
508
|
+
}
|
|
509
|
+
export const EXPORT_FORMATS = ['curl', 'wget', 'httpie', 'powershell', 'fetch', 'axios', 'python', 'go', 'php', 'java', 'csharp', 'ruby', 'rust', 'http'];
|
|
510
|
+
const sq = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
|
|
511
|
+
const js = (v) => JSON.stringify(v);
|
|
512
|
+
/**
|
|
513
|
+
* A request as text to paste somewhere else (cURL, fetch, Python requests, HTTPie, or the raw HTTP message), with the values the runner
|
|
514
|
+
* would use now. What has no value stays as {{name}}; credential headers can be masked.
|
|
515
|
+
*/
|
|
516
|
+
export function exportHttp(folder, relFile, index, format, env, hive, session, redact = false) {
|
|
517
|
+
if (!EXPORT_FORMATS.includes(format))
|
|
518
|
+
throw new ObjectError(`Unknown format "${format}"`);
|
|
519
|
+
const root = realFolder(folder);
|
|
520
|
+
const abs = inside(root, relFile);
|
|
521
|
+
if (!/\.(http|rest)$/i.test(abs))
|
|
522
|
+
throw new ObjectError('That is not a .http file');
|
|
523
|
+
const file = parseHttpFile(readFileSync(abs, 'utf8'), relFile);
|
|
524
|
+
const item = file.requests[index];
|
|
525
|
+
if (!item)
|
|
526
|
+
throw new ObjectError(`Request #${index} is not in ${relFile}`);
|
|
527
|
+
const e = envVars(root, abs, env, hive);
|
|
528
|
+
const vars = { ...file.fileVariables, ...e.vars, ...(session?.globals ?? {}) };
|
|
529
|
+
const unresolved = new Set();
|
|
530
|
+
const sess = session ?? { globals: {}, responses: new Map() };
|
|
531
|
+
const url = substitute(item.url, vars, unresolved, sess);
|
|
532
|
+
const headers = item.headers.map((h) => { const v = basicAuth(h.name, substitute(h.value, vars, unresolved, sess)); return { name: h.name, value: redact ? mask(h.name, v) : v }; });
|
|
533
|
+
let body = item.body === undefined ? undefined : substitute(item.body, vars, unresolved, sess);
|
|
534
|
+
const inc = body && /^<\s+(\S+)\s*$/.exec(body.trim());
|
|
535
|
+
if (inc) {
|
|
536
|
+
try {
|
|
537
|
+
const pth = inside(root, join(relative(root, dirname(abs)), inc[1]));
|
|
538
|
+
if (statSync(pth).size <= MAX_INCLUDE)
|
|
539
|
+
body = readFileSync(pth, 'utf8');
|
|
540
|
+
}
|
|
541
|
+
catch { /* left as written */ }
|
|
542
|
+
}
|
|
543
|
+
const hasBody = body !== undefined && body !== '' && !['GET', 'HEAD'].includes(item.method);
|
|
544
|
+
const m = item.method;
|
|
545
|
+
let text;
|
|
546
|
+
if (format === 'curl') {
|
|
547
|
+
text = `curl ${m === 'GET' ? '' : `-X ${m} `}${sq(url)}` + headers.map((h) => ` \\\n -H ${sq(`${h.name}: ${h.value}`)}`).join('') + (hasBody ? ` \\\n --data-raw ${sq(body)}` : '');
|
|
548
|
+
}
|
|
549
|
+
else if (format === 'fetch') {
|
|
550
|
+
const opts = [` method: ${js(m)}`, ...(headers.length ? [` headers: {\n${headers.map((h) => ` ${js(h.name)}: ${js(h.value)}`).join(',\n')}\n }`] : []), ...(hasBody ? [` body: ${js(body)}`] : [])];
|
|
551
|
+
text = `const res = await fetch(${js(url)}, {\n${opts.join(',\n')}\n});\nconsole.log(res.status, await res.text());`;
|
|
552
|
+
}
|
|
553
|
+
else if (format === 'python') {
|
|
554
|
+
text = `import requests\n\nres = requests.request(\n ${js(m)},\n ${js(url)},\n` + (headers.length ? ` headers={\n${headers.map((h) => ` ${js(h.name)}: ${js(h.value)},`).join('\n')}\n },\n` : '') + (hasBody ? ` data=${js(body)}.encode(),\n` : '') + `)\nprint(res.status_code, res.text)`;
|
|
555
|
+
}
|
|
556
|
+
else if (format === 'wget') {
|
|
557
|
+
text = `wget --quiet --method ${m} ${sq(url)}` + headers.map((h) => ` \\\n --header ${sq(`${h.name}: ${h.value}`)}`).join('') + (hasBody ? ` \\\n --body-data ${sq(body)}` : '') + ' \\\n -O -';
|
|
558
|
+
}
|
|
559
|
+
else if (format === 'powershell') {
|
|
560
|
+
const ps = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
561
|
+
text = (headers.length ? `$headers = @{\n${headers.map((h) => ` ${ps(h.name)} = ${ps(h.value)}`).join('\n')}\n}\n` : '') + `Invoke-RestMethod -Method ${m} -Uri ${ps(url)}` + (headers.length ? ' -Headers $headers' : '') + (hasBody ? ` -Body ${ps(body)}` : '');
|
|
562
|
+
}
|
|
563
|
+
else if (format === 'axios') {
|
|
564
|
+
const opts = [` method: ${js(m.toLowerCase())}`, ` url: ${js(url)}`, ...(headers.length ? [` headers: {\n${headers.map((h) => ` ${js(h.name)}: ${js(h.value)}`).join(',\n')}\n }`] : []), ...(hasBody ? [` data: ${js(body)}`] : [])];
|
|
565
|
+
text = `import axios from 'axios';\n\nconst res = await axios.request({\n${opts.join(',\n')}\n});\nconsole.log(res.status, res.data);`;
|
|
566
|
+
}
|
|
567
|
+
else if (format === 'go') {
|
|
568
|
+
const gs = (v) => '`' + v.replace(/`/g, '` + "`" + `') + '`';
|
|
569
|
+
text = `package main\n\nimport (\n\t"fmt"\n\t"io"\n\t"net/http"\n${hasBody ? '\t"strings"\n' : ''})\n\nfunc main() {\n\treq, _ := http.NewRequest(${js(m)}, ${js(url)}, ${hasBody ? `strings.NewReader(${gs(body)})` : 'nil'})\n` + headers.map((h) => `\treq.Header.Set(${js(h.name)}, ${js(h.value)})\n`).join('') + `\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer res.Body.Close()\n\tout, _ := io.ReadAll(res.Body)\n\tfmt.Println(res.StatusCode, string(out))\n}`;
|
|
570
|
+
}
|
|
571
|
+
else if (format === 'php') {
|
|
572
|
+
const ph = (v) => `'${v.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
573
|
+
text = `<?php\n$ch = curl_init(${ph(url)});\ncurl_setopt_array($ch, [\n CURLOPT_CUSTOMREQUEST => ${ph(m)},\n CURLOPT_RETURNTRANSFER => true,\n` + (headers.length ? ` CURLOPT_HTTPHEADER => [\n${headers.map((h) => ` ${ph(`${h.name}: ${h.value}`)},`).join('\n')}\n ],\n` : '') + (hasBody ? ` CURLOPT_POSTFIELDS => ${ph(body)},\n` : '') + `]);\n$out = curl_exec($ch);\necho curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $out;`;
|
|
574
|
+
}
|
|
575
|
+
else if (format === 'java') {
|
|
576
|
+
text = `import java.net.URI;\nimport java.net.http.*;\n\nvar request = HttpRequest.newBuilder(URI.create(${js(url)}))\n` + headers.filter((h) => !/^(host|content-length|connection)$/i.test(h.name)).map((h) => ` .header(${js(h.name)}, ${js(h.value)})\n`).join('') + ` .method(${js(m)}, ${hasBody ? `HttpRequest.BodyPublishers.ofString(${js(body)})` : 'HttpRequest.BodyPublishers.noBody()'})\n .build();\nvar response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());\nSystem.out.println(response.statusCode() + " " + response.body());`;
|
|
577
|
+
}
|
|
578
|
+
else if (format === 'csharp') {
|
|
579
|
+
const cs = (v) => '@"' + v.replace(/"/g, '""') + '"';
|
|
580
|
+
const ct = headers.find((h) => h.name.toLowerCase() === 'content-type')?.value.split(';')[0].trim();
|
|
581
|
+
text = `using var client = new HttpClient();\nvar request = new HttpRequestMessage(new HttpMethod(${js(m)}), ${cs(url)});\n` + headers.filter((h) => h.name.toLowerCase() !== 'content-type').map((h) => `request.Headers.TryAddWithoutValidation(${js(h.name)}, ${cs(h.value)});\n`).join('') + (hasBody ? `request.Content = new StringContent(${cs(body)}, System.Text.Encoding.UTF8${ct ? `, ${js(ct)}` : ''});\n` : '') + `var response = await client.SendAsync(request);\nConsole.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");`;
|
|
582
|
+
}
|
|
583
|
+
else if (format === 'ruby') {
|
|
584
|
+
const rb = (v) => `'${v.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
585
|
+
text = `require 'net/http'\nrequire 'uri'\n\nuri = URI(${rb(url)})\nrequest = Net::HTTP::${m[0] + m.slice(1).toLowerCase()}.new(uri)\n` + headers.map((h) => `request[${rb(h.name)}] = ${rb(h.value)}\n`).join('') + (hasBody ? `request.body = ${rb(body)}\n` : '') + `response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(request) }\nputs "#{response.code} #{response.body}"`;
|
|
586
|
+
}
|
|
587
|
+
else if (format === 'rust') {
|
|
588
|
+
text = `let client = reqwest::Client::new();\nlet res = client\n .request(reqwest::Method::from_bytes(b${js(m)}).unwrap(), ${js(url)})\n` + headers.map((h) => ` .header(${js(h.name)}, ${js(h.value)})\n`).join('') + (hasBody ? ` .body(${js(body)})\n` : '') + ` .send()\n .await?;\nprintln!("{} {}", res.status(), res.text().await?);`;
|
|
589
|
+
}
|
|
590
|
+
else if (format === 'httpie') {
|
|
591
|
+
text = `http${hasBody ? ` --raw ${sq(body)}` : ''} ${m} ${sq(url)}` + headers.map((h) => ` ${sq(`${h.name}:${h.value}`)}`).join('');
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
let host = '';
|
|
595
|
+
try {
|
|
596
|
+
host = new URL(url).host;
|
|
597
|
+
}
|
|
598
|
+
catch { /* not a full URL */ }
|
|
599
|
+
const hs = headers.some((h) => h.name.toLowerCase() === 'host') || !host ? headers : [{ name: 'Host', value: host }, ...headers];
|
|
600
|
+
text = `${m} ${url} HTTP/1.1\n${hs.map((h) => `${h.name}: ${h.value}`).join('\n')}${hasBody ? `\n\n${body}` : ''}`;
|
|
601
|
+
}
|
|
602
|
+
return { text, unresolved: [...unresolved] };
|
|
603
|
+
}
|
|
604
|
+
/** The requests of a file (or of all the files) as documentation (Markdown) or as a Postman collection (v2.1). Values stay as {{variables}}. */
|
|
605
|
+
export function exportCollection(folder, name, format, relFile, lastRuns) {
|
|
606
|
+
const sc = scanHttp(folder, undefined);
|
|
607
|
+
const files = sc.files.filter((f) => !relFile || f.relFile === relFile);
|
|
608
|
+
if (relFile && !files.length)
|
|
609
|
+
throw new ObjectError(`${relFile} is not one of the .http files`);
|
|
610
|
+
const slug = (relFile ? relFile.replace(/\.(http|rest)$/i, '') : name).replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '') || 'requests';
|
|
611
|
+
if (format === 'postman') {
|
|
612
|
+
const vars = [...new Map(files.flatMap((f) => Object.entries(f.fileVariables)).map(([key, value]) => [key, { key, value }])).values()];
|
|
613
|
+
const item = (f) => f.requests.map((r) => {
|
|
614
|
+
const hasBody = r.body !== undefined && !['GET', 'HEAD'].includes(r.method);
|
|
615
|
+
const ct = r.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? '';
|
|
616
|
+
return { name: r.name, request: { method: r.method, header: r.headers.map((h) => ({ key: h.name, value: h.value })), url: { raw: r.url }, ...(hasBody ? { body: { mode: 'raw', raw: r.body, options: { raw: { language: /json/i.test(ct) ? 'json' : /xml/i.test(ct) ? 'xml' : 'text' } } } } : {}) } };
|
|
617
|
+
});
|
|
618
|
+
const items = files.length === 1 ? item(files[0]) : files.map((f) => ({ name: f.relFile, item: item(f) }));
|
|
619
|
+
return { filename: `${slug}.postman_collection.json`, mime: 'application/json', content: JSON.stringify({ info: { name: relFile ?? name, schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' }, item: items, variable: vars }, null, 2) };
|
|
620
|
+
}
|
|
621
|
+
const out = [`# ${relFile ?? name}`, ''];
|
|
622
|
+
for (const f of files) {
|
|
623
|
+
if (!relFile)
|
|
624
|
+
out.push(`## ${f.relFile}`, '');
|
|
625
|
+
const vs = Object.entries(f.fileVariables);
|
|
626
|
+
if (vs.length)
|
|
627
|
+
out.push('Variables of the file:', '', ...vs.map(([k, v]) => `- \`${k}\` = \`${v}\``), '');
|
|
628
|
+
for (const r of f.requests) {
|
|
629
|
+
out.push(`${relFile ? '##' : '###'} ${r.name}`, '', '```http', `${r.method} ${r.url}`, ...r.headers.map((h) => `${h.name}: ${h.value}`), '```', '');
|
|
630
|
+
if (r.body)
|
|
631
|
+
out.push('Body:', '', '```' + (/^\s*[[{]/.test(r.body) ? 'json' : ''), r.body, '```', '');
|
|
632
|
+
if (r.refs.length)
|
|
633
|
+
out.push(`Uses the answer of: ${r.refs.map((n) => `\`${n}\``).join(', ')}.`, '');
|
|
634
|
+
const last = lastRuns?.(f.relFile, r.index);
|
|
635
|
+
if (last) {
|
|
636
|
+
let b = last.body;
|
|
637
|
+
try {
|
|
638
|
+
b = JSON.stringify(JSON.parse(b), null, 2);
|
|
639
|
+
}
|
|
640
|
+
catch { /* as it came */ }
|
|
641
|
+
out.push(`Example answer (${last.status}):`, '', '```' + (/json/i.test(last.contentType) ? 'json' : ''), b.length > 4000 ? `${b.slice(0, 4000)}\n…` : b, '```', '');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return { filename: `${slug}.md`, mime: 'text/markdown', content: out.join('\n') };
|
|
646
|
+
}
|
|
647
|
+
export async function runHttp(folder, relFile, index, env, hive, session = { globals: {}, responses: new Map() }) {
|
|
648
|
+
const root = realFolder(folder);
|
|
649
|
+
const abs = inside(root, relFile);
|
|
650
|
+
if (!/\.(http|rest)$/i.test(abs))
|
|
651
|
+
throw new ObjectError('That is not a .http file');
|
|
652
|
+
const file = parseHttpFile(readFileSync(abs, 'utf8'), relFile);
|
|
653
|
+
const item = file.requests[index];
|
|
654
|
+
if (!item)
|
|
655
|
+
throw new ObjectError(`Request #${index} is not in ${relFile}`);
|
|
656
|
+
const e = envVars(root, abs, env, hive);
|
|
657
|
+
if (env && !e.names.includes(env))
|
|
658
|
+
throw new ObjectError(`The environment "${env}" is not in the env files`);
|
|
659
|
+
const vars = { ...file.fileVariables, ...e.vars, ...session.globals };
|
|
660
|
+
const unresolved = new Set();
|
|
661
|
+
const url = substitute(item.url, vars, unresolved, session);
|
|
662
|
+
const headers = item.headers.map((h) => ({ name: h.name, value: substitute(h.value, vars, unresolved, session) }));
|
|
663
|
+
for (const h of headers)
|
|
664
|
+
h.value = basicAuth(h.name, h.value);
|
|
665
|
+
let body = item.body === undefined ? undefined : substitute(item.body, vars, unresolved, session);
|
|
666
|
+
const needRun = [...new Set([...unresolved].filter((k) => REF.test(k)).map((k) => REF.exec(k)[1]))];
|
|
667
|
+
if (needRun.length)
|
|
668
|
+
throw new ObjectError(`This request uses the answer of ${needRun.map((n) => `“${n}”`).join(', ')}, which has not been run in this session: run ${needRun.length > 1 ? 'them' : 'it'} first (or the whole file, in order).`);
|
|
669
|
+
if (unresolved.size)
|
|
670
|
+
throw new ObjectError(`Missing values for: ${[...unresolved].map((v) => `{{${v}}}`).join(', ')}${e.names.length ? '. Pick an environment that defines them.' : '. Define them in the file (@name = value), in an http-client.env.json, or in the Variables of this object.'}`);
|
|
671
|
+
// `< ./file` as the whole body: the content of a file next to the .http file (inside the folder).
|
|
672
|
+
const inc = body && /^<\s+(\S+)\s*$/.exec(body.trim());
|
|
673
|
+
if (inc) {
|
|
674
|
+
const p = inside(root, join(relative(root, dirname(abs)), inc[1]));
|
|
675
|
+
if (statSync(p).size > MAX_INCLUDE)
|
|
676
|
+
throw new ObjectError('The included file is too large');
|
|
677
|
+
body = readFileSync(p, 'utf8');
|
|
678
|
+
}
|
|
679
|
+
let target;
|
|
680
|
+
try {
|
|
681
|
+
target = new URL(url);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
throw new ObjectError(`"${url}" is not a full URL: use http:// or https://, or define a {{host}} variable`);
|
|
685
|
+
}
|
|
686
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:')
|
|
687
|
+
throw new ObjectError('Only http and https URLs can be requested');
|
|
688
|
+
const hasBody = body !== undefined && !['GET', 'HEAD'].includes(item.method);
|
|
689
|
+
const started = Date.now(), ctl = new AbortController(), timer = setTimeout(() => ctl.abort(), TIMEOUT_MS);
|
|
690
|
+
try {
|
|
691
|
+
const res = await fetch(target, { method: item.method, headers: Object.fromEntries(headers.map((h) => [h.name, h.value])), body: hasBody ? body : undefined, signal: ctl.signal, redirect: 'follow' });
|
|
692
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
693
|
+
const durationMs = Date.now() - started;
|
|
694
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
695
|
+
const binary = buf.subarray(0, 4000).includes(0) || /^(image|audio|video)\//.test(contentType) || /octet-stream|zip|pdf/.test(contentType);
|
|
696
|
+
const truncated = buf.length > MAX_RESPONSE;
|
|
697
|
+
const text = binary ? '' : buf.subarray(0, MAX_RESPONSE).toString('utf8');
|
|
698
|
+
// The answer, as the handlers and the other requests see it: JSON when it is JSON.
|
|
699
|
+
let parsed = text;
|
|
700
|
+
if (!binary && (/json/i.test(contentType) || /^\s*[[{]/.test(text))) {
|
|
701
|
+
try {
|
|
702
|
+
parsed = JSON.parse(text);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
parsed = text;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const lower = Object.fromEntries([...res.headers.entries()].map(([k, v]) => [k.toLowerCase(), v]));
|
|
709
|
+
if (item.named)
|
|
710
|
+
session.responses.set(item.named, { status: res.status, headers: lower, body: parsed, text, at: Date.now() });
|
|
711
|
+
const handled = item.script ? runScript(item.script, { status: res.status, headers: lower, body: parsed, text, contentType, time: durationMs }, session.globals) : null;
|
|
712
|
+
return {
|
|
713
|
+
tests: handled?.tests ?? [], logs: handled?.logs ?? [], set: handled?.set ?? [], scriptError: handled?.error, scriptFile: item.scriptFile,
|
|
714
|
+
request: { method: item.method, url: target.toString(), headers: headers.map((h) => ({ name: h.name, value: mask(h.name, h.value) })), body: hasBody ? body : undefined },
|
|
715
|
+
status: res.status, statusText: res.statusText, durationMs, size: buf.length,
|
|
716
|
+
headers: [...res.headers.entries()].map(([name, value]) => ({ name, value })),
|
|
717
|
+
body: text, truncated, binary, contentType,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
if (err.name === 'AbortError')
|
|
722
|
+
throw new ObjectError(`The request took more than ${TIMEOUT_MS / 1000} s and was cancelled`);
|
|
723
|
+
const cause = err.cause;
|
|
724
|
+
throw new ObjectError(`The request failed: ${cause?.code ? `${cause.code} (${cause.message ?? ''})` : err.message}`.slice(0, 300));
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
export const httpCount = (folder) => { try {
|
|
731
|
+
return scanHttp(folder).files.reduce((n, f) => n + f.requests.length, 0);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return 0;
|
|
735
|
+
} };
|