fastify-txstate 3.5.0 → 3.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/lib/analytics.js +2 -2
- package/lib/error.d.ts +2 -2
- package/lib/error.js +2 -2
- package/lib/index.d.ts +38 -3
- package/lib/index.js +7 -7
- package/lib/unified-auth.d.ts +18 -2
- package/lib/unified-auth.js +91 -34
- package/lib-esm/index.d.ts +155 -0
- package/package.json +4 -4
package/lib/analytics.js
CHANGED
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.ElasticAnalyticsClient = exports.LoggingAnalyticsClient = exports.AnalyticsClient = void 0;
|
|
7
|
+
exports.analyticsPlugin = analyticsPlugin;
|
|
7
8
|
const elasticsearch_1 = __importDefault(require("@elastic/elasticsearch"));
|
|
8
9
|
const fastify_shared_1 = require("@txstate-mws/fastify-shared");
|
|
9
10
|
const txstate_utils_1 = require("txstate-utils");
|
|
@@ -114,4 +115,3 @@ function analyticsPlugin(fastify, opts, done) {
|
|
|
114
115
|
});
|
|
115
116
|
done();
|
|
116
117
|
}
|
|
117
|
-
exports.analyticsPlugin = analyticsPlugin;
|
package/lib/error.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export declare class FailedValidationError extends HttpError {
|
|
|
15
15
|
}
|
|
16
16
|
export declare class ValidationError extends HttpError {
|
|
17
17
|
path?: string | undefined;
|
|
18
|
-
type?: "
|
|
19
|
-
constructor(message: string, path?: string | undefined, type?: "
|
|
18
|
+
type?: ValidationMessage["type"] | undefined;
|
|
19
|
+
constructor(message: string, path?: string | undefined, type?: ValidationMessage["type"] | undefined);
|
|
20
20
|
}
|
|
21
21
|
export declare class ValidationErrors extends HttpError {
|
|
22
22
|
errors: ValidationMessage[];
|
package/lib/error.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.ValidationErrors = exports.ValidationError = exports.FailedValidationError = exports.HttpError = void 0;
|
|
4
|
+
exports.fstValidationToMessage = fstValidationToMessage;
|
|
4
5
|
const http_status_codes_1 = require("http-status-codes");
|
|
5
6
|
class HttpError extends Error {
|
|
6
7
|
statusCode;
|
|
@@ -54,4 +55,3 @@ function fstValidationToMessage(v) {
|
|
|
54
55
|
const instancePath = v.keyword === 'required' ? v.instancePath + '/' + v.params.missingProperty : v.instancePath;
|
|
55
56
|
return { message: v.message, path: instancePath.substring(1).replace(/\//g, '.'), type: 'error' };
|
|
56
57
|
}
|
|
57
|
-
exports.fstValidationToMessage = fstValidationToMessage;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
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';
|
|
@@ -27,6 +25,13 @@ export interface FastifyTxStateAuthInfo {
|
|
|
27
25
|
* If all else fails, you can sha256 the session token with a salt.
|
|
28
26
|
*/
|
|
29
27
|
sessionId: string;
|
|
28
|
+
/**
|
|
29
|
+
* The date that the session was created, if available. This is useful for considering
|
|
30
|
+
* tokens before a certain date as invalid. For instance, if you want a logout action
|
|
31
|
+
* to invalidate all tokens created until that point, you can compare this field against
|
|
32
|
+
* the last time they logged out.
|
|
33
|
+
*/
|
|
34
|
+
sessionCreatedAt?: Date;
|
|
30
35
|
/**
|
|
31
36
|
* Some authentication systems allow administrators to impersonate regular users, so that
|
|
32
37
|
* they can see what that user sees and troubleshoot issues. We still want to log the administrator
|
|
@@ -45,6 +50,32 @@ export interface FastifyTxStateAuthInfo {
|
|
|
45
50
|
* this field can help log requests that are authenticated with the other application's token.
|
|
46
51
|
*/
|
|
47
52
|
clientId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* A string that designates the current session as one that has limited authorization. The application will
|
|
55
|
+
* be responsible for checking this field and restricting appropriately.
|
|
56
|
+
*
|
|
57
|
+
* For example, a user who authenticated via non-standard mechanism might be given a scope of 'altlogin' and
|
|
58
|
+
* only a portion of the application's functionality would be available to them.
|
|
59
|
+
*/
|
|
60
|
+
scope?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The token or key that was used to authenticate the request. This is useful for
|
|
63
|
+
* making sub-requests to other APIs that can authenticate with the same token.
|
|
64
|
+
*/
|
|
65
|
+
token: string;
|
|
66
|
+
/**
|
|
67
|
+
* The issuer configuration for the token, if applicable. This helps you generate
|
|
68
|
+
* a proper logout url in multi-issuer environments.
|
|
69
|
+
*/
|
|
70
|
+
issuerConfig?: IssuerConfig;
|
|
71
|
+
}
|
|
72
|
+
export interface IssuerConfig {
|
|
73
|
+
iss: string;
|
|
74
|
+
url?: string;
|
|
75
|
+
publicKey?: string;
|
|
76
|
+
secret?: string;
|
|
77
|
+
validateUrl?: URL;
|
|
78
|
+
logoutUrl?: URL;
|
|
48
79
|
}
|
|
49
80
|
export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
50
81
|
https?: http2.SecureServerOptions;
|
|
@@ -82,6 +113,10 @@ export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
|
82
113
|
declare module 'fastify' {
|
|
83
114
|
interface FastifyRequest {
|
|
84
115
|
auth?: FastifyTxStateAuthInfo;
|
|
116
|
+
/**
|
|
117
|
+
* @deprecated Use `req.auth.token` instead. Just trying to keep everything contained.
|
|
118
|
+
* This will be removed in the next major version.
|
|
119
|
+
*/
|
|
85
120
|
token?: string;
|
|
86
121
|
}
|
|
87
122
|
interface FastifyReply {
|
|
@@ -112,7 +147,7 @@ export declare const devLogger: {
|
|
|
112
147
|
(message?: any, ...optionalParams: any[]): void;
|
|
113
148
|
};
|
|
114
149
|
silent: (msg: any) => void;
|
|
115
|
-
child(bindings: any, options?: any): any;
|
|
150
|
+
child(bindings: any, options?: any): /*elided*/ any;
|
|
116
151
|
};
|
|
117
152
|
export declare const prodLogger: FastifyLoggerOptions;
|
|
118
153
|
export type FastifyInstanceTyped = FastifyInstance<RawServerDefault, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, FastifyBaseLogger, JsonSchemaToTsProvider>;
|
package/lib/index.js
CHANGED
|
@@ -211,7 +211,7 @@ class Server {
|
|
|
211
211
|
DELETE: true
|
|
212
212
|
};
|
|
213
213
|
this.app.addHook('onRequest', async (req, res) => {
|
|
214
|
-
if (!authenticatedMethods[req.method] || req.routeOptions.url === '/health' || (this.swaggerEndpoint && req.routeOptions.url?.startsWith(this.swaggerEndpoint)))
|
|
214
|
+
if (!authenticatedMethods[req.method] || (0, txstate_utils_1.isBlank)(req.routeOptions.url) || req.routeOptions.url === '/health' || req.routeOptions.url === '/.uaService' || (this.swaggerEndpoint && req.routeOptions.url?.startsWith(this.swaggerEndpoint)))
|
|
215
215
|
return;
|
|
216
216
|
try {
|
|
217
217
|
req.auth = await config.authenticate(req);
|
|
@@ -292,12 +292,12 @@ class Server {
|
|
|
292
292
|
});
|
|
293
293
|
this.app.get('/health', { logLevel: 'warn' }, async (req, res) => {
|
|
294
294
|
if (this.shuttingDown) {
|
|
295
|
-
res.log.
|
|
295
|
+
res.log.warn('Returning 503 on /health because we are shutting down/restarting.');
|
|
296
296
|
void res.status(503);
|
|
297
297
|
return 'MAINTENANCE';
|
|
298
298
|
}
|
|
299
299
|
else if (this.healthMessage) {
|
|
300
|
-
res.log.
|
|
300
|
+
res.log.error(this.healthMessage);
|
|
301
301
|
void res.status(500);
|
|
302
302
|
return this.healthMessage;
|
|
303
303
|
}
|
|
@@ -305,7 +305,7 @@ class Server {
|
|
|
305
305
|
const resp = await this.healthCallback();
|
|
306
306
|
const [status, msg] = typeof resp === 'string' ? [500, resp] : [resp?.status, resp?.message];
|
|
307
307
|
if (!!msg || !!status) {
|
|
308
|
-
res.log.
|
|
308
|
+
res.log.error(resp, 'Health check callback failed.');
|
|
309
309
|
void res.status(status ?? 500);
|
|
310
310
|
return msg ?? 'FAIL';
|
|
311
311
|
}
|
|
@@ -394,9 +394,9 @@ this is log into this application and use dev tools to pull your token from the
|
|
|
394
394
|
}
|
|
395
395
|
await this.app.register(swagger_1.default, {
|
|
396
396
|
openapi,
|
|
397
|
-
transform(
|
|
398
|
-
const newSchema = findRefs((0, txstate_utils_1.clone)(schema));
|
|
399
|
-
return { schema: newSchema
|
|
397
|
+
transform(transformArgs) {
|
|
398
|
+
const newSchema = findRefs((0, txstate_utils_1.clone)(transformArgs.schema));
|
|
399
|
+
return { ...transformArgs, schema: newSchema };
|
|
400
400
|
}
|
|
401
401
|
});
|
|
402
402
|
this.swaggerEndpoint = opts?.path ?? opts?.ui?.routePrefix ?? '/docs';
|
package/lib/unified-auth.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { type FastifyReply, type FastifyRequest } from 'fastify';
|
|
2
|
-
import { type FastifyInstanceTyped, type FastifyTxStateAuthInfo } from '.';
|
|
2
|
+
import { type IssuerConfig, type FastifyInstanceTyped, type FastifyTxStateAuthInfo } from '.';
|
|
3
|
+
export interface IssuerConfigRaw extends Omit<IssuerConfig, 'validateUrl' | 'logoutUrl'> {
|
|
4
|
+
validateUrl?: string;
|
|
5
|
+
logoutUrl?: string;
|
|
6
|
+
}
|
|
3
7
|
export declare const uaCookieName: string;
|
|
4
|
-
export declare function unifiedAuthenticate(req: FastifyRequest
|
|
8
|
+
export declare function unifiedAuthenticate(req: FastifyRequest, options?: {
|
|
9
|
+
authenticateAll?: boolean;
|
|
10
|
+
exceptRoutes?: Set<string>;
|
|
11
|
+
usingUaCookieRoutes?: boolean;
|
|
12
|
+
}): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
15
|
+
*/
|
|
5
16
|
export declare function unifiedAuthenticateAll(req: FastifyRequest): Promise<FastifyTxStateAuthInfo>;
|
|
17
|
+
/**
|
|
18
|
+
* This function is available for server-side view code instead of a client-side application
|
|
19
|
+
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
20
|
+
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
21
|
+
*/
|
|
6
22
|
export declare function requireCookieAuth(req: FastifyRequest, res: FastifyReply): Promise<boolean>;
|
|
7
23
|
export declare function registerUaCookieRoutes(app: FastifyInstanceTyped): void;
|
package/lib/unified-auth.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.uaCookieName = void 0;
|
|
4
|
+
exports.unifiedAuthenticate = unifiedAuthenticate;
|
|
5
|
+
exports.unifiedAuthenticateAll = unifiedAuthenticateAll;
|
|
6
|
+
exports.requireCookieAuth = requireCookieAuth;
|
|
7
|
+
exports.registerUaCookieRoutes = registerUaCookieRoutes;
|
|
4
8
|
const crypto_1 = require("crypto");
|
|
5
9
|
const jose_1 = require("jose");
|
|
6
10
|
const txstate_utils_1 = require("txstate-utils");
|
|
@@ -64,10 +68,27 @@ function remoteJWKSet(jwkUrl) {
|
|
|
64
68
|
}
|
|
65
69
|
function processIssuerConfig(config) {
|
|
66
70
|
if (config.iss === 'unified-auth') {
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const validateUrl = (0, txstate_utils_1.isNotBlank)(config.validateUrl)
|
|
72
|
+
? new URL(config.validateUrl, config.url)
|
|
73
|
+
: (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
|
|
74
|
+
? new URL(process.env.UA_URL + '/validateToken')
|
|
75
|
+
: new URL('validateToken', config.url);
|
|
76
|
+
const logoutUrl = (0, txstate_utils_1.isNotBlank)(config.logoutUrl)
|
|
77
|
+
? new URL(config.logoutUrl, config.url)
|
|
78
|
+
: (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
|
|
79
|
+
? new URL(process.env.UA_URL + '/logout')
|
|
80
|
+
: new URL('logout', config.url);
|
|
81
|
+
return {
|
|
82
|
+
...config,
|
|
83
|
+
validateUrl,
|
|
84
|
+
logoutUrl
|
|
85
|
+
};
|
|
69
86
|
}
|
|
70
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
...config,
|
|
89
|
+
validateUrl: undefined,
|
|
90
|
+
logoutUrl: config.logoutUrl ? new URL(config.logoutUrl, config.url) : undefined
|
|
91
|
+
};
|
|
71
92
|
}
|
|
72
93
|
function init() {
|
|
73
94
|
hasInit = true;
|
|
@@ -97,7 +118,7 @@ function tokenFromReq(req) {
|
|
|
97
118
|
if (m2 != null)
|
|
98
119
|
return m2[1];
|
|
99
120
|
}
|
|
100
|
-
async function
|
|
121
|
+
async function unifiedAuthenticateInternal(req) {
|
|
101
122
|
if (!hasInit)
|
|
102
123
|
init();
|
|
103
124
|
const token = tokenFromReq(req);
|
|
@@ -109,32 +130,54 @@ async function unifiedAuthenticate(req) {
|
|
|
109
130
|
await validateCache.get(token, payload);
|
|
110
131
|
req.token = token;
|
|
111
132
|
return {
|
|
133
|
+
token,
|
|
134
|
+
issuerConfig: payload.iss ? issuerConfig.get(payload.iss) : undefined,
|
|
112
135
|
username: payload.sub,
|
|
113
136
|
sessionId: payload.sub + '-' + payload.iat,
|
|
137
|
+
sessionCreatedAt: payload.iat ? new Date(payload.iat * 1000) : undefined,
|
|
114
138
|
clientId: payload.client_id,
|
|
115
|
-
impersonatedBy: payload.act?.sub
|
|
139
|
+
impersonatedBy: payload.act?.sub,
|
|
140
|
+
scope: payload.scope
|
|
116
141
|
};
|
|
117
142
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
async function unifiedAuthenticate(req, options) {
|
|
144
|
+
const auth = await unifiedAuthenticateInternal(req);
|
|
145
|
+
if (options?.usingUaCookieRoutes) {
|
|
146
|
+
options.exceptRoutes ??= new Set();
|
|
147
|
+
options.exceptRoutes.add('/.uaService');
|
|
148
|
+
options.exceptRoutes.add('/.uaRedirect');
|
|
149
|
+
}
|
|
150
|
+
const isAuthenticatedRoute = options?.authenticateAll && (options.exceptRoutes == null || !options.exceptRoutes.has(req.routeOptions.url));
|
|
151
|
+
if (isAuthenticatedRoute) {
|
|
152
|
+
if ((0, txstate_utils_1.isBlank)(auth?.username))
|
|
153
|
+
throw new Error('Request requires authentication.');
|
|
154
|
+
}
|
|
123
155
|
return auth;
|
|
124
156
|
}
|
|
125
|
-
|
|
157
|
+
/**
|
|
158
|
+
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
159
|
+
*/
|
|
160
|
+
async function unifiedAuthenticateAll(req) {
|
|
161
|
+
return (await unifiedAuthenticate(req, { authenticateAll: true }));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* This function is available for server-side view code instead of a client-side application
|
|
165
|
+
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
166
|
+
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
167
|
+
*/
|
|
126
168
|
async function requireCookieAuth(req, res) {
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
if ((0, txstate_utils_1.isBlank)(req.auth?.username)) {
|
|
170
|
+
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
171
|
+
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
172
|
+
loginUrl.searchParams.set('returnUrl', (0, txstate_utils_1.isNotBlank)(process.env.PUBLIC_URL) ? new URL(process.env.PUBLIC_URL + '/.uaService').toString() : new URL('/.uaService', req.url).toString());
|
|
173
|
+
loginUrl.searchParams.set('requestedUrl', req.originalUrl);
|
|
174
|
+
void res.redirect(loginUrl.toString());
|
|
131
175
|
return true;
|
|
132
176
|
}
|
|
133
177
|
else {
|
|
134
178
|
return false;
|
|
135
179
|
}
|
|
136
180
|
}
|
|
137
|
-
exports.requireCookieAuth = requireCookieAuth;
|
|
138
181
|
function registerUaCookieRoutes(app) {
|
|
139
182
|
app.get('/.uaLogout', {
|
|
140
183
|
schema: {
|
|
@@ -147,39 +190,53 @@ function registerUaCookieRoutes(app) {
|
|
|
147
190
|
}
|
|
148
191
|
}
|
|
149
192
|
}, async (req, res) => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
193
|
+
const redirectUrl = req.auth?.issuerConfig?.logoutUrl
|
|
194
|
+
? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
|
|
195
|
+
: (process.env.PUBLIC_URL || new URL('..', req.url).toString());
|
|
153
196
|
return res
|
|
154
197
|
.header('Set-Cookie', `${exports.uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`)
|
|
155
|
-
.redirect(
|
|
198
|
+
.redirect(redirectUrl);
|
|
156
199
|
});
|
|
157
200
|
app.get('/.uaService', {
|
|
158
201
|
schema: {
|
|
159
202
|
querystring: {
|
|
160
203
|
type: 'object',
|
|
161
204
|
properties: {
|
|
162
|
-
unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' }
|
|
205
|
+
unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' },
|
|
206
|
+
requestedUrl: { type: 'string', format: 'uri' }
|
|
163
207
|
},
|
|
164
208
|
required: ['unifiedJwt'],
|
|
165
209
|
additionalProperties: false
|
|
166
|
-
}
|
|
167
|
-
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}, async (req, res) => {
|
|
213
|
+
return res
|
|
214
|
+
.header('Set-Cookie', `${exports.uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`)
|
|
215
|
+
.redirect(req.query.requestedUrl ?? (process.env.PUBLIC_URL || new URL('..', req.url).toString()));
|
|
216
|
+
});
|
|
217
|
+
/**
|
|
218
|
+
* In the case of a client-side application that uses the UA cookie to authenticate,
|
|
219
|
+
* the client code can detect a 401 from the API and redirect the user to this endpoint.
|
|
220
|
+
*
|
|
221
|
+
* This endpoint will redirect the browser to Unified Auth so that the client code does
|
|
222
|
+
* not need to have any configuration for Unified Auth.
|
|
223
|
+
*/
|
|
224
|
+
app.get('/.uaRedirect', {
|
|
225
|
+
schema: {
|
|
226
|
+
querystring: {
|
|
168
227
|
type: 'object',
|
|
169
228
|
properties: {
|
|
170
|
-
|
|
229
|
+
requestedUrl: { type: 'string', format: 'uri' }
|
|
171
230
|
},
|
|
172
|
-
|
|
231
|
+
additionalProperties: false
|
|
173
232
|
}
|
|
174
233
|
}
|
|
175
234
|
}, async (req, res) => {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
.redirect(decodeURIComponent(m[1]));
|
|
235
|
+
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
236
|
+
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
237
|
+
loginUrl.searchParams.set('returnUrl', new URL('.uaService', process.env.PUBLIC_URL || new URL(req.url, req.protocol + '://' + req.hostname)).toString());
|
|
238
|
+
if (req.query.requestedUrl)
|
|
239
|
+
loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
|
|
240
|
+
return res.redirect(loginUrl.toString());
|
|
183
241
|
});
|
|
184
242
|
}
|
|
185
|
-
exports.registerUaCookieRoutes = registerUaCookieRoutes;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { type FastifyDynamicSwaggerOptions } from '@fastify/swagger';
|
|
4
|
+
import { type FastifySwaggerUiOptions } from '@fastify/swagger-ui';
|
|
5
|
+
import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts';
|
|
6
|
+
import { type FastifyInstance, type FastifyRequest, type FastifyReply, type FastifyServerOptions, type FastifyLoggerOptions, type FastifyBaseLogger, type RawServerDefault } from 'fastify';
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
import type http2 from 'node:http2';
|
|
9
|
+
type ErrorHandler = (error: Error, req: FastifyRequest, res: FastifyReply) => Promise<void>;
|
|
10
|
+
export interface FastifyTxStateAuthInfo {
|
|
11
|
+
/**
|
|
12
|
+
* The primary identifier for the user that is making the request, after processing
|
|
13
|
+
* their session token / JWT.
|
|
14
|
+
*/
|
|
15
|
+
username: string;
|
|
16
|
+
/**
|
|
17
|
+
* This should be an identifier for the particular session, so that the same user
|
|
18
|
+
* on different devices/browsers/tabs can be distinguished from one another.
|
|
19
|
+
*
|
|
20
|
+
* It should NOT be usable as a cookie or bearer token, as it will appear in logs. If you
|
|
21
|
+
* use JSON Web Tokens, an easy thing is to combine the username with the `iat` issued
|
|
22
|
+
* date to create something unique but not useful to attackers.
|
|
23
|
+
*
|
|
24
|
+
* For lookup tokens, you can do the same `${username}-${createdAt}` after looking up
|
|
25
|
+
* the session in your database.
|
|
26
|
+
*
|
|
27
|
+
* If all else fails, you can sha256 the session token with a salt.
|
|
28
|
+
*/
|
|
29
|
+
sessionId: string;
|
|
30
|
+
/**
|
|
31
|
+
* Some authentication systems allow administrators to impersonate regular users, so that
|
|
32
|
+
* they can see what that user sees and troubleshoot issues. We still want to log the administrator
|
|
33
|
+
* with any actions they take while impersonating someone, for auditing purposes, so you should
|
|
34
|
+
* fill this field when applicable.
|
|
35
|
+
*
|
|
36
|
+
* This will also be available at `req.auth.impersonatedBy`, so it is possible for your API
|
|
37
|
+
* to implement complicated authorization rules based on whether a user is being impersonated.
|
|
38
|
+
* It sort of defeats the purpose of impersonation, but used sparingly it could prevent administrators
|
|
39
|
+
* from making mistakes.
|
|
40
|
+
*/
|
|
41
|
+
impersonatedBy?: string;
|
|
42
|
+
/**
|
|
43
|
+
* If your API may be accessed by a different client application, such that the user is actually logged
|
|
44
|
+
* into that application instead of yours, but you accept that application's session tokens, filling
|
|
45
|
+
* this field can help log requests that are authenticated with the other application's token.
|
|
46
|
+
*/
|
|
47
|
+
clientId?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
50
|
+
https?: http2.SecureServerOptions;
|
|
51
|
+
validOrigins?: string[];
|
|
52
|
+
validOriginHosts?: string[];
|
|
53
|
+
validOriginSuffixes?: string[];
|
|
54
|
+
skipOriginCheck?: boolean;
|
|
55
|
+
checkOrigin?: (req: FastifyRequest) => boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Run an asynchronous function to check the health of the service.
|
|
58
|
+
*
|
|
59
|
+
* Return a non-empty error message to trigger unhealthy status.
|
|
60
|
+
*
|
|
61
|
+
* Setting a health message with setUnhealthy will override this and prevent it from being executed.
|
|
62
|
+
*/
|
|
63
|
+
checkHealth?: () => Promise<string | {
|
|
64
|
+
status?: number;
|
|
65
|
+
message?: string;
|
|
66
|
+
} | undefined>;
|
|
67
|
+
/**
|
|
68
|
+
* Run an async function to get authentication information out of the request
|
|
69
|
+
* object. Should return an object with at least a username and sessionid (see FastifyTxStateAuthInfo
|
|
70
|
+
* for further detail).
|
|
71
|
+
*
|
|
72
|
+
* The return object will be added to the request object as `req.auth` for later
|
|
73
|
+
* use in your route handlers. It will also be added to the logs in production.
|
|
74
|
+
*
|
|
75
|
+
* IMPORTANT: It is not advisable to return excessive amounts of data here, nor anything
|
|
76
|
+
* particularly sensitive, since it will all be included in every log entry.
|
|
77
|
+
*
|
|
78
|
+
* If this function throws, the client will receive a 401 response.
|
|
79
|
+
*/
|
|
80
|
+
authenticate?: (req: FastifyRequest) => Promise<FastifyTxStateAuthInfo | undefined>;
|
|
81
|
+
}
|
|
82
|
+
declare module 'fastify' {
|
|
83
|
+
interface FastifyRequest {
|
|
84
|
+
auth?: FastifyTxStateAuthInfo;
|
|
85
|
+
}
|
|
86
|
+
interface FastifyReply {
|
|
87
|
+
extraLogInfo: any;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export declare const devLogger: {
|
|
91
|
+
level: string;
|
|
92
|
+
info: (msg: any) => void;
|
|
93
|
+
error: {
|
|
94
|
+
(...data: any[]): void;
|
|
95
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
96
|
+
};
|
|
97
|
+
debug: {
|
|
98
|
+
(...data: any[]): void;
|
|
99
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
100
|
+
};
|
|
101
|
+
fatal: {
|
|
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>;
|
|
118
|
+
export type TxServer = Server;
|
|
119
|
+
export default class Server {
|
|
120
|
+
protected config: FastifyTxStateOptions & {
|
|
121
|
+
http2?: true;
|
|
122
|
+
};
|
|
123
|
+
protected https: boolean;
|
|
124
|
+
protected errorHandlers: ErrorHandler[];
|
|
125
|
+
protected healthMessage?: string;
|
|
126
|
+
protected healthCallback?: () => Promise<string | {
|
|
127
|
+
status?: number;
|
|
128
|
+
message?: string;
|
|
129
|
+
} | undefined>;
|
|
130
|
+
protected shuttingDown: boolean;
|
|
131
|
+
protected sigHandler: (signal: any) => void;
|
|
132
|
+
protected validOrigins: Record<string, boolean>;
|
|
133
|
+
protected validOriginHosts: Record<string, boolean>;
|
|
134
|
+
protected validOriginSuffixes: Set<string>;
|
|
135
|
+
app: FastifyInstanceTyped;
|
|
136
|
+
constructor(config?: FastifyTxStateOptions & {
|
|
137
|
+
http2?: true;
|
|
138
|
+
});
|
|
139
|
+
start(port?: number): Promise<void>;
|
|
140
|
+
addErrorHandler(handler: ErrorHandler): void;
|
|
141
|
+
setUnhealthy(message: string): void;
|
|
142
|
+
setHealthy(): void;
|
|
143
|
+
setValidOrigins(origins: string[]): void;
|
|
144
|
+
setValidOriginHosts(hosts: string[]): void;
|
|
145
|
+
setValidOriginSuffixes(suffixes: string[]): void;
|
|
146
|
+
swagger(opts?: {
|
|
147
|
+
path?: string;
|
|
148
|
+
openapi?: FastifyDynamicSwaggerOptions['openapi'];
|
|
149
|
+
ui?: FastifySwaggerUiOptions;
|
|
150
|
+
}): Promise<void>;
|
|
151
|
+
close(softSeconds?: number): Promise<void>;
|
|
152
|
+
}
|
|
153
|
+
export * from './analytics';
|
|
154
|
+
export * from './error';
|
|
155
|
+
export * from './unified-auth';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-txstate",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "A small wrapper for fastify providing a set of common conventions & utility functions we use.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"@txstate-mws/fastify-shared": "^1.0.9",
|
|
26
26
|
"@types/ua-parser-js": ">=0.7.39",
|
|
27
27
|
"ajv-errors": "^3.0.0",
|
|
28
|
-
"ajv-formats": "^
|
|
28
|
+
"ajv-formats": "^3.0.0",
|
|
29
29
|
"fastify": "^4.9.2",
|
|
30
30
|
"fastify-plugin": "^4.5.1",
|
|
31
31
|
"http-status-codes": "^2.1.4",
|
|
32
|
-
"jose": "^
|
|
32
|
+
"jose": "^6.0.0",
|
|
33
33
|
"txstate-utils": "^1.9.5",
|
|
34
34
|
"ua-parser-js": "^1.0.37"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/chai": "^4.2.14",
|
|
38
38
|
"@types/mocha": "^10.0.0",
|
|
39
|
-
"@types/node": "^
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
40
|
"axios": "^1.6.8",
|
|
41
41
|
"chai": "^4.2.0",
|
|
42
42
|
"eslint-config-standard-with-typescript": "^43.0.0",
|