fastify-txstate 3.6.9 → 4.0.1
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 +392 -7
- package/lib/analytics.d.ts +5 -2
- package/lib/analytics.js +28 -33
- package/lib/error.d.ts +1 -10
- package/lib/error.js +7 -28
- package/lib/filestorage.d.ts +1 -1
- package/lib/filestorage.js +27 -31
- package/lib/index.d.ts +9 -194
- package/lib/index.js +9 -422
- package/lib/jwt-auth.d.ts +98 -0
- package/lib/jwt-auth.js +491 -0
- package/lib/oauth.d.ts +49 -0
- package/lib/oauth.js +272 -0
- package/lib/postformdata.js +13 -17
- package/{lib-esm/index.d.ts → lib/server.d.ts} +69 -37
- package/lib/server.js +441 -0
- package/lib/unified-auth.d.ts +13 -7
- package/lib/unified-auth.js +48 -174
- package/package.json +27 -25
- package/lib-esm/index.js +0 -20
- package/lib-esm/package.json +0 -3
package/lib/oauth.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { decodeJwt } from 'jose';
|
|
3
|
+
import { htmlEncode, isBlank, isNotBlank } from 'txstate-utils';
|
|
4
|
+
import { apiBaseUrl, uiBaseUrl } from "./server.js";
|
|
5
|
+
import { accessTokenCookieName, getOAuthDiscovery, getOAuthIssuerUrls, init, oauthCookieName, refreshCookieName, registeredExceptRoutes, registeredOptionalRoutes, toInternalUrl, wrapRefreshToken } from "./jwt-auth.js";
|
|
6
|
+
/**
|
|
7
|
+
* Register cookie-based OAuth login/logout endpoints. Uses the authorization code flow
|
|
8
|
+
* with PKCE (S256) to exchange a code for tokens, then stores the ID token in an HttpOnly
|
|
9
|
+
* cookie. The access token and refresh token are stored in separate cookies (optionally
|
|
10
|
+
* encrypted via OAUTH_COOKIE_SECRET) so that the ID token can be transparently refreshed
|
|
11
|
+
* by jwtAuthenticate when it expires, and the access token is available at
|
|
12
|
+
* `req.auth.accessToken` for calling provider APIs.
|
|
13
|
+
*
|
|
14
|
+
* Requires OAUTH_COOKIE_CLIENT_ID environment variable. OAUTH_COOKIE_CLIENT_SECRET is
|
|
15
|
+
* optional — PKCE provides the security for the code exchange, but some providers require
|
|
16
|
+
* a client secret even with PKCE. OAUTH_COOKIE_SECRET is optional — if set, the access
|
|
17
|
+
* token and refresh token cookies are encrypted with AES-256-GCM; if not, they are stored
|
|
18
|
+
* as plaintext (still HttpOnly and Secure).
|
|
19
|
+
*
|
|
20
|
+
* Trusted issuers are configured via OAUTH_URLS or JWT_TRUSTED_ISSUERS (see jwt-auth.ts).
|
|
21
|
+
*
|
|
22
|
+
* Registers three routes:
|
|
23
|
+
* - `/.oauthRedirect` - Redirects to the OAuth provider's login page. The client passes
|
|
24
|
+
* `requestedUrl` (required) which is sent to the provider as the `state` parameter,
|
|
25
|
+
* round-tripped back, and used as the redirect destination after login.
|
|
26
|
+
* - `/.oauthCallback` - Handles the provider's redirect, exchanges the code for tokens
|
|
27
|
+
* using the PKCE code verifier. Sets the ID token (or JWT access token as fallback),
|
|
28
|
+
* access token, and refresh token as cookies.
|
|
29
|
+
* - `/.oauthLogout` - Clears all OAuth cookies and redirects to the provider's logout
|
|
30
|
+
* endpoint if available.
|
|
31
|
+
*/
|
|
32
|
+
export function registerOAuthCookieRoutes(app, options) {
|
|
33
|
+
const clientId = process.env.OAUTH_COOKIE_CLIENT_ID;
|
|
34
|
+
if (!clientId)
|
|
35
|
+
throw new Error('OAUTH_COOKIE_CLIENT_ID environment variable must be set when using registerOAuthCookieRoutes.');
|
|
36
|
+
init();
|
|
37
|
+
const clientSecret = process.env.OAUTH_COOKIE_CLIENT_SECRET;
|
|
38
|
+
registeredExceptRoutes.add('/.oauthCallback');
|
|
39
|
+
registeredExceptRoutes.add('/.oauthRedirect');
|
|
40
|
+
registeredOptionalRoutes.add('/.oauthLogout');
|
|
41
|
+
const callbackPath = '/.oauthCallback';
|
|
42
|
+
const pkceVerifierCookieName = oauthCookieName + '_pkce';
|
|
43
|
+
const pkceVerifierCookieRegex = new RegExp(`${pkceVerifierCookieName}=([A-Za-z0-9_-]+)`, 'v');
|
|
44
|
+
const issuerCookieName = oauthCookieName + '_iss';
|
|
45
|
+
const issuerCookieRegex = new RegExp(`${issuerCookieName}=([^;]+)`, 'v');
|
|
46
|
+
// flush any pending cookies queued during token refresh by jwtAuthenticate
|
|
47
|
+
app.addHook('onSend', async (req, res) => {
|
|
48
|
+
if (req.pendingOAuthCookies?.length) {
|
|
49
|
+
for (const cookie of req.pendingOAuthCookies)
|
|
50
|
+
void res.header('Set-Cookie', cookie);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
app.get('/.oauthRedirect', {
|
|
54
|
+
schema: {
|
|
55
|
+
querystring: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
requestedUrl: { type: 'string', format: 'uri' },
|
|
59
|
+
scope: { type: 'string' },
|
|
60
|
+
issuer: { type: 'string', format: 'uri' }
|
|
61
|
+
},
|
|
62
|
+
required: ['requestedUrl'],
|
|
63
|
+
additionalProperties: false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, async (req, res) => {
|
|
67
|
+
if (req.originChecker && !req.originChecker.check(req.query.requestedUrl, req.hostname)) {
|
|
68
|
+
void res.status(403);
|
|
69
|
+
return 'Requested URL failed origin check.';
|
|
70
|
+
}
|
|
71
|
+
const issuerUrls = getOAuthIssuerUrls();
|
|
72
|
+
if (!issuerUrls.length)
|
|
73
|
+
throw new Error('No OAuth issuers are configured. Set OAUTH_URLS or include oauth issuers in JWT_TRUSTED_ISSUERS.');
|
|
74
|
+
// if multiple issuers and no issuer specified, show a login selection page
|
|
75
|
+
if (!req.query.issuer && issuerUrls.length > 1 && options?.loginPage) {
|
|
76
|
+
const issuers = issuerUrls.map(iss => {
|
|
77
|
+
const redirectUrl = new URL(apiBaseUrl(req) + '/.oauthRedirect');
|
|
78
|
+
redirectUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
|
|
79
|
+
if (req.query.scope)
|
|
80
|
+
redirectUrl.searchParams.set('scope', req.query.scope);
|
|
81
|
+
redirectUrl.searchParams.set('issuer', iss);
|
|
82
|
+
return { issuerUrl: iss, redirectHref: redirectUrl.toString() };
|
|
83
|
+
});
|
|
84
|
+
void res.type('text/html');
|
|
85
|
+
return options.loginPage(issuers);
|
|
86
|
+
}
|
|
87
|
+
const issuerUrl = req.query.issuer && issuerUrls.includes(req.query.issuer)
|
|
88
|
+
? req.query.issuer
|
|
89
|
+
: issuerUrls[0];
|
|
90
|
+
const discovery = await getOAuthDiscovery(issuerUrl);
|
|
91
|
+
if (!discovery?.authorization_endpoint)
|
|
92
|
+
throw new Error(`OAuth issuer ${issuerUrl} does not have an authorization endpoint.`);
|
|
93
|
+
const codeVerifier = randomBytes(32).toString('base64url');
|
|
94
|
+
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
95
|
+
const redirectUri = apiBaseUrl(req) + callbackPath;
|
|
96
|
+
const authUrl = new URL(discovery.authorization_endpoint);
|
|
97
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
98
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
99
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
100
|
+
const isGoogle = discovery.authorization_endpoint.includes('accounts.google.com');
|
|
101
|
+
const scopeParts = new Set((req.query.scope ?? 'openid').split(' '));
|
|
102
|
+
for (const s of options?.scopes ?? [])
|
|
103
|
+
scopeParts.add(s);
|
|
104
|
+
if (!isGoogle && !req.query.scope)
|
|
105
|
+
scopeParts.add('offline_access');
|
|
106
|
+
authUrl.searchParams.set('scope', [...scopeParts].join(' '));
|
|
107
|
+
authUrl.searchParams.set('state', req.query.requestedUrl);
|
|
108
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
109
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
110
|
+
if (isGoogle) {
|
|
111
|
+
authUrl.searchParams.set('access_type', 'offline');
|
|
112
|
+
authUrl.searchParams.set('prompt', 'consent');
|
|
113
|
+
}
|
|
114
|
+
// store the code verifier and chosen issuer in short-lived HttpOnly cookies
|
|
115
|
+
void res.header('Set-Cookie', `${pkceVerifierCookieName}=${codeVerifier}; Path=${callbackPath}; Secure; HttpOnly; SameSite=Lax; Max-Age=600`);
|
|
116
|
+
void res.header('Set-Cookie', `${issuerCookieName}=${encodeURIComponent(issuerUrl)}; Path=${callbackPath}; Secure; HttpOnly; SameSite=Lax; Max-Age=600`);
|
|
117
|
+
return await res.redirect(authUrl.toString());
|
|
118
|
+
});
|
|
119
|
+
app.get(callbackPath, {
|
|
120
|
+
schema: {
|
|
121
|
+
querystring: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
code: { type: 'string' },
|
|
125
|
+
state: { type: 'string' }
|
|
126
|
+
},
|
|
127
|
+
required: ['code', 'state'],
|
|
128
|
+
additionalProperties: false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, async (req, res) => {
|
|
132
|
+
const verifierMatch = req.headers.cookie?.match(pkceVerifierCookieRegex);
|
|
133
|
+
if (!verifierMatch) {
|
|
134
|
+
void res.status(403);
|
|
135
|
+
return 'Missing PKCE code verifier. The login flow may have expired.';
|
|
136
|
+
}
|
|
137
|
+
const codeVerifier = verifierMatch[1];
|
|
138
|
+
const issuerUrls = getOAuthIssuerUrls();
|
|
139
|
+
const issuerMatch = req.headers.cookie?.match(issuerCookieRegex);
|
|
140
|
+
const issuerUrl = issuerMatch ? decodeURIComponent(issuerMatch[1]) : issuerUrls[0];
|
|
141
|
+
const discovery = await getOAuthDiscovery(issuerUrl);
|
|
142
|
+
if (!discovery?.token_endpoint)
|
|
143
|
+
throw new Error(`OAuth issuer ${issuerUrl} does not have a token endpoint.`);
|
|
144
|
+
const redirectUri = apiBaseUrl(req) + callbackPath;
|
|
145
|
+
const body = {
|
|
146
|
+
grant_type: 'authorization_code',
|
|
147
|
+
code: req.query.code,
|
|
148
|
+
redirect_uri: redirectUri,
|
|
149
|
+
client_id: clientId,
|
|
150
|
+
code_verifier: codeVerifier
|
|
151
|
+
};
|
|
152
|
+
if (clientSecret)
|
|
153
|
+
body.client_secret = clientSecret;
|
|
154
|
+
const tokenResp = await fetch(toInternalUrl(discovery.token_endpoint), {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
157
|
+
body: new URLSearchParams(body)
|
|
158
|
+
});
|
|
159
|
+
if (!tokenResp.ok) {
|
|
160
|
+
req.log.error(`OAuth token exchange failed: ${tokenResp.status} ${await tokenResp.text()}`);
|
|
161
|
+
void res.status(502);
|
|
162
|
+
return 'OAuth token exchange failed.';
|
|
163
|
+
}
|
|
164
|
+
const tokens = await tokenResp.json();
|
|
165
|
+
// prefer id_token, fall back to access_token if it's a JWT (some providers
|
|
166
|
+
// like Okta and Microsoft issue JWT access tokens)
|
|
167
|
+
let sessionToken = tokens.id_token;
|
|
168
|
+
if (!sessionToken && tokens.access_token) {
|
|
169
|
+
try {
|
|
170
|
+
decodeJwt(tokens.access_token);
|
|
171
|
+
sessionToken = tokens.access_token;
|
|
172
|
+
}
|
|
173
|
+
catch { /* not a JWT, can't use it */ }
|
|
174
|
+
}
|
|
175
|
+
if (!sessionToken) {
|
|
176
|
+
req.log.error('OAuth token response did not include a usable JWT (no id_token and access_token is not a JWT).');
|
|
177
|
+
void res.status(502);
|
|
178
|
+
return 'OAuth provider did not return a usable JWT.';
|
|
179
|
+
}
|
|
180
|
+
const destination = isNotBlank(req.query.state) ? req.query.state : uiBaseUrl(req);
|
|
181
|
+
const cookies = [
|
|
182
|
+
`${oauthCookieName}=${sessionToken}; Path=/; Secure; HttpOnly; SameSite=Lax`,
|
|
183
|
+
`${pkceVerifierCookieName}=; Path=${callbackPath}; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
|
|
184
|
+
`${issuerCookieName}=; Path=${callbackPath}; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
|
|
185
|
+
];
|
|
186
|
+
if (tokens.access_token) {
|
|
187
|
+
cookies.push(`${accessTokenCookieName}=${wrapRefreshToken(tokens.access_token)}; Path=/; Secure; HttpOnly; SameSite=Lax`);
|
|
188
|
+
}
|
|
189
|
+
if (tokens.refresh_token) {
|
|
190
|
+
cookies.push(`${refreshCookieName}=${wrapRefreshToken(tokens.refresh_token)}; Path=/; Secure; HttpOnly; SameSite=Lax`);
|
|
191
|
+
}
|
|
192
|
+
for (const cookie of cookies)
|
|
193
|
+
void res.header('Set-Cookie', cookie);
|
|
194
|
+
void res.type('text/html');
|
|
195
|
+
return `<!DOCTYPE html>
|
|
196
|
+
<html lang="en">
|
|
197
|
+
<head>
|
|
198
|
+
<meta charset="UTF-8">
|
|
199
|
+
<meta http-equiv="refresh" content="0; url=${htmlEncode(destination)}">
|
|
200
|
+
<title>Logging in...</title>
|
|
201
|
+
</head>
|
|
202
|
+
<body>
|
|
203
|
+
</body>
|
|
204
|
+
</html>`;
|
|
205
|
+
});
|
|
206
|
+
app.get('/.oauthLogout', {
|
|
207
|
+
schema: {
|
|
208
|
+
querystring: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
returnUrl: { type: 'string', format: 'uri' }
|
|
212
|
+
},
|
|
213
|
+
additionalProperties: false
|
|
214
|
+
},
|
|
215
|
+
headers: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
cookie: { type: 'string', pattern: `${oauthCookieName}=[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+` }
|
|
219
|
+
},
|
|
220
|
+
required: ['cookie']
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}, async (req, res) => {
|
|
224
|
+
if (req.query.returnUrl && req.originChecker && !req.originChecker.check(req.query.returnUrl, req.hostname)) {
|
|
225
|
+
void res.status(403);
|
|
226
|
+
return 'Return URL failed origin check.';
|
|
227
|
+
}
|
|
228
|
+
const postLogoutDestination = req.query.returnUrl ?? uiBaseUrl(req);
|
|
229
|
+
let redirectUrl = postLogoutDestination;
|
|
230
|
+
if (req.auth?.issuerConfig?.logoutUrl) {
|
|
231
|
+
const logoutUrl = new URL(req.auth.issuerConfig.logoutUrl);
|
|
232
|
+
if (isNotBlank(req.auth.token))
|
|
233
|
+
logoutUrl.searchParams.set('id_token_hint', req.auth.token);
|
|
234
|
+
logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutDestination);
|
|
235
|
+
redirectUrl = logoutUrl.toString();
|
|
236
|
+
}
|
|
237
|
+
const cookies = [
|
|
238
|
+
`${oauthCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
|
|
239
|
+
`${accessTokenCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`,
|
|
240
|
+
`${refreshCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
|
|
241
|
+
];
|
|
242
|
+
for (const cookie of cookies)
|
|
243
|
+
void res.header('Set-Cookie', cookie);
|
|
244
|
+
return `<!DOCTYPE html>
|
|
245
|
+
<html lang="en">
|
|
246
|
+
<head>
|
|
247
|
+
<meta charset="UTF-8">
|
|
248
|
+
<meta http-equiv="refresh" content="0; url=${htmlEncode(redirectUrl)}">
|
|
249
|
+
<title>Logging out...</title>
|
|
250
|
+
</head>
|
|
251
|
+
<body>
|
|
252
|
+
</body>
|
|
253
|
+
</html>`;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* This function is available for server-side view code instead of a client-side application
|
|
258
|
+
* using a framework. It will automatically redirect the user through the OAuth login flow
|
|
259
|
+
* (via /.oauthRedirect, which must be registered by registerOAuthCookieRoutes) and return
|
|
260
|
+
* true if they are not authenticated. Otherwise it simply returns false.
|
|
261
|
+
*/
|
|
262
|
+
export async function requireCookieAuthOAuth(req, res) {
|
|
263
|
+
if (isBlank(req.auth?.username)) {
|
|
264
|
+
const redirectUrl = new URL(apiBaseUrl(req) + '/.oauthRedirect');
|
|
265
|
+
redirectUrl.searchParams.set('requestedUrl', apiBaseUrl(req) + req.originalUrl);
|
|
266
|
+
void res.redirect(redirectUrl.toString());
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
package/lib/postformdata.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const node_stream_1 = require("node:stream");
|
|
5
|
-
const web_1 = require("node:stream/web");
|
|
6
|
-
async function postFormData(url, fields, headers = {}) {
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
export async function postFormData(url, fields, headers = {}) {
|
|
7
4
|
const encoder = new TextEncoder();
|
|
8
5
|
const boundary = `${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
|
|
9
6
|
const footer = `--${boundary}--\r\n`;
|
|
@@ -18,7 +15,7 @@ async function postFormData(url, fields, headers = {}) {
|
|
|
18
15
|
}
|
|
19
16
|
let i = 0;
|
|
20
17
|
let part = 'header';
|
|
21
|
-
const stream = new
|
|
18
|
+
const stream = new ReadableStream({
|
|
22
19
|
async pull(controller) {
|
|
23
20
|
if (i === chunks.length) {
|
|
24
21
|
controller.enqueue(encoder.encode(footer));
|
|
@@ -31,27 +28,26 @@ async function postFormData(url, fields, headers = {}) {
|
|
|
31
28
|
part = 'content';
|
|
32
29
|
}
|
|
33
30
|
else if (part === 'content') {
|
|
34
|
-
const
|
|
35
|
-
if (done)
|
|
31
|
+
const result = await chunk.contentReader.read();
|
|
32
|
+
if (result.done)
|
|
36
33
|
part = 'footer';
|
|
37
34
|
else
|
|
38
|
-
controller.enqueue(value);
|
|
35
|
+
controller.enqueue(result.value);
|
|
39
36
|
}
|
|
40
|
-
else
|
|
37
|
+
else {
|
|
41
38
|
controller.enqueue(encoder.encode(chunk.footer));
|
|
42
|
-
i
|
|
39
|
+
i += 1;
|
|
43
40
|
part = 'header';
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
},
|
|
47
44
|
cancel() {
|
|
48
45
|
for (const chunk of chunks) {
|
|
49
|
-
|
|
50
|
-
chunk.contentReader.cancel();
|
|
51
|
-
}
|
|
46
|
+
void chunk.contentReader.cancel();
|
|
52
47
|
}
|
|
53
48
|
}
|
|
54
49
|
});
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- duplex and node ReadableStream aren't in standard RequestInit types
|
|
55
51
|
return await fetch(url, {
|
|
56
52
|
method: 'POST',
|
|
57
53
|
headers,
|
|
@@ -74,12 +70,12 @@ class FormDataChunk {
|
|
|
74
70
|
if (isFileField(field)) {
|
|
75
71
|
this.header += `; filename="${field.filename ?? field.name}"\r\nContent-Type: ${field.filetype ?? 'application/octet-stream'}`;
|
|
76
72
|
this.contentsize = field.filesize;
|
|
77
|
-
this.contentReader = (field.value instanceof
|
|
73
|
+
this.contentReader = (field.value instanceof Readable ? ReadableStream.from(field.value) : field.value).getReader();
|
|
78
74
|
}
|
|
79
75
|
else {
|
|
80
76
|
const encoded = encoder.encode(field.value);
|
|
81
77
|
this.contentsize = encoded.length;
|
|
82
|
-
this.contentReader = new
|
|
78
|
+
this.contentReader = new ReadableStream({
|
|
83
79
|
start: controller => {
|
|
84
80
|
controller.enqueue(encoded);
|
|
85
81
|
controller.close();
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
1
|
import { type FastifyDynamicSwaggerOptions } from '@fastify/swagger';
|
|
4
2
|
import { type FastifySwaggerUiOptions } from '@fastify/swagger-ui';
|
|
5
3
|
import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts';
|
|
6
|
-
import { type FastifyInstance, type FastifyRequest, type FastifyReply, type FastifyServerOptions, type
|
|
4
|
+
import { type FastifyInstance, type FastifyRequest, type FastifyReply, type FastifyServerOptions, type FastifyBaseLogger, type RawServerDefault } from 'fastify';
|
|
5
|
+
import pino from 'pino';
|
|
7
6
|
import http from 'node:http';
|
|
8
7
|
import type http2 from 'node:http2';
|
|
9
8
|
type ErrorHandler = (error: Error, req: FastifyRequest, res: FastifyReply) => Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Get the base URL for this API. Uses PUBLIC_URL if set, otherwise derives
|
|
11
|
+
* from the request's protocol and hostname.
|
|
12
|
+
*/
|
|
13
|
+
export declare function apiBaseUrl(req: FastifyRequest): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get the base URL for the UI that this API serves. Uses UI_URL if set,
|
|
16
|
+
* otherwise assumes the API lives at a subpath (e.g. /api) and the UI is
|
|
17
|
+
* one level up. Falls back to the API base URL if there's no parent path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function uiBaseUrl(req: FastifyRequest): string;
|
|
10
20
|
export interface FastifyTxStateAuthInfo {
|
|
11
21
|
/**
|
|
12
22
|
* The primary identifier for the user that is making the request, after processing
|
|
@@ -27,6 +37,13 @@ export interface FastifyTxStateAuthInfo {
|
|
|
27
37
|
* If all else fails, you can sha256 the session token with a salt.
|
|
28
38
|
*/
|
|
29
39
|
sessionId: string;
|
|
40
|
+
/**
|
|
41
|
+
* The date that the session was created, if available. This is useful for considering
|
|
42
|
+
* tokens before a certain date as invalid. For instance, if you want a logout action
|
|
43
|
+
* to invalidate all tokens created until that point, you can compare this field against
|
|
44
|
+
* the last time they logged out.
|
|
45
|
+
*/
|
|
46
|
+
sessionCreatedAt?: Date;
|
|
30
47
|
/**
|
|
31
48
|
* Some authentication systems allow administrators to impersonate regular users, so that
|
|
32
49
|
* they can see what that user sees and troubleshoot issues. We still want to log the administrator
|
|
@@ -45,6 +62,39 @@ export interface FastifyTxStateAuthInfo {
|
|
|
45
62
|
* this field can help log requests that are authenticated with the other application's token.
|
|
46
63
|
*/
|
|
47
64
|
clientId?: string;
|
|
65
|
+
/**
|
|
66
|
+
* A string that designates the current session as one that has limited authorization. The application will
|
|
67
|
+
* be responsible for checking this field and restricting appropriately.
|
|
68
|
+
*
|
|
69
|
+
* For example, a user who authenticated via non-standard mechanism might be given a scope of 'altlogin' and
|
|
70
|
+
* only a portion of the application's functionality would be available to them.
|
|
71
|
+
*/
|
|
72
|
+
scope?: string;
|
|
73
|
+
/**
|
|
74
|
+
* The token or key that was used to authenticate the request. This is useful for
|
|
75
|
+
* making sub-requests to other APIs that can authenticate with the same token.
|
|
76
|
+
*/
|
|
77
|
+
token: string;
|
|
78
|
+
/**
|
|
79
|
+
* The issuer configuration for the token, if applicable. This helps you generate
|
|
80
|
+
* a proper logout url in multi-issuer environments.
|
|
81
|
+
*/
|
|
82
|
+
issuerConfig?: IssuerConfig;
|
|
83
|
+
/**
|
|
84
|
+
* The OAuth access token, if available. This is useful when your API needs to make
|
|
85
|
+
* requests to the provider's APIs on behalf of the user (e.g. Google Drive, Microsoft
|
|
86
|
+
* Graph). Only populated when using cookie-based OAuth with a provider that returns
|
|
87
|
+
* an access token during the code exchange.
|
|
88
|
+
*/
|
|
89
|
+
accessToken?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface IssuerConfig {
|
|
92
|
+
iss: string;
|
|
93
|
+
url?: string;
|
|
94
|
+
publicKey?: string;
|
|
95
|
+
secret?: string;
|
|
96
|
+
validateUrl?: URL;
|
|
97
|
+
logoutUrl?: URL;
|
|
48
98
|
}
|
|
49
99
|
export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
50
100
|
https?: http2.SecureServerOptions;
|
|
@@ -82,39 +132,24 @@ export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
|
82
132
|
declare module 'fastify' {
|
|
83
133
|
interface FastifyRequest {
|
|
84
134
|
auth?: FastifyTxStateAuthInfo;
|
|
135
|
+
originChecker?: OriginChecker;
|
|
85
136
|
}
|
|
86
137
|
interface FastifyReply {
|
|
87
138
|
extraLogInfo: any;
|
|
88
139
|
}
|
|
89
140
|
}
|
|
90
|
-
export declare const devLogger:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
(...data: any[]): void;
|
|
103
|
-
(message?: any, ...optionalParams: any[]): void;
|
|
104
|
-
};
|
|
105
|
-
warn: {
|
|
106
|
-
(...data: any[]): void;
|
|
107
|
-
(message?: any, ...optionalParams: any[]): void;
|
|
108
|
-
};
|
|
109
|
-
trace: {
|
|
110
|
-
(...data: any[]): void;
|
|
111
|
-
(message?: any, ...optionalParams: any[]): void;
|
|
112
|
-
};
|
|
113
|
-
silent: (msg: any) => void;
|
|
114
|
-
child(bindings: any, options?: any): any;
|
|
115
|
-
};
|
|
116
|
-
export declare const prodLogger: FastifyLoggerOptions;
|
|
117
|
-
export type FastifyInstanceTyped = FastifyInstance<RawServerDefault, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, FastifyBaseLogger, JsonSchemaToTsProvider>;
|
|
141
|
+
export declare const devLogger: pino.Logger<never, boolean>;
|
|
142
|
+
export declare const prodLogger: pino.Logger<never, boolean>;
|
|
143
|
+
export type FastifyInstanceTyped = FastifyInstance<RawServerDefault, http.IncomingMessage, http.ServerResponse, FastifyBaseLogger, JsonSchemaToTsProvider>;
|
|
144
|
+
export declare class OriginChecker {
|
|
145
|
+
protected validOrigins: Record<string, boolean>;
|
|
146
|
+
protected validOriginHosts: Record<string, boolean>;
|
|
147
|
+
protected validOriginSuffixes: Set<string>;
|
|
148
|
+
setValidOrigins(origins: string[]): void;
|
|
149
|
+
setValidOriginHosts(hosts: string[]): void;
|
|
150
|
+
setValidOriginSuffixes(suffixes: string[]): void;
|
|
151
|
+
check(hostname: string, requestHostname?: string): boolean;
|
|
152
|
+
}
|
|
118
153
|
export type TxServer = Server;
|
|
119
154
|
export default class Server {
|
|
120
155
|
protected config: FastifyTxStateOptions & {
|
|
@@ -129,9 +164,8 @@ export default class Server {
|
|
|
129
164
|
} | undefined>;
|
|
130
165
|
protected shuttingDown: boolean;
|
|
131
166
|
protected sigHandler: (signal: any) => void;
|
|
132
|
-
protected
|
|
133
|
-
protected
|
|
134
|
-
protected validOriginSuffixes: Set<string>;
|
|
167
|
+
protected originChecker: OriginChecker;
|
|
168
|
+
protected swaggerEndpoint: string | undefined;
|
|
135
169
|
app: FastifyInstanceTyped;
|
|
136
170
|
constructor(config?: FastifyTxStateOptions & {
|
|
137
171
|
http2?: true;
|
|
@@ -150,6 +184,4 @@ export default class Server {
|
|
|
150
184
|
}): Promise<void>;
|
|
151
185
|
close(softSeconds?: number): Promise<void>;
|
|
152
186
|
}
|
|
153
|
-
export
|
|
154
|
-
export * from './error';
|
|
155
|
-
export * from './unified-auth';
|
|
187
|
+
export {};
|