@lopatnov/conduit 0.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/Readme.md +99 -0
- package/bin/conduit.js +80 -0
- package/bin/download.js +170 -0
- package/bin/native/conduit-aarch64-apple-darwin +0 -0
- package/bin/native/conduit-aarch64-unknown-linux-gnu +0 -0
- package/bin/native/conduit-x86_64-apple-darwin +0 -0
- package/bin/native/conduit-x86_64-pc-windows-msvc.exe +0 -0
- package/bin/native/conduit-x86_64-unknown-linux-gnu +0 -0
- package/bin/native/conduit-x86_64-unknown-linux-musl +0 -0
- package/package.json +40 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @lopatnov/conduit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@lopatnov/conduit)
|
|
4
|
+
[](https://github.com/lopatnov/conduit/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
**High-performance reverse proxy and static file server** — powered by [Cloudflare Pingora](https://github.com/cloudflare/pingora). Runs as a native Rust binary, distributed via npm for convenience.
|
|
7
|
+
|
|
8
|
+
- **Single binary** — no Node.js runtime needed after install
|
|
9
|
+
- **One JSON file** describes your entire server
|
|
10
|
+
- Static file server, reverse proxy, TLS termination, HTTP/2, IP filtering, redirects
|
|
11
|
+
- Hot-reload in dev and Auto-TLS (Let's Encrypt) in production — [coming soon](https://github.com/lopatnov/conduit#implementation-status)
|
|
12
|
+
- Drop-in upgrade path from `express-reverse-proxy`
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Use without installing
|
|
18
|
+
npx @lopatnov/conduit
|
|
19
|
+
|
|
20
|
+
# Or install globally
|
|
21
|
+
npm install -g @lopatnov/conduit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The `postinstall` script automatically downloads the correct pre-built binary for your
|
|
25
|
+
platform from [GitHub Releases](https://github.com/lopatnov/conduit/releases).
|
|
26
|
+
|
|
27
|
+
To skip the download (e.g., you built from source and placed the binary yourself):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
CONDUIT_SKIP_DOWNLOAD=1 npm install @lopatnov/conduit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Interactive setup wizard
|
|
37
|
+
conduit init
|
|
38
|
+
|
|
39
|
+
# Start the server
|
|
40
|
+
conduit
|
|
41
|
+
|
|
42
|
+
# Validate config without starting
|
|
43
|
+
conduit validate
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Minimal `conduit.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"port": 3000,
|
|
51
|
+
"static": "./dist",
|
|
52
|
+
"proxy": { "/api": "http://localhost:4000" }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
conduit start server (reads conduit.json)
|
|
60
|
+
conduit -c <file> use a specific config file
|
|
61
|
+
conduit init interactive wizard
|
|
62
|
+
conduit validate validate config (exit 0 = OK)
|
|
63
|
+
conduit probe HEAD to every upstream
|
|
64
|
+
conduit fmt [--write] pretty-print config
|
|
65
|
+
conduit reload hot-reload config (no restart)
|
|
66
|
+
conduit status server status
|
|
67
|
+
conduit upstreams upstream health and latency
|
|
68
|
+
conduit shutdown graceful shutdown
|
|
69
|
+
conduit --version
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Supported Platforms
|
|
73
|
+
|
|
74
|
+
| Platform | Architecture | Supported |
|
|
75
|
+
| -------- | --------------------- | --------- |
|
|
76
|
+
| Linux | x86-64 | ✅ |
|
|
77
|
+
| Linux | ARM64 | ✅ |
|
|
78
|
+
| macOS | x86-64 (Intel) | ✅ |
|
|
79
|
+
| macOS | ARM64 (Apple Silicon) | ✅ |
|
|
80
|
+
| Windows | x86-64 | ✅ |
|
|
81
|
+
|
|
82
|
+
## Alternatives
|
|
83
|
+
|
|
84
|
+
If the binary download fails or your platform is not supported, install from source:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cargo install lopatnov-conduit
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or download a pre-built binary directly from
|
|
91
|
+
[GitHub Releases](https://github.com/lopatnov/conduit/releases).
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
Full documentation: <https://github.com/lopatnov/conduit>
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
[Apache 2.0](https://github.com/lopatnov/conduit/blob/main/LICENSE)
|
package/bin/conduit.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Conduit — platform wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the native binary for the current OS/arch and spawns it,
|
|
6
|
+
* passing all CLI arguments and stdio through transparently.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
// --------------------------------------------------------------------------
|
|
17
|
+
// Platform → binary name mapping (matches GitHub Release asset names)
|
|
18
|
+
// --------------------------------------------------------------------------
|
|
19
|
+
function getPlatformBinary() {
|
|
20
|
+
const { platform, arch } = process;
|
|
21
|
+
|
|
22
|
+
const map = {
|
|
23
|
+
"linux-x64": "conduit-x86_64-unknown-linux-gnu",
|
|
24
|
+
"linux-arm64": "conduit-aarch64-unknown-linux-gnu",
|
|
25
|
+
"darwin-x64": "conduit-x86_64-apple-darwin",
|
|
26
|
+
"darwin-arm64":"conduit-aarch64-apple-darwin",
|
|
27
|
+
"win32-x64": "conduit-x86_64-pc-windows-msvc.exe",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const key = `${platform}-${arch}`;
|
|
31
|
+
const name = map[key];
|
|
32
|
+
|
|
33
|
+
if (!name) {
|
|
34
|
+
console.error(
|
|
35
|
+
`[conduit] Unsupported platform: ${key}\n` +
|
|
36
|
+
`Supported: ${Object.keys(map).join(", ")}\n` +
|
|
37
|
+
`Install from source: cargo install conduit-proxy`
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --------------------------------------------------------------------------
|
|
46
|
+
// Locate binary
|
|
47
|
+
// --------------------------------------------------------------------------
|
|
48
|
+
const nativeDir = join(__dirname, "native");
|
|
49
|
+
const binaryName = getPlatformBinary();
|
|
50
|
+
const binaryPath = join(nativeDir, binaryName);
|
|
51
|
+
|
|
52
|
+
if (!existsSync(binaryPath)) {
|
|
53
|
+
console.error(
|
|
54
|
+
`[conduit] Native binary not found: ${binaryPath}\n` +
|
|
55
|
+
`Try reinstalling: npm install @lopatnov/conduit\n` +
|
|
56
|
+
`Or install from source: cargo install conduit-proxy`
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --------------------------------------------------------------------------
|
|
62
|
+
// Spawn
|
|
63
|
+
// --------------------------------------------------------------------------
|
|
64
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
65
|
+
stdio: "inherit",
|
|
66
|
+
env: process.env,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
child.on("error", (err) => {
|
|
70
|
+
console.error(`[conduit] Failed to start binary: ${err.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.on("exit", (code, signal) => {
|
|
75
|
+
if (signal) {
|
|
76
|
+
process.kill(process.pid, signal);
|
|
77
|
+
} else {
|
|
78
|
+
process.exit(code ?? 0);
|
|
79
|
+
}
|
|
80
|
+
});
|
package/bin/download.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Conduit — postinstall downloader.
|
|
4
|
+
*
|
|
5
|
+
* Downloads the native binary for the current platform from GitHub Releases.
|
|
6
|
+
* Runs automatically after `npm install`.
|
|
7
|
+
*
|
|
8
|
+
* Skipped when:
|
|
9
|
+
* - CONDUIT_SKIP_DOWNLOAD=1 (opt-out for custom installs)
|
|
10
|
+
* - npm lifecycle is "ci" (avoid re-downloading in production installs
|
|
11
|
+
* where the binary is vendored or built locally)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createWriteStream, existsSync, mkdirSync, chmodSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { get } from "node:https";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
// --------------------------------------------------------------------------
|
|
21
|
+
// Skip conditions
|
|
22
|
+
// --------------------------------------------------------------------------
|
|
23
|
+
if (process.env.CONDUIT_SKIP_DOWNLOAD === "1") {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --------------------------------------------------------------------------
|
|
28
|
+
// Config
|
|
29
|
+
// --------------------------------------------------------------------------
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
32
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
33
|
+
const VERSION = pkg.version;
|
|
34
|
+
const REPO = "lopatnov/conduit";
|
|
35
|
+
const NATIVE_DIR = join(__dirname, "native");
|
|
36
|
+
|
|
37
|
+
// --------------------------------------------------------------------------
|
|
38
|
+
// Platform → asset name
|
|
39
|
+
// --------------------------------------------------------------------------
|
|
40
|
+
function getAssetName() {
|
|
41
|
+
const { platform, arch } = process;
|
|
42
|
+
|
|
43
|
+
const map = {
|
|
44
|
+
"linux-x64": "conduit-x86_64-unknown-linux-gnu",
|
|
45
|
+
"linux-arm64": "conduit-aarch64-unknown-linux-gnu",
|
|
46
|
+
"darwin-x64": "conduit-x86_64-apple-darwin",
|
|
47
|
+
"darwin-arm64": "conduit-aarch64-apple-darwin",
|
|
48
|
+
"win32-x64": "conduit-x86_64-pc-windows-msvc.exe",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const key = `${platform}-${arch}`;
|
|
52
|
+
const name = map[key];
|
|
53
|
+
|
|
54
|
+
if (!name) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`[conduit] Skipping binary download — unsupported platform: ${key}\n` +
|
|
57
|
+
`Install from source: cargo install conduit-proxy`
|
|
58
|
+
);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --------------------------------------------------------------------------
|
|
66
|
+
// Download helper (follows redirects, no external dependencies)
|
|
67
|
+
// --------------------------------------------------------------------------
|
|
68
|
+
const MAX_REDIRECTS = 10;
|
|
69
|
+
|
|
70
|
+
function download(url, dest) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const file = createWriteStream(dest);
|
|
73
|
+
let redirectCount = 0;
|
|
74
|
+
|
|
75
|
+
function request(url) {
|
|
76
|
+
get(url, { headers: { "User-Agent": `conduit-npm/${VERSION}` } }, (res) => {
|
|
77
|
+
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
|
|
78
|
+
if (++redirectCount > MAX_REDIRECTS) {
|
|
79
|
+
file.close();
|
|
80
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
81
|
+
reject(new Error(`Too many redirects (>${MAX_REDIRECTS}) for ${url}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!res.headers.location) {
|
|
85
|
+
file.close();
|
|
86
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
87
|
+
reject(new Error(`Redirect with no Location header from ${url}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Follow redirect (resolve relative URLs against current URL).
|
|
91
|
+
request(new URL(res.headers.location, url).href);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (res.statusCode !== 200) {
|
|
96
|
+
file.close();
|
|
97
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
98
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const total = parseInt(res.headers["content-length"] || "0", 10);
|
|
103
|
+
let received = 0;
|
|
104
|
+
let lastPct = -1;
|
|
105
|
+
|
|
106
|
+
res.on("data", (chunk) => {
|
|
107
|
+
received += chunk.length;
|
|
108
|
+
if (total > 0) {
|
|
109
|
+
const pct = Math.floor((received / total) * 100);
|
|
110
|
+
if (pct !== lastPct && pct % 10 === 0) {
|
|
111
|
+
process.stdout.write(`\r[conduit] Downloading... ${pct}%`);
|
|
112
|
+
lastPct = pct;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
res.pipe(file);
|
|
118
|
+
file.on("finish", () => {
|
|
119
|
+
file.close(() => {
|
|
120
|
+
process.stdout.write("\r[conduit] Downloading... done \n");
|
|
121
|
+
resolve();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}).on("error", (err) => {
|
|
125
|
+
file.close();
|
|
126
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
127
|
+
reject(err);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
request(url);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --------------------------------------------------------------------------
|
|
136
|
+
// Main
|
|
137
|
+
// --------------------------------------------------------------------------
|
|
138
|
+
async function main() {
|
|
139
|
+
const assetName = getAssetName();
|
|
140
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${assetName}`;
|
|
141
|
+
const dest = join(NATIVE_DIR, assetName);
|
|
142
|
+
|
|
143
|
+
// Skip if already downloaded (idempotent)
|
|
144
|
+
if (existsSync(dest)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
mkdirSync(NATIVE_DIR, { recursive: true });
|
|
149
|
+
|
|
150
|
+
console.log(`[conduit] Downloading v${VERSION} for ${process.platform}/${process.arch}`);
|
|
151
|
+
console.log(`[conduit] Source: ${url}`);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await download(url, dest);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`\n[conduit] Download failed: ${err.message}`);
|
|
157
|
+
console.error(`[conduit] You can install from source: cargo install conduit-proxy`);
|
|
158
|
+
// Exit 0 so npm install doesn't fail for the whole project
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Make executable on Unix
|
|
163
|
+
if (process.platform !== "win32") {
|
|
164
|
+
chmodSync(dest, 0o755);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`[conduit] Binary installed to ${dest}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lopatnov/conduit",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "High-performance reverse proxy and static file server powered by Cloudflare Pingora",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"reverse-proxy",
|
|
7
|
+
"proxy",
|
|
8
|
+
"static-server",
|
|
9
|
+
"http",
|
|
10
|
+
"https",
|
|
11
|
+
"http2",
|
|
12
|
+
"load-balancer",
|
|
13
|
+
"rust",
|
|
14
|
+
"pingora"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/lopatnov/conduit#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/lopatnov/conduit/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/lopatnov/conduit.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"author": "lopatnov",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"conduit": "./bin/conduit.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin/",
|
|
32
|
+
"Readme.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"postinstall": "node bin/download.js"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|