@robiki/proxy 1.0.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.
@@ -0,0 +1,226 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ class ProxyConfig {
5
+ config;
6
+ sslConfig;
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.initializeSSL();
10
+ }
11
+ /**
12
+ * Initialize SSL configuration
13
+ */
14
+ initializeSSL() {
15
+ if (!this.config.ssl) return;
16
+ try {
17
+ const key = this.config.ssl.key.includes("-----BEGIN") ? Buffer.from(this.config.ssl.key) : readFileSync(resolve(this.config.ssl.key));
18
+ const cert = this.config.ssl.cert.includes("-----BEGIN") ? Buffer.from(this.config.ssl.cert) : readFileSync(resolve(this.config.ssl.cert));
19
+ const ca = this.config.ssl.ca ? this.config.ssl.ca.includes("-----BEGIN") ? Buffer.from(this.config.ssl.ca) : readFileSync(resolve(this.config.ssl.ca)) : void 0;
20
+ this.sslConfig = {
21
+ key,
22
+ cert,
23
+ ca,
24
+ allowHTTP1: this.config.ssl.allowHTTP1 ?? true
25
+ };
26
+ } catch (error) {
27
+ console.error("Failed to load SSL certificates:", error);
28
+ throw error;
29
+ }
30
+ }
31
+ /**
32
+ * Get SSL configuration
33
+ */
34
+ getSSL() {
35
+ return this.sslConfig;
36
+ }
37
+ /**
38
+ * Get route configuration for a host
39
+ */
40
+ getRoute(host) {
41
+ if (this.config.routes[host]) {
42
+ return this.config.routes[host];
43
+ }
44
+ const hostWithoutPort = host.split(":")[0];
45
+ if (this.config.routes[hostWithoutPort]) {
46
+ return this.config.routes[hostWithoutPort];
47
+ }
48
+ for (const [pattern, route] of Object.entries(this.config.routes)) {
49
+ if (pattern.includes("*")) {
50
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
51
+ if (regex.test(host) || regex.test(hostWithoutPort)) {
52
+ return route;
53
+ }
54
+ }
55
+ }
56
+ return void 0;
57
+ }
58
+ /**
59
+ * Get target for a host
60
+ */
61
+ getTarget(host) {
62
+ const route = this.getRoute(host);
63
+ if (!route) {
64
+ return { target: void 0, ssl: void 0, remap: void 0 };
65
+ }
66
+ return {
67
+ target: route.target,
68
+ ssl: route.ssl ? this.sslConfig : void 0,
69
+ remap: route.remap
70
+ };
71
+ }
72
+ /**
73
+ * Get CORS headers for a request
74
+ */
75
+ getCorsHeaders(origin, host) {
76
+ const route = host ? this.getRoute(host) : void 0;
77
+ const corsConfig = route?.cors || this.config.cors;
78
+ if (!corsConfig) {
79
+ return {
80
+ "access-control-allow-origin": origin,
81
+ "access-control-allow-methods": "*",
82
+ "access-control-allow-headers": "*",
83
+ "access-control-allow-credentials": "true"
84
+ };
85
+ }
86
+ const headers = {};
87
+ if (corsConfig.origin === "*") {
88
+ headers["access-control-allow-origin"] = "*";
89
+ } else if (Array.isArray(corsConfig.origin)) {
90
+ if (corsConfig.origin.includes(origin)) {
91
+ headers["access-control-allow-origin"] = origin;
92
+ }
93
+ } else if (corsConfig.origin) {
94
+ headers["access-control-allow-origin"] = corsConfig.origin;
95
+ } else {
96
+ headers["access-control-allow-origin"] = origin;
97
+ }
98
+ if (corsConfig.methods) {
99
+ headers["access-control-allow-methods"] = corsConfig.methods.join(", ");
100
+ } else {
101
+ headers["access-control-allow-methods"] = "*";
102
+ }
103
+ if (corsConfig.allowedHeaders) {
104
+ headers["access-control-allow-headers"] = corsConfig.allowedHeaders.join(", ");
105
+ } else {
106
+ headers["access-control-allow-headers"] = "*";
107
+ }
108
+ if (corsConfig.exposedHeaders) {
109
+ headers["access-control-expose-headers"] = corsConfig.exposedHeaders.join(", ");
110
+ }
111
+ if (corsConfig.credentials !== void 0) {
112
+ headers["access-control-allow-credentials"] = corsConfig.credentials ? "true" : "false";
113
+ } else {
114
+ headers["access-control-allow-credentials"] = "true";
115
+ }
116
+ if (corsConfig.maxAge) {
117
+ headers["access-control-max-age"] = corsConfig.maxAge.toString();
118
+ }
119
+ return headers;
120
+ }
121
+ /**
122
+ * Validate a request
123
+ */
124
+ async validate(info) {
125
+ const route = this.getRoute(info.authority);
126
+ if (route?.validate) return route.validate(info);
127
+ if (this.config.validate) return this.config.validate(info);
128
+ return { status: true };
129
+ }
130
+ /**
131
+ * Get ports to listen on
132
+ */
133
+ getPorts() {
134
+ return [443, 8080, 9229];
135
+ }
136
+ /**
137
+ * Get the full configuration
138
+ */
139
+ getConfig() {
140
+ return this.config;
141
+ }
142
+ }
143
+ let globalConfig;
144
+ function initConfig(config) {
145
+ globalConfig = new ProxyConfig(config);
146
+ return globalConfig;
147
+ }
148
+ function getConfig() {
149
+ if (!globalConfig) {
150
+ throw new Error("Configuration not initialized. Call initConfig() first.");
151
+ }
152
+ return globalConfig;
153
+ }
154
+ function deepMerge(...objects) {
155
+ const result = {};
156
+ for (const obj of objects) {
157
+ if (!obj) continue;
158
+ for (const key in obj) {
159
+ if (obj[key] === void 0) continue;
160
+ if (typeof obj[key] === "object" && !Array.isArray(obj[key]) && obj[key] !== null) {
161
+ result[key] = deepMerge(result[key] || {}, obj[key]);
162
+ } else {
163
+ result[key] = obj[key];
164
+ }
165
+ }
166
+ }
167
+ return result;
168
+ }
169
+ function getConfigFromEnv() {
170
+ const config = {};
171
+ if (process.env.SSL_KEY && process.env.SSL_CERT) {
172
+ config.ssl = {
173
+ key: process.env.SSL_KEY,
174
+ cert: process.env.SSL_CERT,
175
+ ca: process.env.SSL_CA,
176
+ allowHTTP1: process.env.SSL_ALLOW_HTTP1 === "true"
177
+ };
178
+ }
179
+ if (process.env.CORS_ORIGIN) {
180
+ config.cors = {
181
+ origin: process.env.CORS_ORIGIN === "*" ? "*" : process.env.CORS_ORIGIN.split(","),
182
+ methods: process.env.CORS_METHODS?.split(","),
183
+ allowedHeaders: process.env.CORS_HEADERS?.split(","),
184
+ credentials: process.env.CORS_CREDENTIALS === "true"
185
+ };
186
+ }
187
+ return config;
188
+ }
189
+ function getConfigFromFile() {
190
+ const configPath = process.env.PROXY_CONFIG || "./proxy.config.json";
191
+ try {
192
+ const configFile = readFileSync(resolve(configPath), "utf-8");
193
+ return JSON.parse(configFile);
194
+ } catch (error) {
195
+ return {};
196
+ }
197
+ }
198
+ function loadConfig(programmaticConfig) {
199
+ const defaults = {
200
+ routes: {},
201
+ cors: {
202
+ origin: "*",
203
+ credentials: true
204
+ }
205
+ };
206
+ const fileConfig = getConfigFromFile();
207
+ const envConfig = getConfigFromEnv();
208
+ const merged = deepMerge(defaults, fileConfig, envConfig, programmaticConfig || {});
209
+ return initConfig(merged);
210
+ }
211
+ function loadConfigFromFile(path) {
212
+ try {
213
+ const configFile = readFileSync(resolve(path), "utf-8");
214
+ const config = JSON.parse(configFile);
215
+ return initConfig(config);
216
+ } catch (error) {
217
+ console.error("Failed to load configuration file:", error);
218
+ throw error;
219
+ }
220
+ }
221
+ function loadConfigFromEnv() {
222
+ const config = getConfigFromEnv();
223
+ return initConfig(config);
224
+ }
225
+
226
+ export { ProxyConfig, getConfig, initConfig, loadConfig, loadConfigFromEnv, loadConfigFromFile };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@robiki/proxy",
3
+ "version": "1.0.0",
4
+ "description": "A flexible HTTP/2 proxy server with WebSocket support, configurable routing, CORS, and validation. Use as npm package or Docker container.",
5
+ "keywords": [
6
+ "proxy",
7
+ "http2",
8
+ "websocket",
9
+ "reverse-proxy",
10
+ "docker",
11
+ "cors",
12
+ "ssl",
13
+ "tls",
14
+ "routing",
15
+ "middleware"
16
+ ],
17
+ "homepage": "https://github.com/robiki-ai/robiki-proxy#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/robiki-ai/robiki-proxy/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/robiki-ai/robiki-proxy.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Robiki sp. z o.o.",
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.js",
33
+ "types": "./dist/index.d.ts"
34
+ },
35
+ "./proxy": {
36
+ "import": "./dist/proxy.js",
37
+ "types": "./dist/proxy.d.ts"
38
+ },
39
+ "./config": {
40
+ "import": "./dist/utils/config.js",
41
+ "types": "./dist/utils/config.d.ts"
42
+ }
43
+ },
44
+ "bin": {
45
+ "robiki-proxy": "./dist/index.js"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "build": "pkgroll",
54
+ "dev": "node --watch src/index.ts",
55
+ "start": "node dist/index.js",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "test:ui": "vitest --ui",
59
+ "test:coverage": "vitest run --coverage",
60
+ "test:docker": "make test-docker-full",
61
+ "test:docker:config": "make test-docker-config",
62
+ "test:all": "yarn test && yarn test:docker",
63
+ "prepublishOnly": "yarn build"
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "25.0.3",
67
+ "@types/supertest": "6.0.2",
68
+ "@vitest/coverage-v8": "4.0.16",
69
+ "@vitest/ui": "2.1.8",
70
+ "pkgroll": "2.21.4",
71
+ "supertest": "7.1.4",
72
+ "typescript": "5.9.3",
73
+ "vitest": "4.0.16"
74
+ },
75
+ "dependencies": {
76
+ "@types/ws": "8.18.1",
77
+ "ws": "8.19.0"
78
+ },
79
+ "engines": {
80
+ "node": ">=21.0.0"
81
+ },
82
+ "packageManager": "yarn@1.22.22",
83
+ "publishConfig": {
84
+ "access": "public"
85
+ }
86
+ }