@linkshell/gateway 0.2.47 → 0.3.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 +14 -13
- package/dist/gateway/src/agent-permission-http.d.ts +10 -37
- package/dist/gateway/src/agent-permission-http.js +16 -56
- package/dist/gateway/src/agent-permission-http.js.map +1 -1
- package/dist/gateway/src/embedded.js +121 -57
- package/dist/gateway/src/embedded.js.map +1 -1
- package/dist/gateway/src/index.js +161 -94
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/pairings.d.ts +3 -3
- package/dist/gateway/src/pairings.js +4 -5
- package/dist/gateway/src/pairings.js.map +1 -1
- package/dist/gateway/src/relay.d.ts +2 -2
- package/dist/gateway/src/relay.js +27 -38
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/sessions.d.ts +31 -28
- package/dist/gateway/src/sessions.js +163 -145
- package/dist/gateway/src/sessions.js.map +1 -1
- package/dist/gateway/src/state-store.d.ts +9 -6
- package/dist/gateway/src/state-store.js +26 -19
- package/dist/gateway/src/state-store.js.map +1 -1
- package/dist/gateway/src/tokens.d.ts +27 -7
- package/dist/gateway/src/tokens.js +86 -60
- package/dist/gateway/src/tokens.js.map +1 -1
- package/dist/gateway/src/tunnel.d.ts +11 -13
- 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 +3961 -5788
- package/dist/shared-protocol/src/index.js +19 -84
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +10 -10
- package/src/agent-permission-http.ts +20 -63
- package/src/embedded.ts +124 -56
- package/src/index.ts +165 -94
- package/src/pairings.ts +6 -7
- package/src/relay.ts +38 -48
- package/src/sessions.ts +174 -150
- package/src/state-store.ts +41 -25
- package/src/tokens.ts +109 -63
- package/src/tunnel.ts +49 -43
package/src/sessions.ts
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
import type WebSocket from "ws";
|
|
2
2
|
import type { Envelope } from "@linkshell/protocol";
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type DeviceState = "active" | "host_disconnected" | "terminated";
|
|
5
5
|
|
|
6
6
|
export interface ConnectedDevice {
|
|
7
7
|
socket: WebSocket;
|
|
8
8
|
role: "host" | "client";
|
|
9
9
|
deviceId: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
authorizationId?: string;
|
|
10
12
|
connectedAt: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export interface
|
|
15
|
+
export interface HostDevice {
|
|
14
16
|
id: string;
|
|
15
|
-
|
|
17
|
+
hostDeviceId: string;
|
|
18
|
+
state: DeviceState;
|
|
16
19
|
host: ConnectedDevice | undefined;
|
|
17
20
|
clients: Map<string, ConnectedDevice>;
|
|
18
21
|
controllerId: string | undefined;
|
|
19
22
|
lastActivity: number;
|
|
20
23
|
createdAt: number;
|
|
21
|
-
outputBuffers: Map<string, Envelope[]>;
|
|
22
|
-
lastStatusByTerminal: Map<string, Envelope>;
|
|
24
|
+
outputBuffers: Map<string, Envelope[]>;
|
|
25
|
+
lastStatusByTerminal: Map<string, Envelope>;
|
|
23
26
|
hostDisconnectedAt: number | undefined;
|
|
24
|
-
// Metadata from host's session.connect
|
|
25
|
-
provider: string | undefined;
|
|
26
27
|
machineId: string | undefined;
|
|
27
28
|
hostname: string | undefined;
|
|
28
29
|
platform: string | undefined;
|
|
29
30
|
cwd: string | undefined;
|
|
30
|
-
|
|
31
|
-
// Auth: user who owns this session (set on AUTH_REQUIRED gateways)
|
|
31
|
+
capabilities: string[];
|
|
32
32
|
userId: string | undefined;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const OUTPUT_BUFFER_CAPACITY = 200;
|
|
36
|
-
const HOST_RECONNECT_WINDOW = 60_000;
|
|
36
|
+
const HOST_RECONNECT_WINDOW = 60_000;
|
|
37
37
|
const CLEANUP_INTERVAL = 30_000;
|
|
38
38
|
|
|
39
|
-
export class
|
|
40
|
-
private
|
|
39
|
+
export class DeviceManager {
|
|
40
|
+
private devices = new Map<string, HostDevice>();
|
|
41
41
|
private cleanupTimer: ReturnType<typeof setInterval>;
|
|
42
42
|
|
|
43
43
|
constructor() {
|
|
44
44
|
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
getOrCreate(
|
|
48
|
-
let
|
|
49
|
-
if (!
|
|
50
|
-
|
|
51
|
-
id:
|
|
47
|
+
getOrCreate(hostDeviceId: string): HostDevice {
|
|
48
|
+
let device = this.devices.get(hostDeviceId);
|
|
49
|
+
if (!device) {
|
|
50
|
+
device = {
|
|
51
|
+
id: hostDeviceId,
|
|
52
|
+
hostDeviceId,
|
|
52
53
|
state: "active",
|
|
53
54
|
host: undefined,
|
|
54
55
|
clients: new Map(),
|
|
@@ -58,119 +59,142 @@ export class SessionManager {
|
|
|
58
59
|
outputBuffers: new Map(),
|
|
59
60
|
lastStatusByTerminal: new Map(),
|
|
60
61
|
hostDisconnectedAt: undefined,
|
|
61
|
-
provider: undefined,
|
|
62
62
|
machineId: undefined,
|
|
63
63
|
hostname: undefined,
|
|
64
64
|
platform: undefined,
|
|
65
65
|
cwd: undefined,
|
|
66
|
-
|
|
66
|
+
capabilities: [],
|
|
67
67
|
userId: undefined,
|
|
68
68
|
};
|
|
69
|
-
this.
|
|
69
|
+
this.devices.set(hostDeviceId, device);
|
|
70
70
|
}
|
|
71
|
-
return
|
|
71
|
+
return device;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
get(
|
|
75
|
-
return this.
|
|
74
|
+
get(hostDeviceId: string): HostDevice | undefined {
|
|
75
|
+
return this.devices.get(hostDeviceId);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
setHost(
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
setHost(hostDeviceId: string, socketDevice: ConnectedDevice): void {
|
|
79
|
+
const device = this.getOrCreate(hostDeviceId);
|
|
80
|
+
device.host = socketDevice;
|
|
81
|
+
device.state = "active";
|
|
82
|
+
device.hostDisconnectedAt = undefined;
|
|
83
|
+
device.lastActivity = Date.now();
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
addClient(
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
if (!
|
|
90
|
-
|
|
86
|
+
addClient(hostDeviceId: string, socketDevice: ConnectedDevice): void {
|
|
87
|
+
const device = this.getOrCreate(hostDeviceId);
|
|
88
|
+
device.clients.set(socketDevice.deviceId, socketDevice);
|
|
89
|
+
if (!device.controllerId) {
|
|
90
|
+
device.controllerId = socketDevice.deviceId;
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
device.lastActivity = Date.now();
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
removeHost(
|
|
96
|
-
|
|
96
|
+
hostDeviceId: string,
|
|
97
97
|
): { clients: Map<string, ConnectedDevice> } | undefined {
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return { clients:
|
|
98
|
+
const device = this.devices.get(hostDeviceId);
|
|
99
|
+
if (!device) return undefined;
|
|
100
|
+
device.host = undefined;
|
|
101
|
+
device.state = "host_disconnected";
|
|
102
|
+
device.hostDisconnectedAt = Date.now();
|
|
103
|
+
return { clients: device.clients };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
removeClient(
|
|
107
|
-
const
|
|
108
|
-
if (!
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
session.controllerId = next.done ? undefined : next.value;
|
|
106
|
+
removeClient(hostDeviceId: string, deviceId: string): void {
|
|
107
|
+
const device = this.devices.get(hostDeviceId);
|
|
108
|
+
if (!device) return;
|
|
109
|
+
device.clients.delete(deviceId);
|
|
110
|
+
if (device.controllerId === deviceId) {
|
|
111
|
+
const next = device.clients.keys().next();
|
|
112
|
+
device.controllerId = next.done ? undefined : next.value;
|
|
114
113
|
}
|
|
115
|
-
this.maybeDelete(
|
|
114
|
+
this.maybeDelete(hostDeviceId);
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
disconnectAuthorization(hostDeviceId: string, authorizationId: string): number {
|
|
118
|
+
const device = this.devices.get(hostDeviceId);
|
|
119
|
+
if (!device) return 0;
|
|
120
|
+
let closed = 0;
|
|
121
|
+
for (const [deviceId, client] of device.clients) {
|
|
122
|
+
if (client.authorizationId !== authorizationId) continue;
|
|
123
|
+
try {
|
|
124
|
+
client.socket.close(4001, "authorization revoked");
|
|
125
|
+
} catch {}
|
|
126
|
+
device.clients.delete(deviceId);
|
|
127
|
+
closed++;
|
|
124
128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
if (device.controllerId && !device.clients.has(device.controllerId)) {
|
|
130
|
+
const next = device.clients.keys().next();
|
|
131
|
+
device.controllerId = next.done ? undefined : next.value;
|
|
127
132
|
}
|
|
128
|
-
this.
|
|
133
|
+
this.maybeDelete(hostDeviceId);
|
|
134
|
+
return closed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
forceDelete(hostDeviceId: string): boolean {
|
|
138
|
+
const device = this.devices.get(hostDeviceId);
|
|
139
|
+
if (!device) return false;
|
|
140
|
+
if (device.host) {
|
|
141
|
+
try {
|
|
142
|
+
device.host.socket.close(1000, "device deleted");
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
for (const [, client] of device.clients) {
|
|
146
|
+
try {
|
|
147
|
+
client.socket.close(1000, "device deleted");
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
this.devices.delete(hostDeviceId);
|
|
129
151
|
return true;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
bufferOutput(
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
const
|
|
136
|
-
let
|
|
137
|
-
if (!
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
bufferOutput(hostDeviceId: string, envelope: Envelope): void {
|
|
155
|
+
const device = this.devices.get(hostDeviceId);
|
|
156
|
+
if (!device) return;
|
|
157
|
+
const terminalId = envelope.terminalId ?? "default";
|
|
158
|
+
let buffer = device.outputBuffers.get(terminalId);
|
|
159
|
+
if (!buffer) {
|
|
160
|
+
buffer = [];
|
|
161
|
+
device.outputBuffers.set(terminalId, buffer);
|
|
140
162
|
}
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
163
|
+
buffer.push(envelope);
|
|
164
|
+
if (buffer.length > OUTPUT_BUFFER_CAPACITY) {
|
|
165
|
+
buffer.shift();
|
|
144
166
|
}
|
|
145
|
-
|
|
167
|
+
device.lastActivity = Date.now();
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
cacheStatus(
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
170
|
+
cacheStatus(hostDeviceId: string, envelope: Envelope): void {
|
|
171
|
+
const device = this.devices.get(hostDeviceId);
|
|
172
|
+
if (!device) return;
|
|
173
|
+
const terminalId = envelope.terminalId ?? "default";
|
|
174
|
+
device.lastStatusByTerminal.set(terminalId, envelope);
|
|
175
|
+
device.lastActivity = Date.now();
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
getStatusReplay(
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
return [...
|
|
178
|
+
getStatusReplay(hostDeviceId: string): Envelope[] {
|
|
179
|
+
const device = this.devices.get(hostDeviceId);
|
|
180
|
+
if (!device) return [];
|
|
181
|
+
return [...device.lastStatusByTerminal.values()];
|
|
160
182
|
}
|
|
161
183
|
|
|
162
184
|
getReplayFrom(
|
|
163
|
-
|
|
185
|
+
hostDeviceId: string,
|
|
164
186
|
afterSeqByTerminal: Record<string, number>,
|
|
165
187
|
fallbackAfterSeq = -1,
|
|
166
188
|
): Envelope[] {
|
|
167
|
-
const
|
|
168
|
-
if (!
|
|
189
|
+
const device = this.devices.get(hostDeviceId);
|
|
190
|
+
if (!device) return [];
|
|
169
191
|
const result: Envelope[] = [];
|
|
170
|
-
for (const [terminalId,
|
|
192
|
+
for (const [terminalId, buffer] of device.outputBuffers) {
|
|
171
193
|
const afterSeq = afterSeqByTerminal[terminalId] ?? fallbackAfterSeq;
|
|
172
|
-
for (const
|
|
173
|
-
if (
|
|
194
|
+
for (const envelope of buffer) {
|
|
195
|
+
if (envelope.seq !== undefined && envelope.seq > afterSeq) {
|
|
196
|
+
result.push(envelope);
|
|
197
|
+
}
|
|
174
198
|
}
|
|
175
199
|
}
|
|
176
200
|
return result.sort((a, b) => {
|
|
@@ -181,101 +205,101 @@ export class SessionManager {
|
|
|
181
205
|
});
|
|
182
206
|
}
|
|
183
207
|
|
|
184
|
-
claimControl(
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
|
|
188
|
-
session.controllerId = deviceId;
|
|
208
|
+
claimControl(hostDeviceId: string, deviceId: string): boolean {
|
|
209
|
+
const device = this.devices.get(hostDeviceId);
|
|
210
|
+
if (!device) return false;
|
|
211
|
+
device.controllerId = deviceId;
|
|
189
212
|
return true;
|
|
190
213
|
}
|
|
191
214
|
|
|
192
|
-
releaseControl(
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
195
|
-
if (
|
|
196
|
-
|
|
215
|
+
releaseControl(hostDeviceId: string, deviceId: string): boolean {
|
|
216
|
+
const device = this.devices.get(hostDeviceId);
|
|
217
|
+
if (!device) return false;
|
|
218
|
+
if (device.controllerId !== deviceId) return false;
|
|
219
|
+
device.controllerId = undefined;
|
|
197
220
|
return true;
|
|
198
221
|
}
|
|
199
222
|
|
|
200
|
-
terminate(
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
|
|
223
|
+
terminate(hostDeviceId: string): void {
|
|
224
|
+
const device = this.devices.get(hostDeviceId);
|
|
225
|
+
if (!device) return;
|
|
226
|
+
device.state = "terminated";
|
|
204
227
|
}
|
|
205
228
|
|
|
206
|
-
listActive():
|
|
207
|
-
return [...this.
|
|
229
|
+
listActive(): HostDevice[] {
|
|
230
|
+
return [...this.devices.values()].filter((device) => device.state !== "terminated");
|
|
208
231
|
}
|
|
209
232
|
|
|
210
|
-
getSummary(
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
233
|
+
getSummary(hostDeviceId: string) {
|
|
234
|
+
const device = this.devices.get(hostDeviceId);
|
|
235
|
+
if (!device) return undefined;
|
|
213
236
|
return {
|
|
214
|
-
id:
|
|
215
|
-
|
|
237
|
+
id: device.hostDeviceId,
|
|
238
|
+
hostDeviceId: device.hostDeviceId,
|
|
239
|
+
state: device.state,
|
|
240
|
+
online:
|
|
241
|
+
!!device.host &&
|
|
242
|
+
device.host.socket.readyState === device.host.socket.OPEN,
|
|
216
243
|
hasHost:
|
|
217
|
-
!!
|
|
218
|
-
|
|
219
|
-
clientCount:
|
|
220
|
-
controllerId:
|
|
221
|
-
lastActivity:
|
|
222
|
-
createdAt:
|
|
223
|
-
bufferSize: [...
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
userId: session.userId ?? null,
|
|
244
|
+
!!device.host &&
|
|
245
|
+
device.host.socket.readyState === device.host.socket.OPEN,
|
|
246
|
+
clientCount: device.clients.size,
|
|
247
|
+
controllerId: device.controllerId ?? null,
|
|
248
|
+
lastActivity: device.lastActivity,
|
|
249
|
+
createdAt: device.createdAt,
|
|
250
|
+
bufferSize: [...device.outputBuffers.values()].reduce((sum, buf) => sum + buf.length, 0),
|
|
251
|
+
machineId: device.machineId ?? null,
|
|
252
|
+
hostname: device.hostname ?? null,
|
|
253
|
+
platform: device.platform ?? null,
|
|
254
|
+
cwd: device.cwd ?? null,
|
|
255
|
+
capabilities: device.capabilities,
|
|
256
|
+
userId: device.userId ?? null,
|
|
231
257
|
};
|
|
232
258
|
}
|
|
233
259
|
|
|
234
260
|
setMetadata(
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
hostDeviceId: string,
|
|
262
|
+
_provider?: string,
|
|
237
263
|
machineId?: string,
|
|
238
264
|
hostname?: string,
|
|
239
265
|
platform?: string,
|
|
240
266
|
cwd?: string,
|
|
241
|
-
|
|
267
|
+
_projectName?: string,
|
|
268
|
+
capabilities?: string[],
|
|
242
269
|
): void {
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
if (
|
|
246
|
-
if (
|
|
247
|
-
if (
|
|
248
|
-
if (
|
|
249
|
-
if (
|
|
250
|
-
if (projectName) session.projectName = projectName;
|
|
270
|
+
const device = this.devices.get(hostDeviceId);
|
|
271
|
+
if (!device) return;
|
|
272
|
+
if (machineId) device.machineId = machineId;
|
|
273
|
+
if (hostname) device.hostname = hostname;
|
|
274
|
+
if (platform) device.platform = platform;
|
|
275
|
+
if (cwd) device.cwd = cwd;
|
|
276
|
+
if (capabilities) device.capabilities = capabilities;
|
|
251
277
|
}
|
|
252
278
|
|
|
253
|
-
private maybeDelete(
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
256
|
-
if (!
|
|
257
|
-
this.
|
|
279
|
+
private maybeDelete(hostDeviceId: string): void {
|
|
280
|
+
const device = this.devices.get(hostDeviceId);
|
|
281
|
+
if (!device) return;
|
|
282
|
+
if (!device.host && device.clients.size === 0) {
|
|
283
|
+
this.devices.delete(hostDeviceId);
|
|
258
284
|
}
|
|
259
285
|
}
|
|
260
286
|
|
|
261
287
|
private cleanup(): void {
|
|
262
288
|
const now = Date.now();
|
|
263
|
-
for (const [
|
|
264
|
-
// Remove sessions where host disconnected and window expired
|
|
289
|
+
for (const [hostDeviceId, device] of this.devices) {
|
|
265
290
|
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
now -
|
|
291
|
+
device.state === "host_disconnected" &&
|
|
292
|
+
device.hostDisconnectedAt &&
|
|
293
|
+
now - device.hostDisconnectedAt > HOST_RECONNECT_WINDOW
|
|
269
294
|
) {
|
|
270
|
-
|
|
295
|
+
device.state = "terminated";
|
|
271
296
|
}
|
|
272
|
-
// Clean up terminated sessions with no connections
|
|
273
297
|
if (
|
|
274
|
-
|
|
275
|
-
!
|
|
276
|
-
|
|
298
|
+
device.state === "terminated" &&
|
|
299
|
+
!device.host &&
|
|
300
|
+
device.clients.size === 0
|
|
277
301
|
) {
|
|
278
|
-
this.
|
|
302
|
+
this.devices.delete(hostDeviceId);
|
|
279
303
|
}
|
|
280
304
|
}
|
|
281
305
|
}
|
package/src/state-store.ts
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface StoredAuthorizationRecord {
|
|
2
|
+
authorizationId: string;
|
|
2
3
|
token: string;
|
|
3
|
-
|
|
4
|
+
hostDeviceId: string;
|
|
5
|
+
clientDeviceId?: string;
|
|
6
|
+
clientName?: string;
|
|
4
7
|
createdAt: number;
|
|
5
8
|
lastUsedAt: number;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export interface StoredPairingRecord {
|
|
9
|
-
|
|
12
|
+
hostDeviceId: string;
|
|
10
13
|
pairingCode: string;
|
|
11
14
|
expiresAt: number;
|
|
12
15
|
claimed: boolean;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export interface GatewayStateStore {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
loadAuthorizations(): Promise<StoredAuthorizationRecord[]>;
|
|
20
|
+
saveAuthorization(record: StoredAuthorizationRecord): Promise<void>;
|
|
21
|
+
deleteAuthorization(authorizationId: string): Promise<void>;
|
|
19
22
|
loadPairings(): Promise<StoredPairingRecord[]>;
|
|
20
23
|
savePairing(record: StoredPairingRecord): Promise<void>;
|
|
21
24
|
deletePairing(pairingCode: string): Promise<void>;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
const
|
|
25
|
-
|
|
27
|
+
const AUTHORIZATION_TABLE =
|
|
28
|
+
process.env.SUPABASE_GATEWAY_AUTHORIZATION_TABLE ??
|
|
29
|
+
"linkshell_gateway_device_authorizations";
|
|
30
|
+
const PAIRING_TABLE =
|
|
31
|
+
process.env.SUPABASE_GATEWAY_PAIRING_TABLE ??
|
|
32
|
+
"linkshell_gateway_pairing_challenges";
|
|
26
33
|
const STORE_TIMEOUT_MS = Number(process.env.SUPABASE_STATE_TIMEOUT_MS ?? 3_000);
|
|
27
34
|
|
|
28
35
|
function msToIso(ms: number): string {
|
|
@@ -35,6 +42,10 @@ function isoToMs(value: unknown): number {
|
|
|
35
42
|
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
function maybeString(value: unknown): string | undefined {
|
|
46
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export function createSupabaseStateStore(): GatewayStateStore | undefined {
|
|
39
50
|
const url = process.env.SUPABASE_URL?.replace(/\/+$/, "");
|
|
40
51
|
const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
|
|
@@ -64,49 +75,54 @@ export function createSupabaseStateStore(): GatewayStateStore | undefined {
|
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
return {
|
|
67
|
-
async
|
|
78
|
+
async loadAuthorizations() {
|
|
68
79
|
const rows = await request<Array<Record<string, unknown>>>(
|
|
69
|
-
`${
|
|
80
|
+
`${AUTHORIZATION_TABLE}?select=authorization_id,token,host_device_id,client_device_id,client_name,created_at,last_used_at`,
|
|
70
81
|
);
|
|
71
82
|
return rows.map((row) => ({
|
|
83
|
+
authorizationId: String(row.authorization_id ?? ""),
|
|
72
84
|
token: String(row.token ?? ""),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
hostDeviceId: String(row.host_device_id ?? ""),
|
|
86
|
+
clientDeviceId: maybeString(row.client_device_id),
|
|
87
|
+
clientName: maybeString(row.client_name),
|
|
76
88
|
createdAt: isoToMs(row.created_at),
|
|
77
89
|
lastUsedAt: isoToMs(row.last_used_at),
|
|
78
|
-
})).filter((record) => record.token);
|
|
90
|
+
})).filter((record) => record.authorizationId && record.token && record.hostDeviceId);
|
|
79
91
|
},
|
|
80
|
-
async
|
|
92
|
+
async saveAuthorization(record) {
|
|
81
93
|
await request(
|
|
82
|
-
`${
|
|
94
|
+
`${AUTHORIZATION_TABLE}?on_conflict=authorization_id`,
|
|
83
95
|
{
|
|
84
96
|
method: "POST",
|
|
85
97
|
headers: { Prefer: "resolution=merge-duplicates" },
|
|
86
98
|
body: JSON.stringify({
|
|
99
|
+
authorization_id: record.authorizationId,
|
|
87
100
|
token: record.token,
|
|
88
|
-
|
|
101
|
+
host_device_id: record.hostDeviceId,
|
|
102
|
+
client_device_id: record.clientDeviceId ?? null,
|
|
103
|
+
client_name: record.clientName ?? null,
|
|
89
104
|
created_at: msToIso(record.createdAt),
|
|
90
105
|
last_used_at: msToIso(record.lastUsedAt),
|
|
91
106
|
}),
|
|
92
107
|
},
|
|
93
108
|
);
|
|
94
109
|
},
|
|
95
|
-
async
|
|
96
|
-
await request(
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
async deleteAuthorization(authorizationId) {
|
|
111
|
+
await request(
|
|
112
|
+
`${AUTHORIZATION_TABLE}?authorization_id=eq.${encodeURIComponent(authorizationId)}`,
|
|
113
|
+
{ method: "DELETE" },
|
|
114
|
+
);
|
|
99
115
|
},
|
|
100
116
|
async loadPairings() {
|
|
101
117
|
const rows = await request<Array<Record<string, unknown>>>(
|
|
102
|
-
`${PAIRING_TABLE}?select=pairing_code,
|
|
118
|
+
`${PAIRING_TABLE}?select=pairing_code,host_device_id,expires_at,claimed`,
|
|
103
119
|
);
|
|
104
120
|
return rows.map((row) => ({
|
|
105
121
|
pairingCode: String(row.pairing_code ?? ""),
|
|
106
|
-
|
|
122
|
+
hostDeviceId: String(row.host_device_id ?? ""),
|
|
107
123
|
expiresAt: isoToMs(row.expires_at),
|
|
108
124
|
claimed: row.claimed === true,
|
|
109
|
-
})).filter((record) => record.pairingCode && record.
|
|
125
|
+
})).filter((record) => record.pairingCode && record.hostDeviceId);
|
|
110
126
|
},
|
|
111
127
|
async savePairing(record) {
|
|
112
128
|
await request(
|
|
@@ -116,7 +132,7 @@ export function createSupabaseStateStore(): GatewayStateStore | undefined {
|
|
|
116
132
|
headers: { Prefer: "resolution=merge-duplicates" },
|
|
117
133
|
body: JSON.stringify({
|
|
118
134
|
pairing_code: record.pairingCode,
|
|
119
|
-
|
|
135
|
+
host_device_id: record.hostDeviceId,
|
|
120
136
|
expires_at: msToIso(record.expiresAt),
|
|
121
137
|
claimed: record.claimed,
|
|
122
138
|
}),
|