@ripeseed/rs-tunnel 0.1.2

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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/commands/doctor.d.ts +1 -0
  4. package/dist/commands/doctor.js +20 -0
  5. package/dist/commands/doctor.js.map +1 -0
  6. package/dist/commands/list.d.ts +1 -0
  7. package/dist/commands/list.js +18 -0
  8. package/dist/commands/list.js.map +1 -0
  9. package/dist/commands/login.d.ts +1 -0
  10. package/dist/commands/login.js +45 -0
  11. package/dist/commands/login.js.map +1 -0
  12. package/dist/commands/logout.d.ts +1 -0
  13. package/dist/commands/logout.js +19 -0
  14. package/dist/commands/logout.js.map +1 -0
  15. package/dist/commands/stop.d.ts +1 -0
  16. package/dist/commands/stop.js +15 -0
  17. package/dist/commands/stop.js.map +1 -0
  18. package/dist/commands/up.d.ts +32 -0
  19. package/dist/commands/up.js +319 -0
  20. package/dist/commands/up.js.map +1 -0
  21. package/dist/commands/up.test.d.ts +1 -0
  22. package/dist/commands/up.test.js +467 -0
  23. package/dist/commands/up.test.js.map +1 -0
  24. package/dist/config.d.ts +10 -0
  25. package/dist/config.js +135 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/config.test.d.ts +1 -0
  28. package/dist/config.test.js +67 -0
  29. package/dist/config.test.js.map +1 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +75 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/lib/api-client.d.ts +73 -0
  34. package/dist/lib/api-client.js +146 -0
  35. package/dist/lib/api-client.js.map +1 -0
  36. package/dist/lib/cloudflared.d.ts +3 -0
  37. package/dist/lib/cloudflared.js +165 -0
  38. package/dist/lib/cloudflared.js.map +1 -0
  39. package/dist/lib/local-callback.d.ts +9 -0
  40. package/dist/lib/local-callback.js +68 -0
  41. package/dist/lib/local-callback.js.map +1 -0
  42. package/dist/lib/local-proxy.d.ts +27 -0
  43. package/dist/lib/local-proxy.js +200 -0
  44. package/dist/lib/local-proxy.js.map +1 -0
  45. package/dist/lib/local-proxy.test.d.ts +1 -0
  46. package/dist/lib/local-proxy.test.js +181 -0
  47. package/dist/lib/local-proxy.test.js.map +1 -0
  48. package/dist/lib/pkce.d.ts +4 -0
  49. package/dist/lib/pkce.js +10 -0
  50. package/dist/lib/pkce.js.map +1 -0
  51. package/dist/lib/session.d.ts +4 -0
  52. package/dist/lib/session.js +47 -0
  53. package/dist/lib/session.js.map +1 -0
  54. package/dist/lib/tunnel-stats.d.ts +19 -0
  55. package/dist/lib/tunnel-stats.js +75 -0
  56. package/dist/lib/tunnel-stats.js.map +1 -0
  57. package/dist/lib/tunnel-stats.test.d.ts +1 -0
  58. package/dist/lib/tunnel-stats.test.js +44 -0
  59. package/dist/lib/tunnel-stats.test.js.map +1 -0
  60. package/dist/lib/up-dashboard.d.ts +19 -0
  61. package/dist/lib/up-dashboard.js +170 -0
  62. package/dist/lib/up-dashboard.js.map +1 -0
  63. package/dist/lib/version.d.ts +1 -0
  64. package/dist/lib/version.js +18 -0
  65. package/dist/lib/version.js.map +1 -0
  66. package/dist/store/credentials.d.ts +4 -0
  67. package/dist/store/credentials.js +99 -0
  68. package/dist/store/credentials.js.map +1 -0
  69. package/dist/types.d.ts +10 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +51 -0
