@linkshell/gateway 0.3.8 → 0.3.10

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