@openlivesync/server 1.0.2
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 +366 -0
- package/dist/auth/decode-token.d.ts +40 -0
- package/dist/auth/decode-token.d.ts.map +1 -0
- package/dist/auth/decode-token.js +93 -0
- package/dist/auth/decode-token.js.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token-auth.d.ts +18 -0
- package/dist/auth/token-auth.d.ts.map +1 -0
- package/dist/auth/token-auth.js +45 -0
- package/dist/auth/token-auth.js.map +1 -0
- package/dist/connection.d.ts +36 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +170 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +135 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +17 -0
- package/dist/protocol.js.map +1 -0
- package/dist/room-manager.d.ts +19 -0
- package/dist/room-manager.d.ts.map +1 -0
- package/dist/room-manager.js +34 -0
- package/dist/room-manager.js.map +1 -0
- package/dist/room.d.ts +41 -0
- package/dist/room.d.ts.map +1 -0
- package/dist/room.js +150 -0
- package/dist/room.js.map +1 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +105 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/chat-storage.d.ts +19 -0
- package/dist/storage/chat-storage.d.ts.map +1 -0
- package/dist/storage/chat-storage.js +6 -0
- package/dist/storage/chat-storage.js.map +1 -0
- package/dist/storage/in-memory.d.ts +11 -0
- package/dist/storage/in-memory.d.ts.map +1 -0
- package/dist/storage/in-memory.js +35 -0
- package/dist/storage/in-memory.js.map +1 -0
- package/dist/storage/mysql.d.ts +19 -0
- package/dist/storage/mysql.d.ts.map +1 -0
- package/dist/storage/mysql.js +70 -0
- package/dist/storage/mysql.js.map +1 -0
- package/dist/storage/postgres.d.ts +21 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +70 -0
- package/dist/storage/postgres.js.map +1 -0
- package/dist/storage/sqlite.d.ts +15 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +71 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/package.json +51 -0
- package/src/auth/decode-token.test.ts +119 -0
- package/src/auth/decode-token.ts +138 -0
- package/src/auth/index.ts +16 -0
- package/src/auth/token-auth.test.ts +95 -0
- package/src/auth/token-auth.ts +55 -0
- package/src/connection.test.ts +339 -0
- package/src/connection.ts +204 -0
- package/src/index.ts +80 -0
- package/src/protocol.test.ts +29 -0
- package/src/protocol.ts +137 -0
- package/src/room-manager.ts +45 -0
- package/src/room.test.ts +175 -0
- package/src/room.ts +207 -0
- package/src/server.test.ts +223 -0
- package/src/server.ts +153 -0
- package/src/storage/chat-storage.ts +23 -0
- package/src/storage/db-types.d.ts +43 -0
- package/src/storage/in-memory.test.ts +96 -0
- package/src/storage/in-memory.ts +52 -0
- package/src/storage/mysql.ts +117 -0
- package/src/storage/postgres.ts +117 -0
- package/src/storage/sqlite.ts +120 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { SignJWT } from "jose";
|
|
3
|
+
import { decodeAccessToken } from "./decode-token.js";
|
|
4
|
+
|
|
5
|
+
const TEST_SECRET = new TextEncoder().encode("test-secret");
|
|
6
|
+
|
|
7
|
+
describe("decodeAccessToken", () => {
|
|
8
|
+
it("returns null for empty or invalid token", async () => {
|
|
9
|
+
expect(await decodeAccessToken("")).toBeNull();
|
|
10
|
+
expect(await decodeAccessToken("not-a-jwt")).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("decodes JWT payload (decode-only) and normalizes sub, email, name, provider", async () => {
|
|
14
|
+
const token = await new SignJWT({
|
|
15
|
+
sub: "user-123",
|
|
16
|
+
email: "alice@example.com",
|
|
17
|
+
name: "Alice",
|
|
18
|
+
iss: "https://accounts.google.com",
|
|
19
|
+
})
|
|
20
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
21
|
+
.setIssuedAt()
|
|
22
|
+
.setExpirationTime("1h")
|
|
23
|
+
.sign(TEST_SECRET);
|
|
24
|
+
|
|
25
|
+
const decoded = await decodeAccessToken(token);
|
|
26
|
+
expect(decoded).not.toBeNull();
|
|
27
|
+
expect(decoded!.sub).toBe("user-123");
|
|
28
|
+
expect(decoded!.email).toBe("alice@example.com");
|
|
29
|
+
expect(decoded!.name).toBe("Alice");
|
|
30
|
+
expect(decoded!.provider).toBe("google");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses preferred_username for email when email claim missing (Microsoft-style)", async () => {
|
|
34
|
+
const token = await new SignJWT({
|
|
35
|
+
sub: "oid-456",
|
|
36
|
+
preferred_username: "bob@tenant.com",
|
|
37
|
+
name: "Bob",
|
|
38
|
+
iss: "https://login.microsoftonline.com/tenant-id/v2.0",
|
|
39
|
+
})
|
|
40
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
41
|
+
.setIssuedAt()
|
|
42
|
+
.setExpirationTime("1h")
|
|
43
|
+
.sign(TEST_SECRET);
|
|
44
|
+
|
|
45
|
+
const decoded = await decodeAccessToken(token);
|
|
46
|
+
expect(decoded).not.toBeNull();
|
|
47
|
+
expect(decoded!.email).toBe("bob@tenant.com");
|
|
48
|
+
expect(decoded!.provider).toBe("microsoft");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("treats unknown issuer as custom provider", async () => {
|
|
52
|
+
const token = await new SignJWT({
|
|
53
|
+
sub: "custom-user",
|
|
54
|
+
email: "custom@example.com",
|
|
55
|
+
iss: "https://my-auth.example.com",
|
|
56
|
+
})
|
|
57
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
58
|
+
.setIssuedAt()
|
|
59
|
+
.setExpirationTime("1h")
|
|
60
|
+
.sign(TEST_SECRET);
|
|
61
|
+
|
|
62
|
+
const decoded = await decodeAccessToken(token);
|
|
63
|
+
expect(decoded).not.toBeNull();
|
|
64
|
+
expect(decoded!.provider).toBe("custom");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("with custom decodeOnly returns normalized payload without verification", async () => {
|
|
68
|
+
const token = await new SignJWT({
|
|
69
|
+
sub: "decode-only-user",
|
|
70
|
+
email: "do@example.com",
|
|
71
|
+
iss: "https://custom.issuer.example",
|
|
72
|
+
})
|
|
73
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
74
|
+
.setIssuedAt()
|
|
75
|
+
.setExpirationTime("1h")
|
|
76
|
+
.sign(TEST_SECRET);
|
|
77
|
+
|
|
78
|
+
const decoded = await decodeAccessToken(token, {
|
|
79
|
+
custom: { decodeOnly: true },
|
|
80
|
+
});
|
|
81
|
+
expect(decoded).not.toBeNull();
|
|
82
|
+
expect(decoded!.sub).toBe("decode-only-user");
|
|
83
|
+
expect(decoded!.email).toBe("do@example.com");
|
|
84
|
+
expect(decoded!.provider).toBe("custom");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("with Microsoft config sets issuer/audience (verification failure returns null)", async () => {
|
|
88
|
+
const token = await new SignJWT({
|
|
89
|
+
sub: "ms-user",
|
|
90
|
+
preferred_username: "ms@tenant.com",
|
|
91
|
+
iss: "https://login.microsoftonline.com/tenant-123/v2.0",
|
|
92
|
+
})
|
|
93
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
94
|
+
.setIssuedAt()
|
|
95
|
+
.setExpirationTime("1h")
|
|
96
|
+
.sign(TEST_SECRET);
|
|
97
|
+
|
|
98
|
+
const decoded = await decodeAccessToken(token, {
|
|
99
|
+
microsoft: { tenantId: "tenant-123", clientId: "client-456" },
|
|
100
|
+
});
|
|
101
|
+
expect(decoded).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns null when verification throws (e.g. invalid signature)", async () => {
|
|
105
|
+
const token = await new SignJWT({
|
|
106
|
+
sub: "user",
|
|
107
|
+
iss: "https://accounts.google.com",
|
|
108
|
+
})
|
|
109
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
110
|
+
.setIssuedAt()
|
|
111
|
+
.setExpirationTime("1h")
|
|
112
|
+
.sign(TEST_SECRET);
|
|
113
|
+
|
|
114
|
+
const decoded = await decodeAccessToken(token, {
|
|
115
|
+
google: { clientId: "google-client" },
|
|
116
|
+
});
|
|
117
|
+
expect(decoded).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode and optionally verify OAuth/OpenID access tokens (JWT).
|
|
3
|
+
* Supports Google, Microsoft, and custom providers (JWKS or decode-only).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
7
|
+
|
|
8
|
+
export interface DecodedToken {
|
|
9
|
+
sub: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
iss?: string;
|
|
13
|
+
provider?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuthGoogleConfig {
|
|
17
|
+
/** Optional: validate audience (client ID). */
|
|
18
|
+
clientId?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthMicrosoftConfig {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
/** Optional: validate audience (client ID). */
|
|
24
|
+
clientId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuthCustomConfig {
|
|
28
|
+
/** JWKS URL for signature verification. */
|
|
29
|
+
jwksUrl?: string;
|
|
30
|
+
/** Expected issuer (iss claim). */
|
|
31
|
+
issuer?: string;
|
|
32
|
+
/** If true, only decode payload (no verification). Use for dev or trusted tokens. */
|
|
33
|
+
decodeOnly?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AuthOptions {
|
|
37
|
+
google?: AuthGoogleConfig;
|
|
38
|
+
microsoft?: AuthMicrosoftConfig;
|
|
39
|
+
custom?: AuthCustomConfig;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const GOOGLE_ISSUER = "https://accounts.google.com";
|
|
43
|
+
const GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
|
|
44
|
+
|
|
45
|
+
function normalizePayload(payload: Record<string, unknown>): DecodedToken {
|
|
46
|
+
const sub = typeof payload.sub === "string" ? payload.sub : "";
|
|
47
|
+
const email =
|
|
48
|
+
typeof payload.email === "string"
|
|
49
|
+
? payload.email
|
|
50
|
+
: typeof payload.preferred_username === "string"
|
|
51
|
+
? payload.preferred_username
|
|
52
|
+
: undefined;
|
|
53
|
+
const name =
|
|
54
|
+
typeof payload.name === "string"
|
|
55
|
+
? payload.name
|
|
56
|
+
: [payload.given_name, payload.family_name]
|
|
57
|
+
.filter((x) => typeof x === "string")
|
|
58
|
+
.join(" ")
|
|
59
|
+
.trim() || undefined;
|
|
60
|
+
const iss = typeof payload.iss === "string" ? payload.iss : undefined;
|
|
61
|
+
let provider: string | undefined;
|
|
62
|
+
if (iss) {
|
|
63
|
+
if (iss.includes("accounts.google.com")) provider = "google";
|
|
64
|
+
else if (iss.includes("login.microsoftonline.com")) provider = "microsoft";
|
|
65
|
+
else provider = "custom";
|
|
66
|
+
}
|
|
67
|
+
return { sub, email, name, iss, provider };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getGoogleJwksUrl(): URL {
|
|
71
|
+
return new URL(GOOGLE_JWKS_URL);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getMicrosoftJwksUrl(tenantId: string): URL {
|
|
75
|
+
return new URL(
|
|
76
|
+
`https://login.microsoftonline.com/${tenantId}/discovery/v2.0/keys`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decode (and optionally verify) an access token JWT.
|
|
82
|
+
* Returns normalized claims (sub, email, name, provider) or null on failure.
|
|
83
|
+
* If no auth options are provided or a provider uses decodeOnly, only decoding is performed (no signature verification).
|
|
84
|
+
*/
|
|
85
|
+
export async function decodeAccessToken(
|
|
86
|
+
token: string,
|
|
87
|
+
options?: AuthOptions
|
|
88
|
+
): Promise<DecodedToken | null> {
|
|
89
|
+
if (!token || typeof token !== "string") return null;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const decoded = decodeJwt(token);
|
|
93
|
+
const payload = decoded as unknown as Record<string, unknown>;
|
|
94
|
+
const iss = typeof payload.iss === "string" ? payload.iss : undefined;
|
|
95
|
+
|
|
96
|
+
// Determine which provider config applies and whether to verify
|
|
97
|
+
let verifyUrl: URL | null = null;
|
|
98
|
+
let issuer: string | undefined;
|
|
99
|
+
let audience: string | undefined;
|
|
100
|
+
|
|
101
|
+
if (options?.google && iss?.includes("accounts.google.com")) {
|
|
102
|
+
verifyUrl = getGoogleJwksUrl();
|
|
103
|
+
issuer = GOOGLE_ISSUER;
|
|
104
|
+
audience = options.google.clientId;
|
|
105
|
+
} else if (
|
|
106
|
+
options?.microsoft &&
|
|
107
|
+
iss?.includes("login.microsoftonline.com")
|
|
108
|
+
) {
|
|
109
|
+
const tenantId = options.microsoft.tenantId;
|
|
110
|
+
verifyUrl = getMicrosoftJwksUrl(tenantId);
|
|
111
|
+
issuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;
|
|
112
|
+
audience = options.microsoft.clientId;
|
|
113
|
+
} else if (options?.custom) {
|
|
114
|
+
if (options.custom.decodeOnly) {
|
|
115
|
+
return normalizePayload(payload);
|
|
116
|
+
}
|
|
117
|
+
if (options.custom.jwksUrl) {
|
|
118
|
+
verifyUrl = new URL(options.custom.jwksUrl);
|
|
119
|
+
issuer = options.custom.issuer;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// No verification configured: decode only
|
|
124
|
+
if (!verifyUrl) {
|
|
125
|
+
return normalizePayload(payload);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const JWKS = createRemoteJWKSet(verifyUrl);
|
|
129
|
+
const verifyOptions: { issuer?: string; audience?: string } = {};
|
|
130
|
+
if (issuer) verifyOptions.issuer = issuer;
|
|
131
|
+
if (audience) verifyOptions.audience = audience;
|
|
132
|
+
|
|
133
|
+
const { payload: verified } = await jwtVerify(token, JWKS, verifyOptions);
|
|
134
|
+
return normalizePayload(verified as unknown as Record<string, unknown>);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module: decode and verify OAuth/OpenID access tokens.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
decodeAccessToken,
|
|
7
|
+
type DecodedToken,
|
|
8
|
+
type AuthOptions,
|
|
9
|
+
type AuthGoogleConfig,
|
|
10
|
+
type AuthMicrosoftConfig,
|
|
11
|
+
type AuthCustomConfig,
|
|
12
|
+
} from "./decode-token.js";
|
|
13
|
+
export {
|
|
14
|
+
createTokenAuth,
|
|
15
|
+
type CreateTokenAuthOptions,
|
|
16
|
+
} from "./token-auth.js";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { SignJWT } from "jose";
|
|
3
|
+
import { createTokenAuth } from "./token-auth.js";
|
|
4
|
+
import type { IncomingMessage } from "node:http";
|
|
5
|
+
|
|
6
|
+
const TEST_SECRET = new TextEncoder().encode("test-secret");
|
|
7
|
+
|
|
8
|
+
function mockRequest(overrides: { url?: string; headers?: Record<string, string | string[] | undefined> } = {}): IncomingMessage {
|
|
9
|
+
return {
|
|
10
|
+
url: overrides.url ?? "/",
|
|
11
|
+
headers: overrides.headers ?? {},
|
|
12
|
+
} as IncomingMessage;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("createTokenAuth", () => {
|
|
16
|
+
it("returns UserInfo when token is in query access_token", async () => {
|
|
17
|
+
const token = await new SignJWT({
|
|
18
|
+
sub: "user-q",
|
|
19
|
+
email: "q@example.com",
|
|
20
|
+
name: "Query User",
|
|
21
|
+
})
|
|
22
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
23
|
+
.setIssuedAt()
|
|
24
|
+
.setExpirationTime("1h")
|
|
25
|
+
.sign(TEST_SECRET);
|
|
26
|
+
|
|
27
|
+
const onAuth = createTokenAuth({});
|
|
28
|
+
const req = mockRequest({ url: `http://localhost/live?access_token=${token}` });
|
|
29
|
+
const result = await onAuth(req);
|
|
30
|
+
expect(result).not.toBeNull();
|
|
31
|
+
expect(result!.userId).toBe("user-q");
|
|
32
|
+
expect(result!.email).toBe("q@example.com");
|
|
33
|
+
expect(result!.name).toBe("Query User");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns UserInfo when token is in Authorization Bearer header", async () => {
|
|
37
|
+
const token = await new SignJWT({
|
|
38
|
+
sub: "user-h",
|
|
39
|
+
email: "h@example.com",
|
|
40
|
+
name: "Header User",
|
|
41
|
+
})
|
|
42
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
43
|
+
.setIssuedAt()
|
|
44
|
+
.setExpirationTime("1h")
|
|
45
|
+
.sign(TEST_SECRET);
|
|
46
|
+
|
|
47
|
+
const onAuth = createTokenAuth({});
|
|
48
|
+
const req = mockRequest({ headers: { authorization: `Bearer ${token}` } });
|
|
49
|
+
const result = await onAuth(req);
|
|
50
|
+
expect(result).not.toBeNull();
|
|
51
|
+
expect(result!.userId).toBe("user-h");
|
|
52
|
+
expect(result!.email).toBe("h@example.com");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns null when no token in request", async () => {
|
|
56
|
+
const onAuth = createTokenAuth({});
|
|
57
|
+
const req = mockRequest({ url: "/live" });
|
|
58
|
+
const result = await onAuth(req);
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns null when token is invalid", async () => {
|
|
63
|
+
const onAuth = createTokenAuth({});
|
|
64
|
+
const req = mockRequest({ url: "http://localhost/?access_token=not-a-jwt" });
|
|
65
|
+
const result = await onAuth(req);
|
|
66
|
+
expect(result).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("uses custom tokenFromRequest when provided", async () => {
|
|
70
|
+
const token = await new SignJWT({ sub: "custom", email: "c@example.com" })
|
|
71
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
72
|
+
.setIssuedAt()
|
|
73
|
+
.setExpirationTime("1h")
|
|
74
|
+
.sign(TEST_SECRET);
|
|
75
|
+
|
|
76
|
+
const tokenFromRequest = vi.fn((req: IncomingMessage) => {
|
|
77
|
+
return (req as { _token?: string })._token ?? null;
|
|
78
|
+
});
|
|
79
|
+
const req = mockRequest() as IncomingMessage & { _token?: string };
|
|
80
|
+
req._token = token;
|
|
81
|
+
|
|
82
|
+
const onAuth = createTokenAuth({}, { tokenFromRequest });
|
|
83
|
+
const result = await onAuth(req);
|
|
84
|
+
expect(result).not.toBeNull();
|
|
85
|
+
expect(result!.userId).toBe("custom");
|
|
86
|
+
expect(tokenFromRequest).toHaveBeenCalledWith(req);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns null when tokenFromRequest returns null", async () => {
|
|
90
|
+
const tokenFromRequest = vi.fn(() => null);
|
|
91
|
+
const onAuth = createTokenAuth({}, { tokenFromRequest });
|
|
92
|
+
const result = await onAuth(mockRequest());
|
|
93
|
+
expect(result).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to use access token only at WebSocket connect (query or header).
|
|
3
|
+
* Returns an onAuth function that reads the token from the request and decodes it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IncomingMessage } from "node:http";
|
|
7
|
+
import type { UserInfo } from "../protocol.js";
|
|
8
|
+
import { decodeAccessToken } from "./decode-token.js";
|
|
9
|
+
import type { AuthOptions } from "./decode-token.js";
|
|
10
|
+
|
|
11
|
+
function defaultTokenFromRequest(req: IncomingMessage): string | null {
|
|
12
|
+
const url = req.url ?? "";
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(url, "http://localhost");
|
|
15
|
+
const fromQuery = u.searchParams.get("access_token");
|
|
16
|
+
if (fromQuery) return fromQuery;
|
|
17
|
+
} catch {
|
|
18
|
+
// ignore URL parse errors
|
|
19
|
+
}
|
|
20
|
+
const auth = req.headers.authorization;
|
|
21
|
+
if (typeof auth === "string" && /^Bearer\s+/i.test(auth)) {
|
|
22
|
+
return auth.replace(/^Bearer\s+/i, "").trim() || null;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CreateTokenAuthOptions {
|
|
28
|
+
/** Custom way to extract token from the upgrade request. Default: query access_token or Authorization Bearer. */
|
|
29
|
+
tokenFromRequest?: (req: IncomingMessage) => string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns an onAuth function that reads the access token from the request (query or header),
|
|
34
|
+
* decodes it with decodeAccessToken, and returns UserInfo (userId, name, email, provider).
|
|
35
|
+
* Use this so the token is used only at connect; the connection is then recognized for its lifetime.
|
|
36
|
+
*/
|
|
37
|
+
export function createTokenAuth(
|
|
38
|
+
authOptions: AuthOptions,
|
|
39
|
+
options?: CreateTokenAuthOptions
|
|
40
|
+
): (request: IncomingMessage) => Promise<UserInfo | null> {
|
|
41
|
+
const tokenFromRequest = options?.tokenFromRequest ?? defaultTokenFromRequest;
|
|
42
|
+
|
|
43
|
+
return async (request: IncomingMessage): Promise<UserInfo | null> => {
|
|
44
|
+
const token = tokenFromRequest(request);
|
|
45
|
+
if (!token) return null;
|
|
46
|
+
const decoded = await decodeAccessToken(token, authOptions);
|
|
47
|
+
if (!decoded) return null;
|
|
48
|
+
return {
|
|
49
|
+
userId: decoded.sub,
|
|
50
|
+
name: decoded.name,
|
|
51
|
+
email: decoded.email,
|
|
52
|
+
provider: decoded.provider,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|