@@ -0,0 +1,27 @@
1
+ export type ProxyRequestEvent = {
2
+ startedAtEpochMs: number;
3
+ method: string;
4
+ path: string;
5
+ statusCode: number;
6
+ statusMessage: string;
7
+ durationMs: number;
8
+ responseBytes: number | null;
9
+ error: boolean;
10
+ protocol: 'http' | 'ws';
11
+ };
12
+ export type ProxyConnectionSnapshot = {
13
+ totalConnections: number;
14
+ openConnections: number;
15
+ };
16
+ export type LocalProxy = {
17
+ port: number;
18
+ stop: () => Promise<void>;
19
+ };
20
+ type StartLocalProxyInput = {
21
+ targetPort: number;
22
+ targetHost?: string;
23
+ onRequest?: (event: ProxyRequestEvent) => void;
24
+ onConnectionChange?: (snapshot: ProxyConnectionSnapshot) => void;
25
+ };
26
+ export declare function startLocalProxy(input: StartLocalProxyInput): Promise<LocalProxy>;
27
+ export {};
@@ -0,0 +1,200 @@
1
+ import http from 'node:http';
2
+ import net from 'node:net';
3
+ function toDurationMs(startedAtHrTimeNs) {
4
+ return Number(process.hrtime.bigint() - startedAtHrTimeNs) / 1_000_000;
5
+ }
6
+ function getStatusText(statusCode) {
7
+ return http.STATUS_CODES[statusCode] ?? 'Unknown';
8
+ }
9
+ function emitConnectionSnapshot(onConnectionChange, totalConnections, openConnections) {
10
+ onConnectionChange?.({
11
+ totalConnections,
12
+ openConnections,
13
+ });
14
+ }
15
+ function buildUpgradeRequest(req) {
16
+ const requestLine = `${req.method ?? 'GET'} ${req.url ?? '/'} HTTP/${req.httpVersion}`;
17
+ let headers = '';
18
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
19
+ const headerName = req.rawHeaders[i];
20
+ const headerValue = req.rawHeaders[i + 1];
21
+ if (!headerName || headerValue === undefined) {
22
+ continue;
23
+ }
24
+ headers += `${headerName}: ${headerValue}\r\n`;
25
+ }
26
+ return `${requestLine}\r\n${headers}\r\n`;
27
+ }
28
+ export async function startLocalProxy(input) {
29
+ const targetHost = input.targetHost ?? '127.0.0.1';
30
+ let totalConnections = 0;
31
+ let openConnections = 0;
32
+ const sockets = new Set();
33
+ const server = http.createServer((req, res) => {
34
+ const startedAtEpochMs = Date.now();
35
+ const startedAtHrTimeNs = process.hrtime.bigint();
36
+ const method = req.method ?? 'GET';
37
+ const path = req.url ?? '/';
38
+ let responseBytes = 0;
39
+ let finalized = false;
40
+ let statusCode = 500;
41
+ const finalize = (event) => {
42
+ if (finalized) {
43
+ return;
44
+ }
45
+ finalized = true;
46
+ const finalStatusCode = event.statusCode ?? statusCode;
47
+ const responseBytesValue = event.responseBytes ?? responseBytes;
48
+ input.onRequest?.({
49
+ startedAtEpochMs,
50
+ method,
51
+ path,
52
+ statusCode: finalStatusCode,
53
+ statusMessage: getStatusText(finalStatusCode),
54
+ durationMs: toDurationMs(startedAtHrTimeNs),
55
+ responseBytes: responseBytesValue,
56
+ error: event.error ?? finalStatusCode >= 500,
57
+ protocol: 'http',
58
+ });
59
+ };
60
+ const upstream = http.request({
61
+ host: targetHost,
62
+ port: input.targetPort,
63
+ method,
64
+ path,
65
+ headers: req.headers,
66
+ }, (upstreamResponse) => {
67
+ statusCode = upstreamResponse.statusCode ?? 502;
68
+ res.writeHead(statusCode, upstreamResponse.statusMessage, upstreamResponse.headers);
69
+ upstreamResponse.on('data', (chunk) => {
70
+ responseBytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
71
+ });
72
+ upstreamResponse.pipe(res);
73
+ });
74
+ upstream.on('error', () => {
75
+ statusCode = 502;
76
+ if (!res.headersSent) {
77
+ res.writeHead(statusCode, getStatusText(statusCode));
78
+ }
79
+ res.end('Bad Gateway');
80
+ finalize({
81
+ statusCode,
82
+ error: true,
83
+ });
84
+ });
85
+ req.pipe(upstream);
86
+ res.once('finish', () => {
87
+ finalize({
88
+ statusCode: res.statusCode,
89
+ error: res.statusCode >= 500,
90
+ });
91
+ });
92
+ res.once('close', () => {
93
+ if (!finalized) {
94
+ finalize({
95
+ statusCode: res.statusCode || statusCode,
96
+ error: true,
97
+ });
98
+ }
99
+ });
100
+ });
101
+ server.on('connection', (socket) => {
102
+ sockets.add(socket);
103
+ totalConnections += 1;
104
+ openConnections += 1;
105
+ emitConnectionSnapshot(input.onConnectionChange, totalConnections, openConnections);
106
+ socket.on('close', () => {
107
+ sockets.delete(socket);
108
+ openConnections = Math.max(0, openConnections - 1);
109
+ emitConnectionSnapshot(input.onConnectionChange, totalConnections, openConnections);
110
+ });
111
+ });
112
+ server.on('upgrade', (req, clientSocket, head) => {
113
+ const startedAtEpochMs = Date.now();
114
+ const startedAtHrTimeNs = process.hrtime.bigint();
115
+ const method = req.method ?? 'GET';
116
+ const path = req.url ?? '/';
117
+ const upstreamSocket = net.connect({
118
+ host: targetHost,
119
+ port: input.targetPort,
120
+ });
121
+ let finalized = false;
122
+ const finalize = (statusCode, isError) => {
123
+ if (finalized) {
124
+ return;
125
+ }
126
+ finalized = true;
127
+ input.onRequest?.({
128
+ startedAtEpochMs,
129
+ method,
130
+ path,
131
+ statusCode,
132
+ statusMessage: getStatusText(statusCode),
133
+ durationMs: toDurationMs(startedAtHrTimeNs),
134
+ responseBytes: null,
135
+ error: isError,
136
+ protocol: 'ws',
137
+ });
138
+ };
139
+ upstreamSocket.once('connect', () => {
140
+ upstreamSocket.write(buildUpgradeRequest(req));
141
+ if (head.length > 0) {
142
+ upstreamSocket.write(head);
143
+ }
144
+ clientSocket.pipe(upstreamSocket);
145
+ upstreamSocket.pipe(clientSocket);
146
+ finalize(101, false);
147
+ });
148
+ upstreamSocket.once('error', () => {
149
+ if (!clientSocket.destroyed) {
150
+ clientSocket.write('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n');
151
+ }
152
+ clientSocket.destroy();
153
+ finalize(502, true);
154
+ });
155
+ clientSocket.once('error', () => {
156
+ upstreamSocket.destroy();
157
+ finalize(499, true);
158
+ });
159
+ clientSocket.once('close', () => {
160
+ upstreamSocket.destroy();
161
+ });
162
+ });
163
+ await new Promise((resolve, reject) => {
164
+ server.once('error', reject);
165
+ server.listen(0, '127.0.0.1', () => {
166
+ server.off('error', reject);
167
+ resolve();
168
+ });
169
+ });
170
+ const address = server.address();
171
+ if (!address || typeof address === 'string') {
172
+ await new Promise((resolve) => {
173
+ server.close(() => resolve());
174
+ });
175
+ throw new Error('Local proxy failed to bind to a TCP port.');
176
+ }
177
+ let stopped = false;
178
+ return {
179
+ port: address.port,
180
+ stop: async () => {
181
+ if (stopped) {
182
+ return;
183
+ }
184
+ stopped = true;
185
+ for (const socket of sockets) {
186
+ socket.destroy();
187
+ }
188
+ await new Promise((resolve, reject) => {
189
+ server.close((error) => {
190
+ if (error) {
191
+ reject(error);
192
+ return;
193
+ }
194
+ resolve();
195
+ });
196
+ });
197
+ },
198
+ };
199
+ }
200
+ //# sourceMappingURL=local-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-proxy.js","sourceRoot":"","sources":["../../src/lib/local-proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,181 @@
1
+ import http from 'node:http';
2
+ import net from 'node:net';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { startLocalProxy } from './local-proxy.js';
5
+ async function listenHttpServer(handler) {
6
+ const server = http.createServer(handler);
7
+ await new Promise((resolve, reject) => {
8
+ server.once('error', reject);
9
+ server.listen(0, '127.0.0.1', () => {
10
+ server.off('error', reject);
11
+ resolve();
12
+ });
13
+ });
14
+ const address = server.address();
15
+ if (!address || typeof address === 'string') {
16
+ await new Promise((resolve) => server.close(() => resolve()));
17
+ throw new Error('Failed to bind HTTP server.');
18
+ }
19
+ return {
20
+ server,
21
+ port: address.port,
22
+ };
23
+ }
24
+ async function closeServer(server) {
25
+ await new Promise((resolve) => {
26
+ server.close(() => resolve());
27
+ });
28
+ }
29
+ async function reserveUnusedPort() {
30
+ const reservation = net.createServer();
31
+ await new Promise((resolve, reject) => {
32
+ reservation.once('error', reject);
33
+ reservation.listen(0, '127.0.0.1', () => {
34
+ reservation.off('error', reject);
35
+ resolve();
36
+ });
37
+ });
38
+ const address = reservation.address();
39
+ if (!address || typeof address === 'string') {
40
+ await new Promise((resolve) => reservation.close(() => resolve()));
41
+ throw new Error('Failed to reserve port.');
42
+ }
43
+ const port = address.port;
44
+ await new Promise((resolve) => reservation.close(() => resolve()));
45
+ return port;
46
+ }
47
+ async function wait(ms) {
48
+ await new Promise((resolve) => setTimeout(resolve, ms));
49
+ }
50
+ describe('startLocalProxy', () => {
51
+ it('forwards HTTP requests, emits request events, and tracks connections', async () => {
52
+ const requestEvents = [];
53
+ const connectionEvents = [];
54
+ const backend = await listenHttpServer((req, res) => {
55
+ if (req.url === '/ok') {
56
+ res.writeHead(200, { 'content-type': 'text/plain' });
57
+ res.end('ok');
58
+ return;
59
+ }
60
+ res.writeHead(404, { 'content-type': 'text/plain' });
61
+ res.end('not found');
62
+ });
63
+ const proxy = await startLocalProxy({
64
+ targetPort: backend.port,
65
+ onRequest: (event) => requestEvents.push(event),
66
+ onConnectionChange: (snapshot) => connectionEvents.push(snapshot),
67
+ });
68
+ try {
69
+ const okResponse = await fetch(`http://127.0.0.1:${proxy.port}/ok`, {
70
+ headers: {
71
+ connection: 'close',
72
+ },
73
+ });
74
+ const notFoundResponse = await fetch(`http://127.0.0.1:${proxy.port}/missing`, {
75
+ headers: {
76
+ connection: 'close',
77
+ },
78
+ });
79
+ expect(okResponse.status).toBe(200);
80
+ expect(await okResponse.text()).toBe('ok');
81
+ expect(notFoundResponse.status).toBe(404);
82
+ expect(await notFoundResponse.text()).toBe('not found');
83
+ await wait(60);
84
+ expect(requestEvents.length).toBe(2);
85
+ expect(requestEvents[0]?.statusCode).toBe(200);
86
+ expect(requestEvents[0]?.path).toBe('/ok');
87
+ expect(requestEvents[1]?.statusCode).toBe(404);
88
+ expect(requestEvents[1]?.path).toBe('/missing');
89
+ const maxOpenConnections = Math.max(...connectionEvents.map((event) => event.openConnections));
90
+ const finalOpenConnections = connectionEvents.at(-1)?.openConnections ?? -1;
91
+ expect(maxOpenConnections).toBeGreaterThan(0);
92
+ expect(finalOpenConnections).toBe(0);
93
+ expect(connectionEvents.at(-1)?.totalConnections).toBeGreaterThanOrEqual(1);
94
+ }
95
+ finally {
96
+ await proxy.stop();
97
+ await closeServer(backend.server);
98
+ }
99
+ });
100
+ it('returns 502 and emits error events when upstream target is unreachable', async () => {
101
+ const requestEvents = [];
102
+ const unreachablePort = await reserveUnusedPort();
103
+ const proxy = await startLocalProxy({
104
+ targetPort: unreachablePort,
105
+ onRequest: (event) => requestEvents.push(event),
106
+ });
107
+ try {
108
+ const response = await fetch(`http://127.0.0.1:${proxy.port}/will-fail`, {
109
+ headers: {
110
+ connection: 'close',
111
+ },
112
+ });
113
+ expect(response.status).toBe(502);
114
+ expect(await response.text()).toContain('Bad Gateway');
115
+ await wait(20);
116
+ expect(requestEvents).toHaveLength(1);
117
+ expect(requestEvents[0]?.statusCode).toBe(502);
118
+ expect(requestEvents[0]?.error).toBe(true);
119
+ }
120
+ finally {
121
+ await proxy.stop();
122
+ }
123
+ });
124
+ it('forwards websocket upgrades', async () => {
125
+ const backendServer = http.createServer((_, res) => {
126
+ res.writeHead(426);
127
+ res.end('upgrade required');
128
+ });
129
+ backendServer.on('upgrade', (_req, socket) => {
130
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
131
+ 'Upgrade: websocket\r\n' +
132
+ 'Connection: Upgrade\r\n' +
133
+ '\r\n');
134
+ socket.end();
135
+ });
136
+ await new Promise((resolve, reject) => {
137
+ backendServer.once('error', reject);
138
+ backendServer.listen(0, '127.0.0.1', () => {
139
+ backendServer.off('error', reject);
140
+ resolve();
141
+ });
142
+ });
143
+ const backendAddress = backendServer.address();
144
+ if (!backendAddress || typeof backendAddress === 'string') {
145
+ await closeServer(backendServer);
146
+ throw new Error('Backend server failed to bind.');
147
+ }
148
+ const proxy = await startLocalProxy({
149
+ targetPort: backendAddress.port,
150
+ });
151
+ try {
152
+ const rawResponse = await new Promise((resolve, reject) => {
153
+ const socket = net.connect({ host: '127.0.0.1', port: proxy.port });
154
+ let data = '';
155
+ socket.setEncoding('utf8');
156
+ socket.once('connect', () => {
157
+ socket.write('GET /socket HTTP/1.1\r\n' +
158
+ `Host: 127.0.0.1:${proxy.port}\r\n` +
159
+ 'Connection: Upgrade\r\n' +
160
+ 'Upgrade: websocket\r\n' +
161
+ '\r\n');
162
+ });
163
+ socket.on('data', (chunk) => {
164
+ data += chunk;
165
+ if (data.includes('\r\n\r\n')) {
166
+ resolve(data);
167
+ socket.end();
168
+ }
169
+ });
170
+ socket.on('error', reject);
171
+ });
172
+ expect(rawResponse).toContain('101 Switching Protocols');
173
+ expect(rawResponse).toContain('Upgrade: websocket');
174
+ }
175
+ finally {
176
+ await proxy.stop();
177
+ await closeServer(backendServer);
178
+ }
179
+ });
180
+ });
181
+ //# sourceMappingURL=local-proxy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-proxy.test.js","sourceRoot":"","sources":["../../src/lib/local-proxy.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAwD,MAAM,kBAAkB,CAAC;AAEzG,KAAK,UAAU,gBAAgB,CAC7B,OAAsE;IAEtE,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAE1C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YACjC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IAED,OAAO;QACL,MAAM;QACN,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,MAAmB;IAC5C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,iBAAiB;IAC9B,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;IAEvC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAClC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YACtC,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACjC,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;IACtC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACzE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,EAAU;IAC5B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,aAAa,GAAwB,EAAE,CAAC;QAC9C,MAAM,gBAAgB,GAA8B,EAAE,CAAC;QAEvD,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAClD,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;gBACtB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;gBACrD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO;YACT,CAAC;YAED,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC;YAClC,UAAU,EAAE,OAAO,CAAC,IAAI;YACxB,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;YAC/C,kBAAkB,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC;SAClE,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,oBAAoB,KAAK,CAAC,IAAI,KAAK,EAAE;gBAClE,OAAO,EAAE;oBACP,UAAU,EAAE,OAAO;iBACpB;aACF,CAAC,CAAC;YACH,MAAM,gBAAgB,GAAG,MAAM,KAAK,CAAC,oBAAoB,KAAK,CAAC,IAAI,UAAU,EAAE;gBAC7E,OAAO,EAAE;oBACP,UAAU,EAAE,OAAO;iBACpB;aACF,CAAC,CAAC;YAEH,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,CAAC,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAE3C,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAExD,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;YAEf,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAEhD,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;YAC/F,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,eAAe,IAAI,CAAC,CAAC,CAAC;YAE5E,MAAM,CAAC,kBAAkB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAC9C,MAAM,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QAC9E,CAAC;gBAAS,CAAC;YACT,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,MAAM,aAAa,GAAwB,EAAE,CAAC;QAC9C,MAAM,eAAe,GAAG,MAAM,iBAAiB,EAAE,CAAC;QAElD,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC;YAClC,UAAU,EAAE,eAAe;YAC3B,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;SAChD,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,oBAAoB,KAAK,CAAC,IAAI,YAAY,EAAE;gBACvE,OAAO,EAAE;oBACP,UAAU,EAAE,OAAO;iBACpB;aACF,CAAC,CAAC;YAEH,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;YAEvD,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;YAEf,MAAM,CAAC,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACtC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;gBAAS,CAAC;YACT,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;QAC3C,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;YACjD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YAC3C,MAAM,CAAC,KAAK,CACV,sCAAsC;gBACpC,wBAAwB;gBACxB,yBAAyB;gBACzB,MAAM,CACT,CAAC;YACF,MAAM,CAAC,GAAG,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1C,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACpC,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;gBACxC,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBACnC,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,cAAc,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC;QAC/C,IAAI,CAAC,cAAc,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;YAC1D,MAAM,WAAW,CAAC,aAAa,CAAC,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACpD,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC;YAClC,UAAU,EAAE,cAAc,CAAC,IAAI;SAChC,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAChE,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;gBACpE,IAAI,IAAI,GAAG,EAAE,CAAC;gBAEd,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBAE3B,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;oBAC1B,MAAM,CAAC,KAAK,CACV,0BAA0B;wBACxB,mBAAmB,KAAK,CAAC,IAAI,MAAM;wBACnC,yBAAyB;wBACzB,wBAAwB;wBACxB,MAAM,CACT,CAAC;gBACJ,CAAC,CAAC,CAAC;gBAEH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;oBAClC,IAAI,IAAI,KAAK,CAAC;oBACd,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;wBAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;wBACd,MAAM,CAAC,GAAG,EAAE,CAAC;oBACf,CAAC;gBACH,CAAC,CAAC,CAAC;gBAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC7B,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,WAAW,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAC;YACzD,MAAM,CAAC,WAAW,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;QACtD,CAAC;gBAAS,CAAC;YACT,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,WAAW,CAAC,aAAa,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function createPkcePair(): {
2
+ verifier: string;
3
+ challenge: string;
4
+ };
@@ -0,0 +1,10 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ export function createPkcePair() {
3
+ const verifier = randomBytes(64).toString('base64url');
4
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
5
+ return {
6
+ verifier,
7
+ challenge,
8
+ };
9
+ }
10
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/lib/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,UAAU,cAAc;IAC5B,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5E,OAAO;QACL,QAAQ;QACR,SAAS;KACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { StoredSession } from '../types.js';
2
+ import { ApiClient } from './api-client.js';
3
+ export declare function requireSession(apiClient: ApiClient): Promise<StoredSession>;
4
+ export declare function withAuthenticatedSession<T>(apiClient: ApiClient, run: (session: StoredSession) => Promise<T>): Promise<T>;
@@ -0,0 +1,47 @@
1
+ import { clearSession, loadSession, saveSession } from '../store/credentials.js';
2
+ import { ApiClientError } from './api-client.js';
3
+ const REFRESH_HEADROOM_SEC = 30;
4
+ function isExpired(session) {
5
+ return session.expiresAtEpochSec <= Math.floor(Date.now() / 1000) + REFRESH_HEADROOM_SEC;
6
+ }
7
+ async function refreshSession(apiClient, session) {
8
+ const refreshed = await apiClient.refreshTokens(session.refreshToken);
9
+ const next = {
10
+ accessToken: refreshed.accessToken,
11
+ refreshToken: refreshed.refreshToken,
12
+ expiresAtEpochSec: Math.floor(Date.now() / 1000) + refreshed.expiresInSec,
13
+ profile: refreshed.profile,
14
+ };
15
+ await saveSession(next);
16
+ return next;
17
+ }
18
+ export async function requireSession(apiClient) {
19
+ const session = await loadSession();
20
+ if (!session) {
21
+ throw new Error('Not logged in. Run: rs-tunnel login --email <you@example.com>');
22
+ }
23
+ if (isExpired(session)) {
24
+ try {
25
+ return await refreshSession(apiClient, session);
26
+ }
27
+ catch {
28
+ await clearSession();
29
+ throw new Error('Session expired. Please login again.');
30
+ }
31
+ }
32
+ return session;
33
+ }
34
+ export async function withAuthenticatedSession(apiClient, run) {
35
+ let session = await requireSession(apiClient);
36
+ try {
37
+ return await run(session);
38
+ }
39
+ catch (error) {
40
+ if (!(error instanceof ApiClientError) || error.status !== 401) {
41
+ throw error;
42
+ }
43
+ session = await refreshSession(apiClient, session);
44
+ return run(session);
45
+ }
46
+ }
47
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/lib/session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAa,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE5D,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC,SAAS,SAAS,CAAC,OAAsB;IACvC,OAAO,OAAO,CAAC,iBAAiB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,oBAAoB,CAAC;AAC3F,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,SAAoB,EAAE,OAAsB;IACxE,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAEtE,MAAM,IAAI,GAAkB;QAC1B,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,iBAAiB,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC,YAAY;QACzE,OAAO,EAAE,SAAS,CAAC,OAAO;KAC3B,CAAC;IAEF,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACxB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAoB;IACvD,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,IAAI,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,OAAO,MAAM,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,YAAY,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,SAAoB,EACpB,GAA2C;IAE3C,IAAI,OAAO,GAAG,MAAM,cAAc,CAAC,SAAS,CAAC,CAAC;IAE9C,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,cAAc,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC/D,MAAM,KAAK,CAAC;QACd,CAAC;QAED,OAAO,GAAG,MAAM,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { ProxyConnectionSnapshot, ProxyRequestEvent } from './local-proxy.js';
2
+ export type TunnelStatsSnapshot = {
3
+ ttl: number;
4
+ opn: number;
5
+ rt1Ms: number | null;
6
+ rt5Ms: number | null;
7
+ p50Ms: number | null;
8
+ p90Ms: number | null;
9
+ };
10
+ export declare class TunnelStats {
11
+ private totalConnections;
12
+ private openConnections;
13
+ private readonly latencySamples;
14
+ private sampleStartIndex;
15
+ updateConnections(snapshot: ProxyConnectionSnapshot): void;
16
+ recordRequest(event: Pick<ProxyRequestEvent, 'startedAtEpochMs' | 'durationMs'>): void;
17
+ getSnapshot(nowEpochMs?: number): TunnelStatsSnapshot;
18
+ private prune;
19
+ }
@@ -0,0 +1,75 @@
1
+ const ONE_MINUTE_MS = 60_000;
2
+ const FIVE_MINUTES_MS = 300_000;
3
+ function average(values) {
4
+ if (values.length === 0) {
5
+ return null;
6
+ }
7
+ const total = values.reduce((sum, value) => sum + value, 0);
8
+ return total / values.length;
9
+ }
10
+ function percentile(sortedValues, rank) {
11
+ if (sortedValues.length === 0) {
12
+ return null;
13
+ }
14
+ const index = Math.max(0, Math.ceil(sortedValues.length * rank) - 1);
15
+ return sortedValues[index] ?? null;
16
+ }
17
+ export class TunnelStats {
18
+ totalConnections = 0;
19
+ openConnections = 0;
20
+ latencySamples = [];
21
+ sampleStartIndex = 0;
22
+ updateConnections(snapshot) {
23
+ this.totalConnections = snapshot.totalConnections;
24
+ this.openConnections = snapshot.openConnections;
25
+ }
26
+ recordRequest(event) {
27
+ this.latencySamples.push({
28
+ timestampEpochMs: event.startedAtEpochMs,
29
+ durationMs: Math.max(0, event.durationMs),
30
+ });
31
+ this.prune(event.startedAtEpochMs);
32
+ }
33
+ getSnapshot(nowEpochMs = Date.now()) {
34
+ this.prune(nowEpochMs);
35
+ const oneMinuteCutoff = nowEpochMs - ONE_MINUTE_MS;
36
+ const durationsIn1m = [];
37
+ const durationsIn5m = [];
38
+ for (let i = this.sampleStartIndex; i < this.latencySamples.length; i += 1) {
39
+ const sample = this.latencySamples[i];
40
+ if (!sample) {
41
+ continue;
42
+ }
43
+ durationsIn5m.push(sample.durationMs);
44
+ if (sample.timestampEpochMs >= oneMinuteCutoff) {
45
+ durationsIn1m.push(sample.durationMs);
46
+ }
47
+ }
48
+ const sortedIn5m = [...durationsIn5m].sort((a, b) => a - b);
49
+ return {
50
+ ttl: this.totalConnections,
51
+ opn: this.openConnections,
52
+ rt1Ms: average(durationsIn1m),
53
+ rt5Ms: average(durationsIn5m),
54
+ p50Ms: percentile(sortedIn5m, 0.5),
55
+ p90Ms: percentile(sortedIn5m, 0.9),
56
+ };
57
+ }
58
+ prune(nowEpochMs) {
59
+ const oldestAllowed = nowEpochMs - FIVE_MINUTES_MS;
60
+ while (this.sampleStartIndex < this.latencySamples.length) {
61
+ const sample = this.latencySamples[this.sampleStartIndex];
62
+ if (!sample || sample.timestampEpochMs >= oldestAllowed) {
63
+ break;
64
+ }
65
+ this.sampleStartIndex += 1;
66
+ }
67
+ const shouldCompact = this.sampleStartIndex > 0 &&
68
+ (this.sampleStartIndex >= 1024 || this.sampleStartIndex > Math.floor(this.latencySamples.length / 2));
69
+ if (shouldCompact) {
70
+ this.latencySamples.splice(0, this.sampleStartIndex);
71
+ this.sampleStartIndex = 0;
72
+ }
73
+ }
74
+ }
75
+ //# sourceMappingURL=tunnel-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-stats.js","sourceRoot":"","sources":["../../src/lib/tunnel-stats.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};