-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathutils.ts
More file actions
132 lines (115 loc) · 4.25 KB
/
utils.ts
File metadata and controls
132 lines (115 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import type { AuthClientMetadata, AuthClientMetadataDeviceType } from "@t3tools/contracts";
import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
import * as Crypto from "node:crypto";
const SESSION_COOKIE_NAME = "t3_session";
export function resolveSessionCookieName(input: {
readonly mode: "web" | "desktop";
readonly port: number;
}): string {
if (input.mode !== "desktop") {
return SESSION_COOKIE_NAME;
}
return `${SESSION_COOKIE_NAME}_${input.port}`;
}
export function base64UrlEncode(input: string | Uint8Array): string {
const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
return buffer.toString("base64url");
}
export function base64UrlDecodeUtf8(input: string): string {
return Buffer.from(input, "base64url").toString("utf8");
}
export function signPayload(payload: string, secret: Uint8Array): string {
return Crypto.createHmac("sha256", Buffer.from(secret)).update(payload).digest("base64url");
}
export function timingSafeEqualBase64Url(left: string, right: string): boolean {
const leftBuffer = Buffer.from(left, "base64url");
const rightBuffer = Buffer.from(right, "base64url");
if (leftBuffer.length !== rightBuffer.length) {
return false;
}
return Crypto.timingSafeEqual(leftBuffer, rightBuffer);
}
function normalizeNonEmptyString(value: string | null | undefined): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeIpAddress(value: string | null | undefined): string | undefined {
const normalized = normalizeNonEmptyString(value);
if (!normalized) {
return undefined;
}
return normalized.startsWith("::ffff:") ? normalized.slice("::ffff:".length) : normalized;
}
function inferDeviceType(userAgent: string | undefined): AuthClientMetadataDeviceType {
if (!userAgent) {
return "unknown";
}
const normalized = userAgent.toLowerCase();
if (/bot|crawler|spider|slurp|curl|wget/.test(normalized)) {
return "bot";
}
if (/ipad|tablet/.test(normalized)) {
return "tablet";
}
if (/iphone|android.+mobile|mobile/.test(normalized)) {
return "mobile";
}
return "desktop";
}
function inferBrowser(userAgent: string | undefined): string | undefined {
if (!userAgent) {
return undefined;
}
const normalized = userAgent.toLowerCase();
if (/edg\//.test(normalized)) return "Edge";
if (/opr\//.test(normalized)) return "Opera";
if (/firefox\//.test(normalized)) return "Firefox";
if (/electron\//.test(normalized)) return "Electron";
if (/chrome\//.test(normalized) || /crios\//.test(normalized)) return "Chrome";
if (/safari\//.test(normalized) && !/chrome\//.test(normalized)) return "Safari";
return undefined;
}
function inferOs(userAgent: string | undefined): string | undefined {
if (!userAgent) {
return undefined;
}
const normalized = userAgent.toLowerCase();
if (/iphone|ipad|ipod/.test(normalized)) return "iOS";
if (/android/.test(normalized)) return "Android";
if (/mac os x|macintosh/.test(normalized)) return "macOS";
if (/windows nt/.test(normalized)) return "Windows";
if (/linux/.test(normalized)) return "Linux";
return undefined;
}
function readRemoteAddressFromSource(source: unknown): string | undefined {
if (!source || typeof source !== "object") {
return undefined;
}
const candidate = source as {
readonly remoteAddress?: string | null;
readonly socket?: {
readonly remoteAddress?: string | null;
};
};
return normalizeIpAddress(candidate.socket?.remoteAddress ?? candidate.remoteAddress);
}
export function deriveAuthClientMetadata(input: {
readonly request: HttpServerRequest.HttpServerRequest;
readonly label?: string;
}): AuthClientMetadata {
const userAgent = normalizeNonEmptyString(input.request.headers["user-agent"]);
const ipAddress = readRemoteAddressFromSource(input.request.source);
const os = inferOs(userAgent);
const browser = inferBrowser(userAgent);
return {
...(input.label ? { label: input.label } : {}),
...(ipAddress ? { ipAddress } : {}),
...(userAgent ? { userAgent } : {}),
deviceType: inferDeviceType(userAgent),
...(os ? { os } : {}),
...(browser ? { browser } : {}),
};
}