@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.
Files changed (40) hide show
  1. package/README.md +14 -13
  2. package/dist/gateway/src/agent-permission-http.d.ts +10 -37
  3. package/dist/gateway/src/agent-permission-http.js +16 -56
  4. package/dist/gateway/src/agent-permission-http.js.map +1 -1
  5. package/dist/gateway/src/embedded.js +121 -57
  6. package/dist/gateway/src/embedded.js.map +1 -1
  7. package/dist/gateway/src/index.js +161 -94
  8. package/dist/gateway/src/index.js.map +1 -1
  9. package/dist/gateway/src/pairings.d.ts +3 -3
  10. package/dist/gateway/src/pairings.js +4 -5
  11. package/dist/gateway/src/pairings.js.map +1 -1
  12. package/dist/gateway/src/relay.d.ts +2 -2
  13. package/dist/gateway/src/relay.js +27 -38
  14. package/dist/gateway/src/relay.js.map +1 -1
  15. package/dist/gateway/src/sessions.d.ts +31 -28
  16. package/dist/gateway/src/sessions.js +163 -145
  17. package/dist/gateway/src/sessions.js.map +1 -1
  18. package/dist/gateway/src/state-store.d.ts +9 -6
  19. package/dist/gateway/src/state-store.js +26 -19
  20. package/dist/gateway/src/state-store.js.map +1 -1
  21. package/dist/gateway/src/tokens.d.ts +27 -7
  22. package/dist/gateway/src/tokens.js +86 -60
  23. package/dist/gateway/src/tokens.js.map +1 -1
  24. package/dist/gateway/src/tunnel.d.ts +11 -13
  25. package/dist/gateway/src/tunnel.js +36 -36
  26. package/dist/gateway/src/tunnel.js.map +1 -1
  27. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  28. package/dist/shared-protocol/src/index.d.ts +3961 -5788
  29. package/dist/shared-protocol/src/index.js +19 -84
  30. package/dist/shared-protocol/src/index.js.map +1 -1
  31. package/package.json +10 -10
  32. package/src/agent-permission-http.ts +20 -63
  33. package/src/embedded.ts +124 -56
  34. package/src/index.ts +165 -94
  35. package/src/pairings.ts +6 -7
  36. package/src/relay.ts +38 -48
  37. package/src/sessions.ts +174 -150
  38. package/src/state-store.ts +41 -25
  39. package/src/tokens.ts +109 -63
  40. 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 SessionState = "active" | "host_disconnected" | "terminated";
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 Session {
15
+ export interface HostDevice {
14
16
  id: string;
15
- state: SessionState;
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[]>; // keyed by terminalId
22
- lastStatusByTerminal: Map<string, Envelope>; // last terminal.status per terminal
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
- projectName: string | undefined;
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; // 60s
36
+ const HOST_RECONNECT_WINDOW = 60_000;
37
37
  const CLEANUP_INTERVAL = 30_000;
38
38
 
39
- export class SessionManager {
40
- private sessions = new Map<string, Session>();
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(sessionId: string): Session {
48
- let session = this.sessions.get(sessionId);
49
- if (!session) {
50
- session = {
51
- id: sessionId,
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
- projectName: undefined,
66
+ capabilities: [],
67
67
  userId: undefined,
68
68
  };
69
- this.sessions.set(sessionId, session);
69
+ this.devices.set(hostDeviceId, device);
70
70
  }
71
- return session;
71
+ return device;
72
72
  }
73
73
 
74
- get(sessionId: string): Session | undefined {
75
- return this.sessions.get(sessionId);
74
+ get(hostDeviceId: string): HostDevice | undefined {
75
+ return this.devices.get(hostDeviceId);
76
76
  }
77
77
 
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();
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(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;
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
- session.lastActivity = Date.now();
92
+ device.lastActivity = Date.now();
93
93
  }
94
94
 
95
95
  removeHost(
96
- sessionId: string,
96
+ hostDeviceId: string,
97
97
  ): { clients: Map<string, ConnectedDevice> } | undefined {
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 };
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(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;
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(sessionId);
114
+ this.maybeDelete(hostDeviceId);
116
115
  }
117
116
 
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 {}
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
- for (const [, client] of session.clients) {
126
- try { client.socket.close(1000, "session deleted"); } catch {}
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.sessions.delete(sessionId);
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(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);
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
- buf.push(envelope);
142
- if (buf.length > OUTPUT_BUFFER_CAPACITY) {
143
- buf.shift();
163
+ buffer.push(envelope);
164
+ if (buffer.length > OUTPUT_BUFFER_CAPACITY) {
165
+ buffer.shift();
144
166
  }
145
- session.lastActivity = Date.now();
167
+ device.lastActivity = Date.now();
146
168
  }
147
169
 
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();
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(sessionId: string): Envelope[] {
157
- const session = this.sessions.get(sessionId);
158
- if (!session) return [];
159
- return [...session.lastStatusByTerminal.values()];
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
- sessionId: string,
185
+ hostDeviceId: string,
164
186
  afterSeqByTerminal: Record<string, number>,
165
187
  fallbackAfterSeq = -1,
166
188
  ): Envelope[] {
167
- const session = this.sessions.get(sessionId);
168
- if (!session) return [];
189
+ const device = this.devices.get(hostDeviceId);
190
+ if (!device) return [];
169
191
  const result: Envelope[] = [];
170
- for (const [terminalId, buf] of session.outputBuffers) {
192
+ for (const [terminalId, buffer] of device.outputBuffers) {
171
193
  const afterSeq = afterSeqByTerminal[terminalId] ?? fallbackAfterSeq;
172
- for (const e of buf) {
173
- if (e.seq !== undefined && e.seq > afterSeq) result.push(e);
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(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;
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(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;
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(sessionId: string): void {
201
- const session = this.sessions.get(sessionId);
202
- if (!session) return;
203
- session.state = "terminated";
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(): Session[] {
207
- return [...this.sessions.values()].filter((s) => s.state !== "terminated");
229
+ listActive(): HostDevice[] {
230
+ return [...this.devices.values()].filter((device) => device.state !== "terminated");
208
231
  }
209
232
 
210
- getSummary(sessionId: string) {
211
- const session = this.sessions.get(sessionId);
212
- if (!session) return undefined;
233
+ getSummary(hostDeviceId: string) {
234
+ const device = this.devices.get(hostDeviceId);
235
+ if (!device) return undefined;
213
236
  return {
214
- id: session.id,
215
- state: session.state,
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
- !!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,
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
- sessionId: string,
236
- provider?: string,
261
+ hostDeviceId: string,
262
+ _provider?: string,
237
263
  machineId?: string,
238
264
  hostname?: string,
239
265
  platform?: string,
240
266
  cwd?: string,
241
- projectName?: string,
267
+ _projectName?: string,
268
+ capabilities?: string[],
242
269
  ): void {
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;
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(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);
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 [id, session] of this.sessions) {
264
- // Remove sessions where host disconnected and window expired
289
+ for (const [hostDeviceId, device] of this.devices) {
265
290
  if (
266
- session.state === "host_disconnected" &&
267
- session.hostDisconnectedAt &&
268
- now - session.hostDisconnectedAt > HOST_RECONNECT_WINDOW
291
+ device.state === "host_disconnected" &&
292
+ device.hostDisconnectedAt &&
293
+ now - device.hostDisconnectedAt > HOST_RECONNECT_WINDOW
269
294
  ) {
270
- session.state = "terminated";
295
+ device.state = "terminated";
271
296
  }
272
- // Clean up terminated sessions with no connections
273
297
  if (
274
- session.state === "terminated" &&
275
- !session.host &&
276
- session.clients.size === 0
298
+ device.state === "terminated" &&
299
+ !device.host &&
300
+ device.clients.size === 0
277
301
  ) {
278
- this.sessions.delete(id);
302
+ this.devices.delete(hostDeviceId);
279
303
  }
280
304
  }
281
305
  }
@@ -1,28 +1,35 @@
1
- export interface StoredTokenRecord {
1
+ export interface StoredAuthorizationRecord {
2
+ authorizationId: string;
2
3
  token: string;
3
- sessionIds: string[];
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
- sessionId: string;
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
- loadTokens(): Promise<StoredTokenRecord[]>;
17
- saveToken(record: StoredTokenRecord): Promise<void>;
18
- deleteToken(token: string): Promise<void>;
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 TOKEN_TABLE = process.env.SUPABASE_GATEWAY_TOKEN_TABLE ?? "linkshell_gateway_tokens";
25
- const PAIRING_TABLE = process.env.SUPABASE_GATEWAY_PAIRING_TABLE ?? "linkshell_gateway_pairings";
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 loadTokens() {
78
+ async loadAuthorizations() {
68
79
  const rows = await request<Array<Record<string, unknown>>>(
69
- `${TOKEN_TABLE}?select=token,session_ids,created_at,last_used_at`,
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
- sessionIds: Array.isArray(row.session_ids)
74
- ? row.session_ids.map(String)
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 saveToken(record) {
92
+ async saveAuthorization(record) {
81
93
  await request(
82
- `${TOKEN_TABLE}?on_conflict=token`,
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
- session_ids: record.sessionIds,
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 deleteToken(token) {
96
- await request(`${TOKEN_TABLE}?token=eq.${encodeURIComponent(token)}`, {
97
- method: "DELETE",
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,session_id,expires_at,claimed`,
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
- sessionId: String(row.session_id ?? ""),
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.sessionId);
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
- session_id: record.sessionId,
135
+ host_device_id: record.hostDeviceId,
120
136
  expires_at: msToIso(record.expiresAt),
121
137
  claimed: record.claimed,
122
138
  }),