pathes
Build self-hosted HTTP Toolkit / build (linux-x64, ubuntu-latest, true) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (macos-arm64, macos-latest, true) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (windows-x64, windows-latest, false) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / release (push) Canceled after 0s

This commit is contained in:
2026-08-15 01:59:20 +05:00
commit aeb59e8eda
16 changed files with 862 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
/*
* Single-file binary entry point (compiled with `bun build --compile`).
*
* Not part of upstream. Copied to the server submodule root by scripts/apply.sh.
* The built UI is embedded into the binary as `ui.tar.gz` (created by
* scripts/build.sh) and extracted to a temp dir on first run.
*
* Usage: httptoolkit [--port 8080] [--server-port 45457] [--mockttp-port 45456] [--no-open]
*/
// Bun global is present at runtime in the compiled binary:
declare const Bun: { file(path: string): { arrayBuffer(): Promise<ArrayBuffer> } };
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import * as fsp from 'fs/promises';
import { x as extractTar } from 'tar';
import { startHtkApp } from './src/htk-app';
import { SERVER_VERSION } from './src/constants';
// Embedded UI bundle. Resolves to a path Bun can read from the compiled binary:
import uiArchive from './ui.tar.gz' with { type: 'file' };
function printHelp() {
console.log(`HTTP Toolkit (self-hosted single binary) v${SERVER_VERSION}
Usage: httptoolkit [options]
Options:
-p, --port <port> Port to serve the web UI on (default: 8080)
--server-port <port> HTK management API port (default: 45457)
--mockttp-port <port> Mockttp admin port (default: 45456)
--no-open Don't open a browser automatically
-h, --help Show this help
`);
}
function parseArgs(argv: string[]) {
const opts = { port: 8080, serverPort: 45457, mockttpPort: 45456, open: true };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const eq = a.indexOf('=');
const inlineVal = eq !== -1 ? a.slice(eq + 1) : undefined;
const key = eq !== -1 ? a.slice(0, eq) : a;
const val = () => inlineVal ?? argv[++i];
switch (key) {
case '-p': case '--port': opts.port = parseInt(val(), 10); break;
case '--server-port': opts.serverPort = parseInt(val(), 10); break;
case '--mockttp-port': opts.mockttpPort = parseInt(val(), 10); break;
case '--no-open': opts.open = false; break;
case '-h': case '--help': printHelp(); process.exit(0);
default:
console.error(`Unknown option: ${a}`);
printHelp();
process.exit(1);
}
}
if (!Number.isInteger(opts.port) || opts.port <= 0 || opts.port > 65535) {
console.error(`Invalid --port value`); process.exit(1);
}
return opts;
}
async function extractUi(): Promise<string> {
const dir = path.join(os.tmpdir(), `httptoolkit-ui-${SERVER_VERSION}`);
const marker = path.join(dir, '.extracted');
if (fs.existsSync(marker)) return dir; // already extracted for this version
await fsp.rm(dir, { recursive: true, force: true });
await fsp.mkdir(dir, { recursive: true });
const bytes = new Uint8Array(await Bun.file(uiArchive).arrayBuffer());
const tmpArchive = path.join(dir, 'ui.tar.gz');
await fsp.writeFile(tmpArchive, bytes);
await extractTar({ file: tmpArchive, cwd: dir });
await fsp.unlink(tmpArchive).catch(() => {});
await fsp.writeFile(marker, '');
return dir;
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
const uiDir = await extractUi();
await startHtkApp({ ...opts, uiDir });
}
main().catch((e) => {
console.error('Failed to start HTTP Toolkit:', e);
process.exit(1);
});
+37
View File
@@ -0,0 +1,37 @@
/*
* Fallback oclif command: `httptoolkit-server app`.
*
* Not part of upstream. Copied into the server submodule by scripts/apply.sh.
* This lets the same feature run via the classic oclif-packed build (or `npm`),
* as an alternative to the Bun single-file binary. Here the built UI is expected
* to live in a sibling `ui/` directory next to the package root (APP_ROOT).
*/
import * as path from 'path';
import { Command, flags } from '@oclif/command';
import { APP_ROOT } from '../constants';
import { startHtkApp } from '../htk-app';
export default class App extends Command {
static description = 'run the HTK server and serve the bundled web UI locally';
static flags = {
help: flags.help({ char: 'h' }),
port: flags.integer({ char: 'p', description: 'port to serve the web UI on', default: 8080 }),
'server-port': flags.integer({ description: 'HTK management API port', default: 45457 }),
'mockttp-port': flags.integer({ description: 'Mockttp admin port', default: 45456 }),
'no-open': flags.boolean({ description: "don't open a browser automatically", default: false })
};
async run() {
const { flags: f } = this.parse(App);
await startHtkApp({
uiDir: path.join(APP_ROOT, 'ui'),
port: f.port,
serverPort: f['server-port'],
mockttpPort: f['mockttp-port'],
open: !f['no-open']
});
}
}
+133
View File
@@ -0,0 +1,133 @@
/*
* Self-hosted HTTP Toolkit: shared core logic.
*
* This is NOT part of upstream httptoolkit-server. It's copied into the server
* submodule by scripts/apply.sh. It starts the normal HTK backend (proxy admin
* + management API) via runHTK(), then serves the pre-built web UI over HTTP on
* a local port and (optionally) opens a browser.
*
* The UI connects back to 127.0.0.1:<serverPort>/<mockttpPort> at runtime (its
* built-in defaults), so as long as those match, everything wires up with no
* auth token required (we never set HTK_SERVER_TOKEN).
*/
import * as http from 'http';
import * as fs from 'fs';
import * as fsp from 'fs/promises';
import * as path from 'path';
import * as net from 'net';
import { spawn } from 'child_process';
import { runHTK } from './index';
const MIME: { [ext: string]: string } = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.wasm': 'application/wasm',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.map': 'application/json; charset=utf-8',
'.txt': 'text/plain; charset=utf-8'
};
const contentType = (file: string) =>
MIME[path.extname(file).toLowerCase()] || 'application/octet-stream';
function openBrowser(url: string) {
const platform = process.platform;
const [cmd, args] = platform === 'darwin'
? ['open', [url]]
: platform === 'win32'
? ['cmd', ['/c', 'start', '', url]]
: ['xdg-open', [url]];
try {
const child = spawn(cmd, args as string[], { stdio: 'ignore', detached: true });
// Never let a missing opener crash the app - the URL is printed anyway.
child.on('error', () => {});
child.unref();
} catch { /* ignore - user opens the printed URL manually */ }
}
async function serveStaticUi(uiDir: string, port: number): Promise<http.Server> {
const indexHtml = path.join(uiDir, 'index.html');
const root = path.resolve(uiDir);
const server = http.createServer(async (req, res) => {
try {
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
let filePath = path.resolve(root, '.' + path.posix.normalize(urlPath));
// Prevent path traversal outside the UI directory:
if (filePath !== root && !filePath.startsWith(root + path.sep)) {
res.writeHead(403); res.end('Forbidden'); return;
}
let stat = await fsp.stat(filePath).catch(() => null);
if (stat && stat.isDirectory()) {
filePath = path.join(filePath, 'index.html');
stat = await fsp.stat(filePath).catch(() => null);
}
// SPA fallback: unknown non-file routes serve index.html so the UI's
// client-side router (reach/router) can handle them.
if (!stat) {
filePath = indexHtml;
stat = await fsp.stat(filePath).catch(() => null);
if (!stat) { res.writeHead(404); res.end('UI bundle not found'); return; }
}
res.writeHead(200, {
'Content-Type': contentType(filePath),
'Content-Length': stat.size
});
fs.createReadStream(filePath).pipe(res);
} catch {
if (!res.headersSent) res.writeHead(500);
res.end('Internal error');
}
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', resolve);
});
return server;
}
export interface HtkAppOptions {
uiDir: string; // directory containing the built UI (index.html + assets)
port: number; // port to serve the UI on
serverPort: number; // HTK management API (UI default: 45457)
mockttpPort: number; // Mockttp admin (UI default: 45456)
open: boolean; // whether to auto-open a browser
}
export async function startHtkApp(opts: HtkAppOptions) {
// Match upstream start.ts: disable autoSelectFamily (unstable on Node 20+).
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
// Start the real HTK backend. No auth token => the locally-served UI can
// talk to it without an ?authToken param.
await runHTK({
serverPort: opts.serverPort,
mockttpPort: opts.mockttpPort
});
await serveStaticUi(opts.uiDir, opts.port);
const url = `http://localhost:${opts.port}`;
console.log(`\n HTTP Toolkit is running at ${url}`);
console.log(` (management API: 127.0.0.1:${opts.serverPort}, mockttp: 127.0.0.1:${opts.mockttpPort})\n`);
if (opts.open) openBrowser(url);
}