-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathToken.ts
More file actions
151 lines (133 loc) · 3.83 KB
/
Token.ts
File metadata and controls
151 lines (133 loc) · 3.83 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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { HeadersInit } from 'node-fetch';
/**
* Safety buffer in seconds to consider a token expired before its actual expiration time.
* This prevents using tokens that are about to expire during in-flight requests.
*/
const EXPIRATION_BUFFER_SECONDS = 30;
/**
* Represents an access token with optional metadata and lifecycle management.
*/
export default class Token {
private readonly _accessToken: string;
private readonly _tokenType: string;
private readonly _expiresAt?: Date;
private readonly _refreshToken?: string;
private readonly _scopes?: string[];
constructor(
accessToken: string,
options?: {
tokenType?: string;
expiresAt?: Date;
refreshToken?: string;
scopes?: string[];
},
) {
this._accessToken = accessToken;
this._tokenType = options?.tokenType ?? 'Bearer';
this._expiresAt = options?.expiresAt;
this._refreshToken = options?.refreshToken;
this._scopes = options?.scopes;
}
/**
* The access token string.
*/
get accessToken(): string {
return this._accessToken;
}
/**
* The token type (e.g., "Bearer").
*/
get tokenType(): string {
return this._tokenType;
}
/**
* The expiration time of the token, if known.
*/
get expiresAt(): Date | undefined {
return this._expiresAt;
}
/**
* The refresh token, if available.
*/
get refreshToken(): string | undefined {
return this._refreshToken;
}
/**
* The scopes associated with this token.
*/
get scopes(): string[] | undefined {
return this._scopes;
}
/**
* Checks if the token has expired, including a safety buffer.
* Returns false if expiration time is unknown.
*/
isExpired(): boolean {
if (!this._expiresAt) {
return false;
}
const now = new Date();
const bufferMs = EXPIRATION_BUFFER_SECONDS * 1000;
return this._expiresAt.getTime() - bufferMs <= now.getTime();
}
/**
* Sets the Authorization header on the provided headers object.
* @param headers - The headers object to modify
* @returns The modified headers object with Authorization set
*/
setAuthHeader(headers: HeadersInit): HeadersInit {
return {
...headers,
Authorization: `${this._tokenType} ${this._accessToken}`,
};
}
/**
* Creates a Token from a JWT string, extracting the expiration time from the payload.
* If the JWT cannot be decoded, the token is created without expiration info.
* The server will validate the token anyway, so decoding failures are handled gracefully.
* @param jwt - The JWT token string
* @param options - Additional token options (tokenType, refreshToken, scopes)
* @returns A new Token instance with expiration extracted from the JWT (if available)
*/
static fromJWT(
jwt: string,
options?: {
tokenType?: string;
refreshToken?: string;
scopes?: string[];
},
): Token {
let expiresAt: Date | undefined;
try {
const parts = jwt.split('.');
if (parts.length >= 2) {
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
const decoded = JSON.parse(payload);
if (typeof decoded.exp === 'number') {
expiresAt = new Date(decoded.exp * 1000);
}
}
} catch {
// If we can't decode the JWT, we'll proceed without expiration info
// The server will validate the token anyway
}
return new Token(jwt, {
tokenType: options?.tokenType,
expiresAt,
refreshToken: options?.refreshToken,
scopes: options?.scopes,
});
}
/**
* Converts the token to a plain object for serialization.
*/
toJSON(): Record<string, unknown> {
return {
accessToken: this._accessToken,
tokenType: this._tokenType,
expiresAt: this._expiresAt?.toISOString(),
refreshToken: this._refreshToken,
scopes: this._scopes,
};
}
}