@linkshell/gateway 0.5.3 → 0.6.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 +8 -22
- package/README.md +99 -44
- package/dist/gateway/src/accounts.js.map +1 -0
- package/dist/gateway/src/index.d.ts +4 -1
- package/dist/gateway/src/index.js +4 -863
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/main.js +57 -0
- package/dist/gateway/src/main.js.map +1 -0
- package/dist/gateway/src/rate-limit.d.ts +22 -0
- package/dist/gateway/src/rate-limit.js +66 -0
- package/dist/gateway/src/rate-limit.js.map +1 -0
- package/dist/gateway/src/relay.d.ts +84 -5
- package/dist/gateway/src/relay.js +402 -529
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/serve.d.ts +34 -0
- package/dist/gateway/src/serve.js +102 -0
- package/dist/gateway/src/serve.js.map +1 -0
- package/dist/{gateway-v2 → gateway}/src/store.d.ts +5 -0
- package/dist/{gateway-v2 → gateway}/src/store.js +7 -0
- package/dist/gateway/src/store.js.map +1 -0
- package/dist/gateway/src/subscription.d.ts +18 -0
- package/dist/gateway/src/subscription.js +46 -0
- package/dist/gateway/src/subscription.js.map +1 -0
- package/dist/gateway/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/direct.d.ts +22 -0
- package/dist/wire/src/direct.js +37 -0
- package/dist/wire/src/direct.js.map +1 -0
- package/dist/wire/src/index.d.ts +1 -0
- package/dist/wire/src/index.js +1 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +287 -6
- package/dist/wire/src/model.js +33 -0
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/relay.d.ts +8 -8
- package/dist/wire/src/rpc.d.ts +1780 -44
- package/dist/wire/src/rpc.js +48 -3
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +356 -0
- package/dist/wire/src/updates.js +11 -1
- package/dist/wire/src/updates.js.map +1 -1
- package/package.json +12 -12
- package/src/accounts.ts +32 -0
- package/src/index.ts +4 -997
- package/src/main.ts +65 -0
- package/src/rate-limit.ts +72 -0
- package/src/relay.ts +435 -620
- package/src/serve.ts +130 -0
- package/src/store.ts +135 -0
- package/src/subscription.ts +51 -0
- package/dist/gateway/src/auth-middleware.d.ts +0 -44
- package/dist/gateway/src/auth-middleware.js +0 -204
- package/dist/gateway/src/auth-middleware.js.map +0 -1
- package/dist/gateway/src/cors.d.ts +0 -2
- package/dist/gateway/src/cors.js +0 -7
- package/dist/gateway/src/cors.js.map +0 -1
- package/dist/gateway/src/embedded.d.ts +0 -21
- package/dist/gateway/src/embedded.js +0 -438
- package/dist/gateway/src/embedded.js.map +0 -1
- package/dist/gateway/src/host-auth.d.ts +0 -32
- package/dist/gateway/src/host-auth.js +0 -78
- package/dist/gateway/src/host-auth.js.map +0 -1
- package/dist/gateway/src/pairings.d.ts +0 -38
- package/dist/gateway/src/pairings.js +0 -119
- package/dist/gateway/src/pairings.js.map +0 -1
- package/dist/gateway/src/presence.d.ts +0 -28
- package/dist/gateway/src/presence.js +0 -92
- package/dist/gateway/src/presence.js.map +0 -1
- package/dist/gateway/src/sessions.d.ts +0 -113
- package/dist/gateway/src/sessions.js +0 -352
- package/dist/gateway/src/sessions.js.map +0 -1
- package/dist/gateway/src/state-store.d.ts +0 -21
- package/dist/gateway/src/state-store.js +0 -103
- package/dist/gateway/src/state-store.js.map +0 -1
- package/dist/gateway/src/static-web.d.ts +0 -15
- package/dist/gateway/src/static-web.js +0 -129
- package/dist/gateway/src/static-web.js.map +0 -1
- package/dist/gateway/src/tokens.d.ts +0 -19
- package/dist/gateway/src/tokens.js +0 -151
- package/dist/gateway/src/tokens.js.map +0 -1
- package/dist/gateway/src/tunnel.d.ts +0 -61
- package/dist/gateway/src/tunnel.js +0 -576
- package/dist/gateway/src/tunnel.js.map +0 -1
- package/dist/gateway-v2/src/accounts.js.map +0 -1
- package/dist/gateway-v2/src/index.d.ts +0 -3
- package/dist/gateway-v2/src/index.js +0 -4
- package/dist/gateway-v2/src/index.js.map +0 -1
- package/dist/gateway-v2/src/main.js +0 -17
- package/dist/gateway-v2/src/main.js.map +0 -1
- package/dist/gateway-v2/src/server.d.ts +0 -84
- package/dist/gateway-v2/src/server.js +0 -426
- package/dist/gateway-v2/src/server.js.map +0 -1
- package/dist/gateway-v2/src/store.js.map +0 -1
- package/dist/shared-protocol/src/index.d.ts +0 -17029
- package/dist/shared-protocol/src/index.js +0 -1064
- package/dist/shared-protocol/src/index.js.map +0 -1
- package/src/auth-middleware.ts +0 -266
- package/src/cors.ts +0 -8
- package/src/embedded.ts +0 -526
- package/src/host-auth.ts +0 -88
- package/src/pairings.ts +0 -133
- package/src/presence.ts +0 -118
- package/src/sessions.ts +0 -432
- package/src/state-store.ts +0 -140
- package/src/static-web.ts +0 -133
- package/src/tokens.ts +0 -159
- package/src/tunnel.ts +0 -644
- /package/dist/{gateway-v2 → gateway}/src/accounts.d.ts +0 -0
- /package/dist/{gateway-v2 → gateway}/src/accounts.js +0 -0
- /package/dist/{gateway-v2 → gateway}/src/main.d.ts +0 -0
package/src/state-store.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
export interface StoredTokenRecord {
|
|
2
|
-
token: string;
|
|
3
|
-
sessionIds: string[];
|
|
4
|
-
createdAt: number;
|
|
5
|
-
lastUsedAt: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface StoredPairingRecord {
|
|
9
|
-
sessionId: string;
|
|
10
|
-
pairingCode: string;
|
|
11
|
-
expiresAt: number;
|
|
12
|
-
claimed: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface GatewayStateStore {
|
|
16
|
-
loadTokens(): Promise<StoredTokenRecord[]>;
|
|
17
|
-
saveToken(record: StoredTokenRecord): Promise<void>;
|
|
18
|
-
deleteToken(token: string): Promise<void>;
|
|
19
|
-
loadPairings(): Promise<StoredPairingRecord[]>;
|
|
20
|
-
savePairing(record: StoredPairingRecord): Promise<void>;
|
|
21
|
-
deletePairing(pairingCode: string): Promise<void>;
|
|
22
|
-
}
|
|
23
|
-
|
|
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";
|
|
26
|
-
const STORE_TIMEOUT_MS = Number(process.env.SUPABASE_STATE_TIMEOUT_MS ?? 3_000);
|
|
27
|
-
|
|
28
|
-
function msToIso(ms: number): string {
|
|
29
|
-
return new Date(ms).toISOString();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isoToMs(value: unknown): number {
|
|
33
|
-
if (typeof value !== "string") return Date.now();
|
|
34
|
-
const parsed = Date.parse(value);
|
|
35
|
-
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createSupabaseStateStore(): GatewayStateStore | undefined {
|
|
39
|
-
const url = process.env.SUPABASE_URL?.replace(/\/+$/, "");
|
|
40
|
-
const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_ANON_KEY;
|
|
41
|
-
if (!url || !key) return undefined;
|
|
42
|
-
|
|
43
|
-
const headers = {
|
|
44
|
-
apikey: key,
|
|
45
|
-
Authorization: `Bearer ${key}`,
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
50
|
-
const res = await fetch(`${url}/rest/v1/${path}`, {
|
|
51
|
-
...init,
|
|
52
|
-
signal: init?.signal ?? AbortSignal.timeout(STORE_TIMEOUT_MS),
|
|
53
|
-
headers: {
|
|
54
|
-
...headers,
|
|
55
|
-
...(init?.headers ?? {}),
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
const body = await res.text().catch(() => "");
|
|
60
|
-
throw new Error(`Supabase state store ${res.status}: ${body || res.statusText}`);
|
|
61
|
-
}
|
|
62
|
-
if (res.status === 204) return undefined as T;
|
|
63
|
-
// Writes (POST upsert / DELETE) can return 2xx with an EMPTY body depending
|
|
64
|
-
// on PostgREST's Prefer handling — calling res.json() on that throws
|
|
65
|
-
// "Unexpected end of JSON input". Read as text and only parse when non-empty
|
|
66
|
-
// so writes resolve to undefined cleanly; reads (SELECT) always return a
|
|
67
|
-
// JSON array ("[]" at minimum), so they still parse.
|
|
68
|
-
const text = await res.text();
|
|
69
|
-
if (!text.trim()) return undefined as T;
|
|
70
|
-
return JSON.parse(text) as T;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
async loadTokens() {
|
|
75
|
-
const rows = await request<Array<Record<string, unknown>>>(
|
|
76
|
-
`${TOKEN_TABLE}?select=token,session_ids,created_at,last_used_at`,
|
|
77
|
-
);
|
|
78
|
-
return rows.map((row) => ({
|
|
79
|
-
token: String(row.token ?? ""),
|
|
80
|
-
sessionIds: Array.isArray(row.session_ids)
|
|
81
|
-
? row.session_ids.map(String)
|
|
82
|
-
: [],
|
|
83
|
-
createdAt: isoToMs(row.created_at),
|
|
84
|
-
lastUsedAt: isoToMs(row.last_used_at),
|
|
85
|
-
})).filter((record) => record.token);
|
|
86
|
-
},
|
|
87
|
-
async saveToken(record) {
|
|
88
|
-
await request(
|
|
89
|
-
`${TOKEN_TABLE}?on_conflict=token`,
|
|
90
|
-
{
|
|
91
|
-
method: "POST",
|
|
92
|
-
headers: { Prefer: "return=minimal,resolution=merge-duplicates" },
|
|
93
|
-
body: JSON.stringify({
|
|
94
|
-
token: record.token,
|
|
95
|
-
session_ids: record.sessionIds,
|
|
96
|
-
created_at: msToIso(record.createdAt),
|
|
97
|
-
last_used_at: msToIso(record.lastUsedAt),
|
|
98
|
-
}),
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
},
|
|
102
|
-
async deleteToken(token) {
|
|
103
|
-
await request(`${TOKEN_TABLE}?token=eq.${encodeURIComponent(token)}`, {
|
|
104
|
-
method: "DELETE",
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
async loadPairings() {
|
|
108
|
-
const rows = await request<Array<Record<string, unknown>>>(
|
|
109
|
-
`${PAIRING_TABLE}?select=pairing_code,session_id,expires_at,claimed`,
|
|
110
|
-
);
|
|
111
|
-
return rows.map((row) => ({
|
|
112
|
-
pairingCode: String(row.pairing_code ?? ""),
|
|
113
|
-
sessionId: String(row.session_id ?? ""),
|
|
114
|
-
expiresAt: isoToMs(row.expires_at),
|
|
115
|
-
claimed: row.claimed === true,
|
|
116
|
-
})).filter((record) => record.pairingCode && record.sessionId);
|
|
117
|
-
},
|
|
118
|
-
async savePairing(record) {
|
|
119
|
-
await request(
|
|
120
|
-
`${PAIRING_TABLE}?on_conflict=pairing_code`,
|
|
121
|
-
{
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { Prefer: "return=minimal,resolution=merge-duplicates" },
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
pairing_code: record.pairingCode,
|
|
126
|
-
session_id: record.sessionId,
|
|
127
|
-
expires_at: msToIso(record.expiresAt),
|
|
128
|
-
claimed: record.claimed,
|
|
129
|
-
}),
|
|
130
|
-
},
|
|
131
|
-
);
|
|
132
|
-
},
|
|
133
|
-
async deletePairing(pairingCode) {
|
|
134
|
-
await request(
|
|
135
|
-
`${PAIRING_TABLE}?pairing_code=eq.${encodeURIComponent(pairingCode)}`,
|
|
136
|
-
{ method: "DELETE" },
|
|
137
|
-
);
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
package/src/static-web.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { stat, readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve, join, extname, normalize } from "node:path";
|
|
4
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
-
|
|
6
|
-
// Serves the built web-dashboard SPA from the gateway, so a single deployment
|
|
7
|
-
// (and single origin) hosts both the API/WebSocket and the UI. The web is
|
|
8
|
-
// optional: if WEB_DIST doesn't exist (e.g. local dev using the Vite dev
|
|
9
|
-
// server), the gateway runs API-only and this is a no-op.
|
|
10
|
-
|
|
11
|
-
const WEB_DIST = process.env.WEB_DIST ?? resolve(process.cwd(), "web");
|
|
12
|
-
const hasWeb = existsSync(join(WEB_DIST, "index.html"));
|
|
13
|
-
|
|
14
|
-
const CONTENT_TYPES: Record<string, string> = {
|
|
15
|
-
".html": "text/html; charset=utf-8",
|
|
16
|
-
".js": "text/javascript; charset=utf-8",
|
|
17
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
18
|
-
".css": "text/css; charset=utf-8",
|
|
19
|
-
".json": "application/json; charset=utf-8",
|
|
20
|
-
".svg": "image/svg+xml",
|
|
21
|
-
".png": "image/png",
|
|
22
|
-
".jpg": "image/jpeg",
|
|
23
|
-
".jpeg": "image/jpeg",
|
|
24
|
-
".gif": "image/gif",
|
|
25
|
-
".ico": "image/x-icon",
|
|
26
|
-
".webp": "image/webp",
|
|
27
|
-
".woff": "font/woff",
|
|
28
|
-
".woff2": "font/woff2",
|
|
29
|
-
".ttf": "font/ttf",
|
|
30
|
-
".map": "application/json; charset=utf-8",
|
|
31
|
-
".txt": "text/plain; charset=utf-8",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export function webEnabled(): boolean {
|
|
35
|
-
return hasWeb;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function webDistPath(): string {
|
|
39
|
-
return WEB_DIST;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function resolveWebFile(pathname: string, allowSpaFallback: boolean): Promise<string | null> {
|
|
43
|
-
// Path-traversal guard: normalize, then confine to WEB_DIST.
|
|
44
|
-
const candidate = resolve(WEB_DIST, "." + normalize(pathname));
|
|
45
|
-
const inRoot = candidate === WEB_DIST || candidate.startsWith(WEB_DIST + "/");
|
|
46
|
-
|
|
47
|
-
let filePath: string | null = null;
|
|
48
|
-
if (inRoot && pathname !== "/") {
|
|
49
|
-
try {
|
|
50
|
-
const s = await stat(candidate);
|
|
51
|
-
if (s.isFile()) filePath = candidate;
|
|
52
|
-
} catch {
|
|
53
|
-
// not a real file
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// SPA fallback: serve index.html for "/" and for any path without a file
|
|
58
|
-
// extension (a client-side route). Missing assets (with an extension) 404.
|
|
59
|
-
if (!filePath) {
|
|
60
|
-
if (allowSpaFallback && (pathname === "/" || extname(pathname) === "")) {
|
|
61
|
-
filePath = join(WEB_DIST, "index.html");
|
|
62
|
-
} else {
|
|
63
|
-
return null; // let the caller 404/proxy the missing asset
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return filePath;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function serveWebFile(req: IncomingMessage, res: ServerResponse, filePath: string): Promise<boolean> {
|
|
71
|
-
try {
|
|
72
|
-
const ext = extname(filePath).toLowerCase();
|
|
73
|
-
const type = CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
74
|
-
res.setHeader("Content-Type", type);
|
|
75
|
-
// Hashed assets (Vite emits content-hashed filenames) can cache hard;
|
|
76
|
-
// index.html must never be cached so deploys take effect immediately.
|
|
77
|
-
if (filePath.endsWith("index.html")) {
|
|
78
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
79
|
-
// lsh_tunnel is an HttpOnly helper cookie for preview subresources. Older
|
|
80
|
-
// builds used Path=/, so stale values are still sent to the LinkShell app
|
|
81
|
-
// shell and even /ws handshakes. Clear it whenever the real app document
|
|
82
|
-
// is served; /tunnel/... responses will set a fresh value when preview is
|
|
83
|
-
// actually opened.
|
|
84
|
-
res.setHeader("Set-Cookie", "lsh_tunnel=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax");
|
|
85
|
-
} else {
|
|
86
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
87
|
-
}
|
|
88
|
-
if (req.method === "HEAD") {
|
|
89
|
-
res.writeHead(200);
|
|
90
|
-
res.end();
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
const body = await readFile(filePath);
|
|
94
|
-
res.writeHead(200);
|
|
95
|
-
res.end(body);
|
|
96
|
-
return true;
|
|
97
|
-
} catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Try to serve a concrete web asset without the SPA fallback. This lets the
|
|
104
|
-
* gateway protect its own built assets before tunnel cookie fallback handles a
|
|
105
|
-
* previewed app's absolute assets such as "/assets/app.css".
|
|
106
|
-
*/
|
|
107
|
-
export async function serveWebAsset(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
108
|
-
if (!hasWeb) return false;
|
|
109
|
-
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
110
|
-
|
|
111
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
112
|
-
const pathname = decodeURIComponent(url.pathname);
|
|
113
|
-
const filePath = await resolveWebFile(pathname, false);
|
|
114
|
-
if (!filePath) return false;
|
|
115
|
-
return serveWebFile(req, res, filePath);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Try to serve a GET/HEAD request from the web dist. Returns true if handled.
|
|
120
|
-
* Place AFTER all API routes so it never shadows them. Unmatched non-asset
|
|
121
|
-
* paths fall back to index.html (SPA client-side routing).
|
|
122
|
-
*/
|
|
123
|
-
export async function serveWeb(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
124
|
-
if (!hasWeb) return false;
|
|
125
|
-
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
126
|
-
|
|
127
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
128
|
-
const pathname = decodeURIComponent(url.pathname);
|
|
129
|
-
const filePath = await resolveWebFile(pathname, true);
|
|
130
|
-
if (!filePath) return false;
|
|
131
|
-
|
|
132
|
-
return serveWebFile(req, res, filePath);
|
|
133
|
-
}
|
package/src/tokens.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { GatewayStateStore } from "./state-store.js";
|
|
3
|
-
|
|
4
|
-
const CLEANUP_INTERVAL = 5 * 60_000;
|
|
5
|
-
const SESSION_TTL = 7 * 24 * 60 * 60_000; // 7 days — prune stale bindings
|
|
6
|
-
|
|
7
|
-
interface TokenRecord {
|
|
8
|
-
token: string;
|
|
9
|
-
sessionIds: Set<string>;
|
|
10
|
-
createdAt: number;
|
|
11
|
-
lastUsedAt: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class TokenManager {
|
|
15
|
-
private tokens = new Map<string, TokenRecord>();
|
|
16
|
-
private sessionToToken = new Map<string, string>();
|
|
17
|
-
private cleanupTimer: ReturnType<typeof setInterval>;
|
|
18
|
-
|
|
19
|
-
constructor(private readonly store?: GatewayStateStore) {
|
|
20
|
-
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async hydrate(): Promise<void> {
|
|
24
|
-
if (!this.store) return;
|
|
25
|
-
try {
|
|
26
|
-
const records = await this.store.loadTokens();
|
|
27
|
-
const now = Date.now();
|
|
28
|
-
for (const record of records) {
|
|
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
|
-
createdAt: record.createdAt,
|
|
37
|
-
lastUsedAt: record.lastUsedAt,
|
|
38
|
-
});
|
|
39
|
-
for (const sessionId of record.sessionIds) {
|
|
40
|
-
this.sessionToToken.set(sessionId, record.token);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
} catch (err) {
|
|
44
|
-
process.stderr.write(`[gateway] token store hydrate failed, using memory only: ${err}\n`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
register(deviceToken?: string): string {
|
|
49
|
-
if (deviceToken && this.tokens.has(deviceToken)) {
|
|
50
|
-
const record = this.tokens.get(deviceToken)!;
|
|
51
|
-
record.lastUsedAt = Date.now();
|
|
52
|
-
this.persist(record);
|
|
53
|
-
return deviceToken;
|
|
54
|
-
}
|
|
55
|
-
const token = deviceToken || randomUUID();
|
|
56
|
-
this.tokens.set(token, {
|
|
57
|
-
token,
|
|
58
|
-
sessionIds: new Set(),
|
|
59
|
-
createdAt: Date.now(),
|
|
60
|
-
lastUsedAt: Date.now(),
|
|
61
|
-
});
|
|
62
|
-
this.persist(this.tokens.get(token)!);
|
|
63
|
-
return token;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
bind(token: string, sessionId: string): boolean {
|
|
67
|
-
const record = this.tokens.get(token);
|
|
68
|
-
if (!record) return false;
|
|
69
|
-
record.sessionIds.add(sessionId);
|
|
70
|
-
record.lastUsedAt = Date.now();
|
|
71
|
-
this.sessionToToken.set(sessionId, token);
|
|
72
|
-
this.persist(record);
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
validate(token: string): boolean {
|
|
77
|
-
const record = this.tokens.get(token);
|
|
78
|
-
if (!record) return false;
|
|
79
|
-
record.lastUsedAt = Date.now();
|
|
80
|
-
this.persist(record);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
owns(token: string, sessionId: string): boolean {
|
|
85
|
-
const record = this.tokens.get(token);
|
|
86
|
-
if (!record) return false;
|
|
87
|
-
record.lastUsedAt = Date.now();
|
|
88
|
-
this.persist(record);
|
|
89
|
-
return record.sessionIds.has(sessionId);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async ownsFresh(token: string, sessionId: string): Promise<boolean> {
|
|
93
|
-
if (this.owns(token, sessionId)) return true;
|
|
94
|
-
if (!this.store) return false;
|
|
95
|
-
try {
|
|
96
|
-
const records = await this.store.loadTokens();
|
|
97
|
-
const now = Date.now();
|
|
98
|
-
for (const record of records) {
|
|
99
|
-
if (now - record.lastUsedAt > SESSION_TTL) continue;
|
|
100
|
-
if (record.token !== token) continue;
|
|
101
|
-
const next = {
|
|
102
|
-
token: record.token,
|
|
103
|
-
sessionIds: new Set(record.sessionIds),
|
|
104
|
-
createdAt: record.createdAt,
|
|
105
|
-
lastUsedAt: Date.now(),
|
|
106
|
-
};
|
|
107
|
-
this.tokens.set(record.token, next);
|
|
108
|
-
for (const sid of record.sessionIds) {
|
|
109
|
-
this.sessionToToken.set(sid, record.token);
|
|
110
|
-
}
|
|
111
|
-
this.persist(next);
|
|
112
|
-
return next.sessionIds.has(sessionId);
|
|
113
|
-
}
|
|
114
|
-
} catch (err) {
|
|
115
|
-
process.stderr.write(`[gateway] token store refresh failed: ${err}\n`);
|
|
116
|
-
}
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
getSessionIds(token: string): Set<string> {
|
|
121
|
-
const record = this.tokens.get(token);
|
|
122
|
-
if (!record) return new Set();
|
|
123
|
-
record.lastUsedAt = Date.now();
|
|
124
|
-
this.persist(record);
|
|
125
|
-
return record.sessionIds;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
getTokenForSession(sessionId: string): string | undefined {
|
|
129
|
-
return this.sessionToToken.get(sessionId);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private cleanup(): void {
|
|
133
|
-
const now = Date.now();
|
|
134
|
-
for (const [token, record] of this.tokens) {
|
|
135
|
-
if (now - record.lastUsedAt > SESSION_TTL) {
|
|
136
|
-
for (const sid of record.sessionIds) {
|
|
137
|
-
this.sessionToToken.delete(sid);
|
|
138
|
-
}
|
|
139
|
-
this.tokens.delete(token);
|
|
140
|
-
void this.store?.deleteToken(token).catch(() => {});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private persist(record: TokenRecord): void {
|
|
146
|
-
void this.store?.saveToken({
|
|
147
|
-
token: record.token,
|
|
148
|
-
sessionIds: [...record.sessionIds],
|
|
149
|
-
createdAt: record.createdAt,
|
|
150
|
-
lastUsedAt: record.lastUsedAt,
|
|
151
|
-
}).catch((err) => {
|
|
152
|
-
process.stderr.write(`[gateway] token store save failed: ${err}\n`);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
destroy(): void {
|
|
157
|
-
clearInterval(this.cleanupTimer);
|
|
158
|
-
}
|
|
159
|
-
}
|