@linkshell/gateway 0.3.9 → 0.4.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/Dockerfile +1 -3
- package/README.md +13 -14
- package/dist/gateway/src/agent-permission-http.d.ts +74 -19
- package/dist/gateway/src/agent-permission-http.js +56 -16
- package/dist/gateway/src/agent-permission-http.js.map +1 -1
- package/dist/gateway/src/embedded.js +61 -153
- package/dist/gateway/src/embedded.js.map +1 -1
- package/dist/gateway/src/index.js +98 -193
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/pairings.d.ts +3 -3
- package/dist/gateway/src/pairings.js +5 -4
- package/dist/gateway/src/pairings.js.map +1 -1
- package/dist/gateway/src/relay.d.ts +2 -2
- package/dist/gateway/src/relay.js +85 -161
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/sessions.d.ts +28 -42
- package/dist/gateway/src/sessions.js +145 -200
- package/dist/gateway/src/sessions.js.map +1 -1
- package/dist/gateway/src/state-store.d.ts +6 -9
- package/dist/gateway/src/state-store.js +19 -26
- package/dist/gateway/src/state-store.js.map +1 -1
- package/dist/gateway/src/tokens.d.ts +7 -27
- package/dist/gateway/src/tokens.js +60 -86
- package/dist/gateway/src/tokens.js.map +1 -1
- package/dist/gateway/src/tunnel.d.ts +13 -11
- package/dist/gateway/src/tunnel.js +36 -36
- package/dist/gateway/src/tunnel.js.map +1 -1
- package/dist/gateway/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +11978 -3423
- package/dist/shared-protocol/src/index.js +114 -163
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +11 -11
- package/src/agent-permission-http.ts +63 -20
- package/src/embedded.ts +60 -158
- package/src/index.ts +98 -199
- package/src/pairings.ts +7 -6
- package/src/relay.ts +97 -193
- package/src/sessions.ts +150 -213
- package/src/state-store.ts +25 -41
- package/src/tokens.ts +63 -109
- package/src/tunnel.ts +43 -49
package/src/tokens.ts
CHANGED
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { GatewayStateStore } from "./state-store.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
hostDeviceId: string;
|
|
7
|
-
clientDeviceId: string | undefined;
|
|
8
|
-
clientName: string | undefined;
|
|
9
|
-
createdAt: number;
|
|
10
|
-
lastUsedAt: number;
|
|
11
|
-
}
|
|
4
|
+
const CLEANUP_INTERVAL = 5 * 60_000;
|
|
5
|
+
const SESSION_TTL = 7 * 24 * 60 * 60_000; // 7 days — prune stale bindings
|
|
12
6
|
|
|
13
7
|
interface TokenRecord {
|
|
14
8
|
token: string;
|
|
15
|
-
|
|
9
|
+
sessionIds: Set<string>;
|
|
16
10
|
createdAt: number;
|
|
17
11
|
lastUsedAt: number;
|
|
18
12
|
}
|
|
19
13
|
|
|
20
|
-
export class
|
|
14
|
+
export class TokenManager {
|
|
21
15
|
private tokens = new Map<string, TokenRecord>();
|
|
22
|
-
private
|
|
16
|
+
private sessionToToken = new Map<string, string>();
|
|
17
|
+
private cleanupTimer: ReturnType<typeof setInterval>;
|
|
23
18
|
|
|
24
|
-
constructor(private readonly store?: GatewayStateStore) {
|
|
19
|
+
constructor(private readonly store?: GatewayStateStore) {
|
|
20
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
|
|
21
|
+
}
|
|
25
22
|
|
|
26
23
|
async hydrate(): Promise<void> {
|
|
27
24
|
if (!this.store) return;
|
|
28
25
|
try {
|
|
29
|
-
const records = await this.store.
|
|
26
|
+
const records = await this.store.loadTokens();
|
|
27
|
+
const now = Date.now();
|
|
30
28
|
for (const record of records) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
if (now - record.lastUsedAt > SESSION_TTL) {
|
|
30
|
+
void this.store.deleteToken(record.token).catch(() => {});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
this.tokens.set(record.token, {
|
|
34
|
+
token: record.token,
|
|
35
|
+
sessionIds: new Set(record.sessionIds),
|
|
36
36
|
createdAt: record.createdAt,
|
|
37
37
|
lastUsedAt: record.lastUsedAt,
|
|
38
|
-
persist: false,
|
|
39
38
|
});
|
|
39
|
+
for (const sessionId of record.sessionIds) {
|
|
40
|
+
this.sessionToToken.set(sessionId, record.token);
|
|
41
|
+
}
|
|
40
42
|
}
|
|
41
43
|
} catch (err) {
|
|
42
|
-
process.stderr.write(`[gateway]
|
|
44
|
+
process.stderr.write(`[gateway] token store hydrate failed, using memory only: ${err}\n`);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -47,131 +49,83 @@ export class AuthorizationManager {
|
|
|
47
49
|
if (deviceToken && this.tokens.has(deviceToken)) {
|
|
48
50
|
const record = this.tokens.get(deviceToken)!;
|
|
49
51
|
record.lastUsedAt = Date.now();
|
|
52
|
+
this.persist(record);
|
|
50
53
|
return deviceToken;
|
|
51
54
|
}
|
|
52
55
|
const token = deviceToken || randomUUID();
|
|
53
56
|
this.tokens.set(token, {
|
|
54
57
|
token,
|
|
55
|
-
|
|
58
|
+
sessionIds: new Set(),
|
|
56
59
|
createdAt: Date.now(),
|
|
57
60
|
lastUsedAt: Date.now(),
|
|
58
61
|
});
|
|
62
|
+
this.persist(this.tokens.get(token)!);
|
|
59
63
|
return token;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
token: string,
|
|
64
|
-
hostDeviceId: string,
|
|
65
|
-
input: {
|
|
66
|
-
authorizationId?: string;
|
|
67
|
-
clientDeviceId?: string;
|
|
68
|
-
clientName?: string;
|
|
69
|
-
createdAt?: number;
|
|
70
|
-
lastUsedAt?: number;
|
|
71
|
-
persist?: boolean;
|
|
72
|
-
} = {},
|
|
73
|
-
): DeviceAuthorization | undefined {
|
|
74
|
-
const record = this.tokens.get(token);
|
|
75
|
-
if (!record) return undefined;
|
|
76
|
-
const existing = record.authorizations.get(hostDeviceId);
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
const authorization: DeviceAuthorization = {
|
|
79
|
-
authorizationId: input.authorizationId ?? existing?.authorizationId ?? randomUUID(),
|
|
80
|
-
hostDeviceId,
|
|
81
|
-
clientDeviceId: input.clientDeviceId ?? existing?.clientDeviceId,
|
|
82
|
-
clientName: input.clientName ?? existing?.clientName,
|
|
83
|
-
createdAt: input.createdAt ?? existing?.createdAt ?? now,
|
|
84
|
-
lastUsedAt: input.lastUsedAt ?? now,
|
|
85
|
-
};
|
|
86
|
-
record.authorizations.set(hostDeviceId, authorization);
|
|
87
|
-
record.lastUsedAt = now;
|
|
88
|
-
let tokens = this.hostDeviceToTokens.get(hostDeviceId);
|
|
89
|
-
if (!tokens) {
|
|
90
|
-
tokens = new Set();
|
|
91
|
-
this.hostDeviceToTokens.set(hostDeviceId, tokens);
|
|
92
|
-
}
|
|
93
|
-
tokens.add(token);
|
|
94
|
-
if (input.persist !== false) {
|
|
95
|
-
this.persist(token, authorization);
|
|
96
|
-
}
|
|
97
|
-
return authorization;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
validate(token: string): boolean {
|
|
66
|
+
bind(token: string, sessionId: string): boolean {
|
|
101
67
|
const record = this.tokens.get(token);
|
|
102
68
|
if (!record) return false;
|
|
69
|
+
record.sessionIds.add(sessionId);
|
|
103
70
|
record.lastUsedAt = Date.now();
|
|
71
|
+
this.sessionToToken.set(sessionId, token);
|
|
72
|
+
this.persist(record);
|
|
104
73
|
return true;
|
|
105
74
|
}
|
|
106
75
|
|
|
107
|
-
|
|
76
|
+
validate(token: string): boolean {
|
|
108
77
|
const record = this.tokens.get(token);
|
|
109
78
|
if (!record) return false;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const now = Date.now();
|
|
113
|
-
record.lastUsedAt = now;
|
|
114
|
-
authorization.lastUsedAt = now;
|
|
115
|
-
this.persist(token, authorization);
|
|
79
|
+
record.lastUsedAt = Date.now();
|
|
80
|
+
this.persist(record);
|
|
116
81
|
return true;
|
|
117
82
|
}
|
|
118
83
|
|
|
119
|
-
|
|
84
|
+
owns(token: string, sessionId: string): boolean {
|
|
120
85
|
const record = this.tokens.get(token);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
record.authorizations.delete(hostDeviceId);
|
|
126
|
-
const tokens = this.hostDeviceToTokens.get(hostDeviceId);
|
|
127
|
-
tokens?.delete(token);
|
|
128
|
-
if (tokens && tokens.size === 0) {
|
|
129
|
-
this.hostDeviceToTokens.delete(hostDeviceId);
|
|
130
|
-
}
|
|
131
|
-
void this.store?.deleteAuthorization(authorizationId).catch((err) => {
|
|
132
|
-
process.stderr.write(`[gateway] authorization store delete failed: ${err}\n`);
|
|
133
|
-
});
|
|
134
|
-
return true;
|
|
86
|
+
if (!record) return false;
|
|
87
|
+
record.lastUsedAt = Date.now();
|
|
88
|
+
this.persist(record);
|
|
89
|
+
return record.sessionIds.has(sessionId);
|
|
135
90
|
}
|
|
136
91
|
|
|
137
|
-
|
|
92
|
+
getSessionIds(token: string): Set<string> {
|
|
138
93
|
const record = this.tokens.get(token);
|
|
139
94
|
if (!record) return new Set();
|
|
140
95
|
record.lastUsedAt = Date.now();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
getSessionIds(token: string): Set<string> {
|
|
145
|
-
return this.getHostDeviceIds(token);
|
|
96
|
+
this.persist(record);
|
|
97
|
+
return record.sessionIds;
|
|
146
98
|
}
|
|
147
99
|
|
|
148
|
-
|
|
149
|
-
return this.
|
|
100
|
+
getTokenForSession(sessionId: string): string | undefined {
|
|
101
|
+
return this.sessionToToken.get(sessionId);
|
|
150
102
|
}
|
|
151
103
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
104
|
+
private cleanup(): void {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
for (const [token, record] of this.tokens) {
|
|
107
|
+
if (now - record.lastUsedAt > SESSION_TTL) {
|
|
108
|
+
for (const sid of record.sessionIds) {
|
|
109
|
+
this.sessionToToken.delete(sid);
|
|
110
|
+
}
|
|
111
|
+
this.tokens.delete(token);
|
|
112
|
+
void this.store?.deleteToken(token).catch(() => {});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
158
115
|
}
|
|
159
116
|
|
|
160
|
-
private persist(
|
|
161
|
-
void this.store?.
|
|
162
|
-
token,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
clientName: authorization.clientName,
|
|
167
|
-
createdAt: authorization.createdAt,
|
|
168
|
-
lastUsedAt: authorization.lastUsedAt,
|
|
117
|
+
private persist(record: TokenRecord): void {
|
|
118
|
+
void this.store?.saveToken({
|
|
119
|
+
token: record.token,
|
|
120
|
+
sessionIds: [...record.sessionIds],
|
|
121
|
+
createdAt: record.createdAt,
|
|
122
|
+
lastUsedAt: record.lastUsedAt,
|
|
169
123
|
}).catch((err) => {
|
|
170
|
-
process.stderr.write(`[gateway]
|
|
124
|
+
process.stderr.write(`[gateway] token store save failed: ${err}\n`);
|
|
171
125
|
});
|
|
172
126
|
}
|
|
173
127
|
|
|
174
|
-
destroy(): void {
|
|
128
|
+
destroy(): void {
|
|
129
|
+
clearInterval(this.cleanupTimer);
|
|
130
|
+
}
|
|
175
131
|
}
|
|
176
|
-
|
|
177
|
-
export class TokenManager extends AuthorizationManager {}
|
package/src/tunnel.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createEnvelope,
|
|
6
6
|
serializeEnvelope,
|
|
7
7
|
} from "@linkshell/protocol";
|
|
8
|
-
import type {
|
|
8
|
+
import type { SessionManager } from "./sessions.js";
|
|
9
9
|
import type { TokenManager } from "./tokens.js";
|
|
10
10
|
import { AUTH_REQUIRED } from "./auth-middleware.js";
|
|
11
11
|
|
|
@@ -26,23 +26,23 @@ export interface PendingTunnelWs {
|
|
|
26
26
|
const pendingRequests = new Map<string, PendingTunnelRequest>();
|
|
27
27
|
const pendingWsSockets = new Map<string, PendingTunnelWs>();
|
|
28
28
|
|
|
29
|
-
// Track requestIds per
|
|
30
|
-
const
|
|
29
|
+
// Track requestIds per session for cleanup on host disconnect
|
|
30
|
+
const sessionRequests = new Map<string, Set<string>>();
|
|
31
31
|
|
|
32
|
-
function trackRequest(
|
|
33
|
-
let set =
|
|
32
|
+
function trackRequest(sessionId: string, requestId: string): void {
|
|
33
|
+
let set = sessionRequests.get(sessionId);
|
|
34
34
|
if (!set) {
|
|
35
35
|
set = new Set();
|
|
36
|
-
|
|
36
|
+
sessionRequests.set(sessionId, set);
|
|
37
37
|
}
|
|
38
38
|
set.add(requestId);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function untrackRequest(
|
|
42
|
-
const set =
|
|
41
|
+
function untrackRequest(sessionId: string, requestId: string): void {
|
|
42
|
+
const set = sessionRequests.get(sessionId);
|
|
43
43
|
if (set) {
|
|
44
44
|
set.delete(requestId);
|
|
45
|
-
if (set.size === 0)
|
|
45
|
+
if (set.size === 0) sessionRequests.delete(sessionId);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -65,19 +65,19 @@ function extractToken(req: IncomingMessage, url: URL): string | null {
|
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** Parse lsh_tunnel cookie: "
|
|
69
|
-
export function parseTunnelCookie(req: IncomingMessage): {
|
|
68
|
+
/** Parse lsh_tunnel cookie: "sessionId:port:token" */
|
|
69
|
+
export function parseTunnelCookie(req: IncomingMessage): { sessionId: string; port: number; token: string } | null {
|
|
70
70
|
const cookie = req.headers.cookie;
|
|
71
71
|
if (!cookie) return null;
|
|
72
72
|
const match = cookie.match(/lsh_tunnel=([^;]+)/);
|
|
73
73
|
if (!match?.[1]) return null;
|
|
74
74
|
const parts = decodeURIComponent(match[1]).split(":");
|
|
75
75
|
if (parts.length < 3) return null;
|
|
76
|
-
const
|
|
76
|
+
const sessionId = parts[0]!;
|
|
77
77
|
const port = Number(parts[1]);
|
|
78
78
|
const token = parts.slice(2).join(":"); // token may contain colons
|
|
79
|
-
if (!
|
|
80
|
-
return {
|
|
79
|
+
if (!sessionId || isNaN(port) || port < 1 || port > 65535 || !token) return null;
|
|
80
|
+
return { sessionId, port, token };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function errorResponse(res: ServerResponse, status: number, message: string): void {
|
|
@@ -89,38 +89,32 @@ function errorResponse(res: ServerResponse, status: number, message: string): vo
|
|
|
89
89
|
res.end(message);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function parseTunnelPath(pathname: string): {
|
|
92
|
+
export function parseTunnelPath(pathname: string): { sessionId: string; port: number; path: string } | null {
|
|
93
93
|
const match = pathname.match(/^\/tunnel\/([^/]+)\/(\d+)(\/.*)?$/);
|
|
94
94
|
if (!match) return null;
|
|
95
95
|
const port = Number(match[2]);
|
|
96
96
|
if (port < 1 || port > 65535) return null;
|
|
97
97
|
return {
|
|
98
|
-
|
|
98
|
+
sessionId: match[1]!,
|
|
99
99
|
port,
|
|
100
100
|
path: match[3] || "/",
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
type ParsedTunnelTarget = {
|
|
105
|
-
hostDeviceId: string;
|
|
106
|
-
port: number;
|
|
107
|
-
path: string;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
104
|
export async function handleTunnelRequest(
|
|
111
105
|
req: IncomingMessage,
|
|
112
106
|
res: ServerResponse,
|
|
113
|
-
sessions:
|
|
107
|
+
sessions: SessionManager,
|
|
114
108
|
tokens: TokenManager,
|
|
115
|
-
parsed:
|
|
109
|
+
parsed: { sessionId: string; port: number; path: string },
|
|
116
110
|
url: URL,
|
|
117
111
|
preAuthToken?: string,
|
|
118
112
|
): Promise<void> {
|
|
119
|
-
const {
|
|
113
|
+
const { sessionId, port, path } = parsed;
|
|
120
114
|
|
|
121
115
|
// Auth: device token OR Supabase JWT (userId owns session)
|
|
122
116
|
const token = preAuthToken || extractToken(req, url);
|
|
123
|
-
const tokenOwns = token && tokens.owns(token,
|
|
117
|
+
const tokenOwns = token && tokens.owns(token, sessionId);
|
|
124
118
|
let authOwns = false;
|
|
125
119
|
let authJwt: string | null = null;
|
|
126
120
|
if (!tokenOwns && AUTH_REQUIRED) {
|
|
@@ -141,7 +135,7 @@ export async function handleTunnelRequest(
|
|
|
141
135
|
});
|
|
142
136
|
if (userRes.ok) {
|
|
143
137
|
const user = (await userRes.json()) as { id: string };
|
|
144
|
-
const session = sessions.get(
|
|
138
|
+
const session = sessions.get(sessionId);
|
|
145
139
|
if (user.id && session?.userId && user.id === session.userId) {
|
|
146
140
|
authOwns = true;
|
|
147
141
|
authJwt = jwtCandidate;
|
|
@@ -159,12 +153,12 @@ export async function handleTunnelRequest(
|
|
|
159
153
|
// Set auth cookie for subsequent sub-resource requests
|
|
160
154
|
const cookieToken = tokenOwns ? token : authJwt;
|
|
161
155
|
if (cookieToken) {
|
|
162
|
-
const cookieVal = encodeURIComponent(`${
|
|
156
|
+
const cookieVal = encodeURIComponent(`${sessionId}:${port}:${cookieToken}`);
|
|
163
157
|
res.setHeader("Set-Cookie", `lsh_tunnel=${cookieVal}; Path=/; HttpOnly; SameSite=Lax`);
|
|
164
158
|
}
|
|
165
159
|
|
|
166
160
|
// Validate session & host
|
|
167
|
-
const session = sessions.get(
|
|
161
|
+
const session = sessions.get(sessionId);
|
|
168
162
|
if (!session || !session.host || session.host.socket.readyState !== session.host.socket.OPEN) {
|
|
169
163
|
errorResponse(res, 502, "Host not connected");
|
|
170
164
|
return;
|
|
@@ -210,17 +204,17 @@ export async function handleTunnelRequest(
|
|
|
210
204
|
headersSent: false,
|
|
211
205
|
timeout: setTimeout(() => {
|
|
212
206
|
pendingRequests.delete(requestId);
|
|
213
|
-
untrackRequest(
|
|
207
|
+
untrackRequest(sessionId, requestId);
|
|
214
208
|
errorResponse(res, 504, "Tunnel request timed out");
|
|
215
209
|
}, TUNNEL_TIMEOUT),
|
|
216
210
|
};
|
|
217
211
|
pendingRequests.set(requestId, pending);
|
|
218
|
-
trackRequest(
|
|
212
|
+
trackRequest(sessionId, requestId);
|
|
219
213
|
|
|
220
214
|
// Send tunnel.request to host
|
|
221
215
|
const envelope = createEnvelope({
|
|
222
216
|
type: "tunnel.request",
|
|
223
|
-
|
|
217
|
+
sessionId,
|
|
224
218
|
payload: {
|
|
225
219
|
requestId,
|
|
226
220
|
method,
|
|
@@ -238,7 +232,7 @@ export async function handleTunnelRequest(
|
|
|
238
232
|
if (p) {
|
|
239
233
|
clearTimeout(p.timeout);
|
|
240
234
|
pendingRequests.delete(requestId);
|
|
241
|
-
untrackRequest(
|
|
235
|
+
untrackRequest(sessionId, requestId);
|
|
242
236
|
}
|
|
243
237
|
});
|
|
244
238
|
}
|
|
@@ -312,8 +306,8 @@ export function removeTunnelWs(requestId: string): void {
|
|
|
312
306
|
pendingWsSockets.delete(requestId);
|
|
313
307
|
}
|
|
314
308
|
|
|
315
|
-
export function cleanupSessionTunnels(
|
|
316
|
-
const requestIds =
|
|
309
|
+
export function cleanupSessionTunnels(sessionId: string): void {
|
|
310
|
+
const requestIds = sessionRequests.get(sessionId);
|
|
317
311
|
if (!requestIds) return;
|
|
318
312
|
for (const rid of requestIds) {
|
|
319
313
|
const pending = pendingRequests.get(rid);
|
|
@@ -328,21 +322,21 @@ export function cleanupSessionTunnels(hostDeviceId: string): void {
|
|
|
328
322
|
pendingWsSockets.delete(rid);
|
|
329
323
|
}
|
|
330
324
|
}
|
|
331
|
-
|
|
325
|
+
sessionRequests.delete(sessionId);
|
|
332
326
|
}
|
|
333
327
|
|
|
334
328
|
export async function handleTunnelWsUpgrade(
|
|
335
329
|
ws: WebSocket,
|
|
336
|
-
parsed:
|
|
330
|
+
parsed: { sessionId: string; port: number; path: string },
|
|
337
331
|
url: URL,
|
|
338
|
-
sessions:
|
|
332
|
+
sessions: SessionManager,
|
|
339
333
|
tokens: TokenManager,
|
|
340
334
|
): Promise<void> {
|
|
341
|
-
const {
|
|
335
|
+
const { sessionId, port, path } = parsed;
|
|
342
336
|
|
|
343
337
|
// Auth: device token OR Supabase JWT (userId owns session)
|
|
344
338
|
const token = url.searchParams.get("token");
|
|
345
|
-
const tokenOwns = token && tokens.owns(token,
|
|
339
|
+
const tokenOwns = token && tokens.owns(token, sessionId);
|
|
346
340
|
let authOwns = false;
|
|
347
341
|
if (!tokenOwns && AUTH_REQUIRED) {
|
|
348
342
|
// Try auth_token param first, then fall back to token param (cookie fallback stores JWT there)
|
|
@@ -358,7 +352,7 @@ export async function handleTunnelWsUpgrade(
|
|
|
358
352
|
});
|
|
359
353
|
if (userRes.ok) {
|
|
360
354
|
const user = (await userRes.json()) as { id: string };
|
|
361
|
-
const session = sessions.get(
|
|
355
|
+
const session = sessions.get(sessionId);
|
|
362
356
|
if (user.id && session?.userId && user.id === session.userId) {
|
|
363
357
|
authOwns = true;
|
|
364
358
|
}
|
|
@@ -372,7 +366,7 @@ export async function handleTunnelWsUpgrade(
|
|
|
372
366
|
return;
|
|
373
367
|
}
|
|
374
368
|
|
|
375
|
-
const session = sessions.get(
|
|
369
|
+
const session = sessions.get(sessionId);
|
|
376
370
|
if (!session || !session.host || session.host.socket.readyState !== session.host.socket.OPEN) {
|
|
377
371
|
ws.close(4002, "Host not connected");
|
|
378
372
|
return;
|
|
@@ -383,12 +377,12 @@ export async function handleTunnelWsUpgrade(
|
|
|
383
377
|
|
|
384
378
|
// Register this WS so host responses route here
|
|
385
379
|
registerTunnelWs(requestId, ws);
|
|
386
|
-
trackRequest(
|
|
380
|
+
trackRequest(sessionId, requestId);
|
|
387
381
|
|
|
388
382
|
// Send tunnel.request with upgrade header to host
|
|
389
383
|
const envelope = createEnvelope({
|
|
390
384
|
type: "tunnel.request",
|
|
391
|
-
|
|
385
|
+
sessionId,
|
|
392
386
|
payload: {
|
|
393
387
|
requestId,
|
|
394
388
|
method: "GET",
|
|
@@ -403,13 +397,13 @@ export async function handleTunnelWsUpgrade(
|
|
|
403
397
|
// Forward data from browser WS to host
|
|
404
398
|
ws.on("message", (data: Buffer | string) => {
|
|
405
399
|
try {
|
|
406
|
-
const s = sessions.get(
|
|
400
|
+
const s = sessions.get(sessionId);
|
|
407
401
|
if (!s?.host || s.host.socket.readyState !== s.host.socket.OPEN) return;
|
|
408
402
|
const isBinary = typeof data !== "string";
|
|
409
403
|
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
410
404
|
const fwd = createEnvelope({
|
|
411
405
|
type: "tunnel.ws.data",
|
|
412
|
-
|
|
406
|
+
sessionId,
|
|
413
407
|
payload: {
|
|
414
408
|
requestId,
|
|
415
409
|
data: buf.toString("base64"),
|
|
@@ -423,13 +417,13 @@ export async function handleTunnelWsUpgrade(
|
|
|
423
417
|
ws.on("close", (code, reason) => {
|
|
424
418
|
try {
|
|
425
419
|
removeTunnelWs(requestId);
|
|
426
|
-
untrackRequest(
|
|
427
|
-
const s = sessions.get(
|
|
420
|
+
untrackRequest(sessionId, requestId);
|
|
421
|
+
const s = sessions.get(sessionId);
|
|
428
422
|
if (!s?.host || s.host.socket.readyState !== s.host.socket.OPEN) return;
|
|
429
423
|
const safeCode = typeof code === "number" && code >= 1000 && code <= 4999 ? code : 1000;
|
|
430
424
|
const fwd = createEnvelope({
|
|
431
425
|
type: "tunnel.ws.close",
|
|
432
|
-
|
|
426
|
+
sessionId,
|
|
433
427
|
payload: {
|
|
434
428
|
requestId,
|
|
435
429
|
code: safeCode,
|