fastify-txstate 3.6.9 → 4.0.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/README.md +354 -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 +8 -194
- package/lib/index.js +8 -422
- package/lib/oauth.d.ts +71 -0
- package/lib/oauth.js +507 -0
- package/lib/postformdata.js +13 -17
- package/{lib-esm/index.d.ts → lib/server.d.ts} +69 -37
- package/lib/server.js +440 -0
- package/lib/unified-auth.d.ts +2 -2
- package/lib/unified-auth.js +55 -47
- package/package.json +27 -25
- package/lib-esm/index.js +0 -20
- package/lib-esm/package.json +0 -3
package/lib/index.js
CHANGED
|
@@ -1,422 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
-
};
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.prodLogger = exports.devLogger = void 0;
|
|
21
|
-
const ajv_1 = __importDefault(require("ajv"));
|
|
22
|
-
const swagger_1 = __importDefault(require("@fastify/swagger"));
|
|
23
|
-
const swagger_ui_1 = __importDefault(require("@fastify/swagger-ui"));
|
|
24
|
-
const fastify_shared_1 = require("@txstate-mws/fastify-shared");
|
|
25
|
-
const ajv_errors_1 = __importDefault(require("ajv-errors"));
|
|
26
|
-
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
27
|
-
const fastify_1 = require("fastify");
|
|
28
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
|
-
const node_http_1 = __importDefault(require("node:http"));
|
|
30
|
-
const txstate_utils_1 = require("txstate-utils");
|
|
31
|
-
const error_1 = require("./error");
|
|
32
|
-
exports.devLogger = {
|
|
33
|
-
level: 'info',
|
|
34
|
-
info: (msg) => { console.info(msg.req ? `${msg.req.method} ${msg.req.url}` : msg.res ? `${msg.res.statusCode} - ${msg.responseTime}` : msg); },
|
|
35
|
-
error: console.error,
|
|
36
|
-
debug: console.debug,
|
|
37
|
-
fatal: console.error,
|
|
38
|
-
warn: console.warn,
|
|
39
|
-
trace: console.trace,
|
|
40
|
-
silent: (msg) => { },
|
|
41
|
-
child(bindings, options) { return this; }
|
|
42
|
-
};
|
|
43
|
-
exports.prodLogger = {
|
|
44
|
-
level: 'info',
|
|
45
|
-
serializers: {
|
|
46
|
-
req(req) {
|
|
47
|
-
return {
|
|
48
|
-
method: req.method,
|
|
49
|
-
url: req.url.replace(/(token|unifiedJwt)=[\w.]+/i, '$1=redacted'),
|
|
50
|
-
remoteAddress: req.ip,
|
|
51
|
-
traceparent: req.headers.traceparent
|
|
52
|
-
};
|
|
53
|
-
},
|
|
54
|
-
res(res) {
|
|
55
|
-
return {
|
|
56
|
-
statusCode: res.statusCode,
|
|
57
|
-
url: res.request?.url.replace(/(token|unifiedJwt)=[\w.]+/i, '$1=redacted'),
|
|
58
|
-
length: Number((0, txstate_utils_1.toArray)(res.getHeader?.('content-length'))[0]),
|
|
59
|
-
...res.extraLogInfo,
|
|
60
|
-
auth: (0, txstate_utils_1.omit)(res.request?.auth ?? res.extraLogInfo?.auth ?? {}, 'token', 'issuerConfig')
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
class Server {
|
|
66
|
-
config;
|
|
67
|
-
https = false;
|
|
68
|
-
errorHandlers = [];
|
|
69
|
-
healthMessage;
|
|
70
|
-
healthCallback;
|
|
71
|
-
shuttingDown = false;
|
|
72
|
-
sigHandler;
|
|
73
|
-
validOrigins = {};
|
|
74
|
-
validOriginHosts = {};
|
|
75
|
-
validOriginSuffixes = new Set();
|
|
76
|
-
swaggerEndpoint;
|
|
77
|
-
app;
|
|
78
|
-
constructor(config = {}) {
|
|
79
|
-
this.config = config;
|
|
80
|
-
try {
|
|
81
|
-
const key = node_fs_1.default.readFileSync('/securekeys/private.key');
|
|
82
|
-
const cert = node_fs_1.default.readFileSync('/securekeys/cert.pem');
|
|
83
|
-
config.https = {
|
|
84
|
-
...config.https,
|
|
85
|
-
allowHTTP1: true,
|
|
86
|
-
key,
|
|
87
|
-
cert,
|
|
88
|
-
minVersion: 'TLSv1.2'
|
|
89
|
-
};
|
|
90
|
-
config.http2 = true;
|
|
91
|
-
this.https = true;
|
|
92
|
-
}
|
|
93
|
-
catch (e) {
|
|
94
|
-
this.https = false;
|
|
95
|
-
delete config.https;
|
|
96
|
-
}
|
|
97
|
-
if (typeof config.logger === 'undefined') {
|
|
98
|
-
config.logger = process.env.NODE_ENV === 'development'
|
|
99
|
-
? exports.devLogger
|
|
100
|
-
: exports.prodLogger;
|
|
101
|
-
}
|
|
102
|
-
if (process.env.TRUST_PROXY != null) {
|
|
103
|
-
if (['true', '1'].includes(process.env.TRUST_PROXY))
|
|
104
|
-
config.trustProxy = true;
|
|
105
|
-
else
|
|
106
|
-
config.trustProxy = process.env.TRUST_PROXY;
|
|
107
|
-
}
|
|
108
|
-
config.ajv = { ...config.ajv, plugins: [...(config.ajv?.plugins ?? []), ajv_errors_1.default, [ajv_formats_1.default, { mode: 'fast' }]], customOptions: { ...config.ajv?.customOptions, allErrors: true, strictSchema: false, coerceTypes: true } };
|
|
109
|
-
this.healthCallback = config.checkHealth;
|
|
110
|
-
this.app = (0, fastify_1.fastify)(config);
|
|
111
|
-
this.app.addHook('onRoute', route => {
|
|
112
|
-
if (!route.schema?.body)
|
|
113
|
-
return;
|
|
114
|
-
const missingResponse = route.schema?.response == null;
|
|
115
|
-
const response400 = (0, txstate_utils_1.set)(fastify_shared_1.validatedResponse.properties.messages, 'description', 'Basic validation failure. This means that the UI provided input that failed validation as defined in the openapi specification published by the API. The UI is at fault and should be re-coded to avoid sending invalid data.');
|
|
116
|
-
let newSchema = (0, txstate_utils_1.set)(route.schema ?? {}, 'response.400', response400);
|
|
117
|
-
const response422 = (0, txstate_utils_1.set)(fastify_shared_1.validatedResponse, 'description', 'Validation failure. This means that the user provided an invalid object. The user should be shown their error so that they can correct it.');
|
|
118
|
-
newSchema = (0, txstate_utils_1.set)(newSchema, 'response.422', response422);
|
|
119
|
-
if (missingResponse) {
|
|
120
|
-
newSchema.response['200'] = {
|
|
121
|
-
description: 'Success. Return type has not been specified.',
|
|
122
|
-
type: 'object'
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
route.schema = newSchema;
|
|
126
|
-
});
|
|
127
|
-
this.app.addHook('preValidation', (req, res, done) => {
|
|
128
|
-
if (req.body != null && req.routeOptions.schema?.body)
|
|
129
|
-
(0, txstate_utils_1.destroyNulls)(req.body);
|
|
130
|
-
done();
|
|
131
|
-
});
|
|
132
|
-
// use Ajv to validate responses instead of @fastify/json-fast-stringify since ajv does
|
|
133
|
-
// a better job with recursive types and we don't want to have different behavior between
|
|
134
|
-
// input and output validation
|
|
135
|
-
const ajv = new ajv_1.default(config.ajv.customOptions);
|
|
136
|
-
for (const pluginConfig of config.ajv.plugins ?? []) {
|
|
137
|
-
const [plugin, opts] = (0, txstate_utils_1.toArray)(pluginConfig);
|
|
138
|
-
plugin(ajv, opts);
|
|
139
|
-
}
|
|
140
|
-
this.app.setSerializerCompiler((route) => {
|
|
141
|
-
const schema = route.schema;
|
|
142
|
-
const validate = schema == null ? ajv.compile({ type: 'object' }) : ajv.compile(schema);
|
|
143
|
-
return data => {
|
|
144
|
-
/**
|
|
145
|
-
* Ajv unfortunately treats optional properties as non-nullable, so they're allowed to
|
|
146
|
-
* be undefined but not allowed to be null. Worse, with `coerceTypes`, null will be converted
|
|
147
|
-
* to empty string or 0 or false. This is silly behavior, so we're converting all nulls to
|
|
148
|
-
* undefined before we validate.
|
|
149
|
-
*/
|
|
150
|
-
if (schema != null)
|
|
151
|
-
(0, txstate_utils_1.destroyNulls)((0, txstate_utils_1.stringifyDates)(data));
|
|
152
|
-
if (!validate(data))
|
|
153
|
-
throw new Error('Output validation failed. ' + validate.errors?.[0].instancePath + ': ' + validate.errors?.[0].message);
|
|
154
|
-
return JSON.stringify(data);
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
this.app.addHook('onRequest', (req, res, done) => {
|
|
158
|
-
res.extraLogInfo = {};
|
|
159
|
-
done();
|
|
160
|
-
});
|
|
161
|
-
if (!config.skipOriginCheck && !process.env.SKIP_ORIGIN_CHECK) {
|
|
162
|
-
this.setValidOrigins([...(config.validOrigins ?? []), ...(process.env.VALID_ORIGINS?.split(',') ?? [])]);
|
|
163
|
-
this.setValidOriginHosts([...(config.validOriginHosts ?? []), ...(process.env.VALID_ORIGIN_HOSTS?.split(',') ?? [])]);
|
|
164
|
-
this.setValidOriginSuffixes([...(config.validOriginSuffixes ?? []), ...(process.env.VALID_ORIGIN_SUFFIXES?.split(',') ?? [])]);
|
|
165
|
-
this.app.addHook('onRequest', async (req, res) => {
|
|
166
|
-
if (!req.headers.origin)
|
|
167
|
-
return;
|
|
168
|
-
let passed = this.validOrigins[req.headers.origin];
|
|
169
|
-
if (!passed && req.headers.origin === 'null')
|
|
170
|
-
passed = process.env.NODE_ENV === 'development';
|
|
171
|
-
else if (!passed) {
|
|
172
|
-
const parsedOrigin = new URL(req.headers.origin);
|
|
173
|
-
if (req.hostname.replace(/:\d+$/, '') === parsedOrigin.hostname)
|
|
174
|
-
passed = true;
|
|
175
|
-
if (this.validOriginHosts[parsedOrigin.hostname])
|
|
176
|
-
passed = true;
|
|
177
|
-
if (!passed && this.validOriginSuffixes.size > 0) {
|
|
178
|
-
const originParts = parsedOrigin.hostname.split('.');
|
|
179
|
-
for (let i = 0; i < originParts.length; i++) {
|
|
180
|
-
const suffix = originParts.slice(i).join('.');
|
|
181
|
-
if (this.validOriginSuffixes.has(suffix))
|
|
182
|
-
passed = true;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (!passed && config.checkOrigin?.(req))
|
|
187
|
-
passed = true;
|
|
188
|
-
if (!passed) {
|
|
189
|
-
await res.status(403).send('Origin check failed. Suspected XSRF attack.');
|
|
190
|
-
return res;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
void res.header('Access-Control-Allow-Origin', req.headers.origin);
|
|
194
|
-
void res.header('Access-Control-Max-Age', '600'); // ask browser to skip pre-flights for 10 minutes after a yes
|
|
195
|
-
if (req.headers['access-control-request-method'])
|
|
196
|
-
void res.header('access-control-allow-methods', req.headers['access-control-request-method']);
|
|
197
|
-
if (req.headers['access-control-request-headers'])
|
|
198
|
-
void res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
this.app.options('*', async (req, res) => {
|
|
202
|
-
await res.send();
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
if (config.authenticate) {
|
|
206
|
-
const authenticatedMethods = {
|
|
207
|
-
GET: true,
|
|
208
|
-
POST: true,
|
|
209
|
-
PUT: true,
|
|
210
|
-
PATCH: true,
|
|
211
|
-
DELETE: true
|
|
212
|
-
};
|
|
213
|
-
this.app.addHook('onRequest', async (req, res) => {
|
|
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
|
-
return;
|
|
216
|
-
try {
|
|
217
|
-
req.auth = await config.authenticate(req);
|
|
218
|
-
}
|
|
219
|
-
catch (e) {
|
|
220
|
-
await res.status(401).send('Failed to authenticate.');
|
|
221
|
-
return res;
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
this.app.addHook('onSend', this.https && process.env.NODE_ENV !== 'development'
|
|
226
|
-
? async (_, resp) => {
|
|
227
|
-
void resp.removeHeader('X-Powered-By');
|
|
228
|
-
void resp.header('Strict-Transport-Security', 'max-age=31536000');
|
|
229
|
-
if (resp.getHeader('content-type') === 'text/html')
|
|
230
|
-
void resp.type('text/html; charset=utf-8');
|
|
231
|
-
}
|
|
232
|
-
: async (_, resp) => {
|
|
233
|
-
void resp.removeHeader('X-Powered-By');
|
|
234
|
-
if (resp.getHeader('content-type') === 'text/html')
|
|
235
|
-
void resp.type('text/html; charset=utf-8');
|
|
236
|
-
});
|
|
237
|
-
this.app.setNotFoundHandler((req, res) => { void res.status(404).send('Not Found.'); });
|
|
238
|
-
this.app.setErrorHandler(async (err, req, res) => {
|
|
239
|
-
req.log.warn(err);
|
|
240
|
-
for (const errorHandler of this.errorHandlers) {
|
|
241
|
-
if (!res.sent)
|
|
242
|
-
await errorHandler(err, req, res);
|
|
243
|
-
}
|
|
244
|
-
if (!res.sent) {
|
|
245
|
-
if (err instanceof error_1.FailedValidationError) {
|
|
246
|
-
await res.status(err.statusCode).send(err.errors);
|
|
247
|
-
}
|
|
248
|
-
else if (err instanceof error_1.ValidationError) {
|
|
249
|
-
await res.status(err.statusCode).send({ success: false, messages: [{ message: err.message, path: err.path, type: err.type ?? 'error' }] });
|
|
250
|
-
}
|
|
251
|
-
else if (err instanceof error_1.ValidationErrors) {
|
|
252
|
-
await res.status(err.statusCode).send({ success: false, messages: err.errors });
|
|
253
|
-
}
|
|
254
|
-
else if (err instanceof error_1.HttpError) {
|
|
255
|
-
await res.status(err.statusCode).send(err.message);
|
|
256
|
-
}
|
|
257
|
-
else if (err.code === 'FST_ERR_VALIDATION') {
|
|
258
|
-
const developerErrors = [];
|
|
259
|
-
const userErrors = [];
|
|
260
|
-
for (const v of err.validation ?? []) {
|
|
261
|
-
if (v.keyword === 'errorMessage') {
|
|
262
|
-
for (const ov of v.params.errors) {
|
|
263
|
-
if (['type', 'additionalProperties', 'minProperties'].includes(ov.keyword))
|
|
264
|
-
developerErrors.push({ ...ov, message: v.message });
|
|
265
|
-
else if (ov.keyword === 'required')
|
|
266
|
-
userErrors.push({ ...ov, message: 'This field is required.' });
|
|
267
|
-
else
|
|
268
|
-
userErrors.push({ ...ov, message: v.message });
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
if (['type', 'additionalProperties', 'minProperties'].includes(v.keyword))
|
|
273
|
-
developerErrors.push(v);
|
|
274
|
-
else if (v.keyword === 'required')
|
|
275
|
-
userErrors.push({ ...v, message: 'This field is required.' });
|
|
276
|
-
else
|
|
277
|
-
userErrors.push(v);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (userErrors.length)
|
|
281
|
-
await res.status(422).send({ success: false, messages: userErrors.map(error_1.fstValidationToMessage) });
|
|
282
|
-
else
|
|
283
|
-
await res.status(400).send(developerErrors.map(error_1.fstValidationToMessage));
|
|
284
|
-
}
|
|
285
|
-
else if (err.statusCode) {
|
|
286
|
-
await res.status(err.statusCode).send(new error_1.HttpError(err.statusCode).message);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
await res.status(500).send('Internal Server Error.');
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
this.app.get('/health', { logLevel: 'warn' }, async (req, res) => {
|
|
294
|
-
if (this.shuttingDown) {
|
|
295
|
-
res.log.warn('Returning 503 on /health because we are shutting down/restarting.');
|
|
296
|
-
void res.status(503);
|
|
297
|
-
return 'MAINTENANCE';
|
|
298
|
-
}
|
|
299
|
-
else if (this.healthMessage) {
|
|
300
|
-
res.log.error(this.healthMessage);
|
|
301
|
-
void res.status(500);
|
|
302
|
-
return this.healthMessage;
|
|
303
|
-
}
|
|
304
|
-
else if (this.healthCallback) {
|
|
305
|
-
const resp = await this.healthCallback();
|
|
306
|
-
const [status, msg] = typeof resp === 'string' ? [500, resp] : [resp?.status, resp?.message];
|
|
307
|
-
if (!!msg || !!status) {
|
|
308
|
-
res.log.error(resp, 'Health check callback failed.');
|
|
309
|
-
void res.status(status ?? 500);
|
|
310
|
-
return msg ?? 'FAIL';
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return 'OK';
|
|
314
|
-
});
|
|
315
|
-
this.sigHandler = () => {
|
|
316
|
-
this.close().then(() => {
|
|
317
|
-
process.exit();
|
|
318
|
-
}).catch(e => { console.error(e); });
|
|
319
|
-
};
|
|
320
|
-
process.on('SIGTERM', this.sigHandler);
|
|
321
|
-
process.on('SIGINT', this.sigHandler);
|
|
322
|
-
}
|
|
323
|
-
async start(port) {
|
|
324
|
-
const customPort = port ?? parseInt(process.env.PORT ?? '0');
|
|
325
|
-
await this.app.ready();
|
|
326
|
-
this.app.swagger?.();
|
|
327
|
-
if (customPort) {
|
|
328
|
-
await this.app.listen({ port: customPort, host: '0.0.0.0' });
|
|
329
|
-
}
|
|
330
|
-
else if (this.https) {
|
|
331
|
-
// redirect 80 to 443
|
|
332
|
-
node_http_1.default.createServer((req, res) => {
|
|
333
|
-
res.writeHead(301, { Location: 'https://' + (req?.headers?.host?.replace(/:\d+$/, '') ?? '') + (req.url ?? '') });
|
|
334
|
-
res.end();
|
|
335
|
-
}).listen(80);
|
|
336
|
-
await this.app.listen({ port: 443, host: '0.0.0.0' });
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
await this.app.listen({ port: 80, host: '0.0.0.0' });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
addErrorHandler(handler) {
|
|
343
|
-
this.errorHandlers.push(handler);
|
|
344
|
-
}
|
|
345
|
-
setUnhealthy(message) {
|
|
346
|
-
this.healthMessage = message;
|
|
347
|
-
}
|
|
348
|
-
setHealthy() {
|
|
349
|
-
this.healthMessage = undefined;
|
|
350
|
-
}
|
|
351
|
-
setValidOrigins(origins) {
|
|
352
|
-
this.validOrigins = origins.reduce((validOrigins, origin) => ({ ...validOrigins, [origin]: true }), {});
|
|
353
|
-
}
|
|
354
|
-
setValidOriginHosts(hosts) {
|
|
355
|
-
this.validOriginHosts = hosts.reduce((validHosts, host) => ({ ...validHosts, [host]: true }), {});
|
|
356
|
-
}
|
|
357
|
-
setValidOriginSuffixes(suffixes) {
|
|
358
|
-
this.validOriginSuffixes.clear();
|
|
359
|
-
for (const s of suffixes)
|
|
360
|
-
this.validOriginSuffixes.add(s.replace(/^\./, ''));
|
|
361
|
-
}
|
|
362
|
-
async swagger(opts) {
|
|
363
|
-
let openapi = opts?.openapi ?? {};
|
|
364
|
-
if (this.config.authenticate != null) {
|
|
365
|
-
openapi = (0, txstate_utils_1.set)(openapi, 'components.securitySchemes', {
|
|
366
|
-
unifiedAuth: {
|
|
367
|
-
type: 'http',
|
|
368
|
-
scheme: 'bearer',
|
|
369
|
-
bearerFormat: 'JWT',
|
|
370
|
-
description: `Enter a token obtained from the TxState Unified Authentication service. An easy way to do
|
|
371
|
-
this is log into this application and use dev tools to pull your token from the Authorization header.`
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
// Apply the security globally to all operations
|
|
375
|
-
openapi.security = [{ unifiedAuth: [] }];
|
|
376
|
-
}
|
|
377
|
-
function findRefs(obj, id) {
|
|
378
|
-
if (obj == null)
|
|
379
|
-
return undefined;
|
|
380
|
-
if (obj.$id?.length)
|
|
381
|
-
id = obj.$id;
|
|
382
|
-
if (obj.$ref === '#' && id?.length) {
|
|
383
|
-
obj.type = 'string';
|
|
384
|
-
obj.enum = [id];
|
|
385
|
-
delete obj.$ref;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
for (const val of Object.values(obj)) {
|
|
389
|
-
if (typeof val === 'object' && !(val instanceof Date))
|
|
390
|
-
findRefs(val, id);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return obj;
|
|
394
|
-
}
|
|
395
|
-
await this.app.register(swagger_1.default, {
|
|
396
|
-
openapi,
|
|
397
|
-
transform(transformArgs) {
|
|
398
|
-
const newSchema = findRefs((0, txstate_utils_1.clone)(transformArgs.schema));
|
|
399
|
-
return { ...transformArgs, schema: newSchema };
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
this.swaggerEndpoint = opts?.path ?? opts?.ui?.routePrefix ?? '/docs';
|
|
403
|
-
await this.app.register(swagger_ui_1.default, { ...opts?.ui, routePrefix: this.swaggerEndpoint });
|
|
404
|
-
}
|
|
405
|
-
async close(softSeconds) {
|
|
406
|
-
if (typeof softSeconds === 'undefined')
|
|
407
|
-
softSeconds = parseInt(process.env.LOAD_BALANCE_TIMEOUT ?? '0');
|
|
408
|
-
process.removeListener('SIGTERM', this.sigHandler);
|
|
409
|
-
process.removeListener('SIGINT', this.sigHandler);
|
|
410
|
-
if (softSeconds) {
|
|
411
|
-
this.shuttingDown = true;
|
|
412
|
-
await (0, txstate_utils_1.sleep)(softSeconds);
|
|
413
|
-
}
|
|
414
|
-
await this.app.close();
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
exports.default = Server;
|
|
418
|
-
__exportStar(require("./analytics"), exports);
|
|
419
|
-
__exportStar(require("./error"), exports);
|
|
420
|
-
__exportStar(require("./filestorage"), exports);
|
|
421
|
-
__exportStar(require("./unified-auth"), exports);
|
|
422
|
-
__exportStar(require("./postformdata"), exports);
|
|
1
|
+
export { default } from "./server.js";
|
|
2
|
+
export * from "./server.js";
|
|
3
|
+
export * from "./analytics.js";
|
|
4
|
+
export * from "./error.js";
|
|
5
|
+
export * from "./filestorage.js";
|
|
6
|
+
export * from "./unified-auth.js";
|
|
7
|
+
export * from "./oauth.js";
|
|
8
|
+
export * from "./postformdata.js";
|
package/lib/oauth.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import { type FastifyTxStateAuthInfo, type FastifyInstanceTyped } from './server.ts';
|
|
3
|
+
declare module 'fastify' {
|
|
4
|
+
interface FastifyRequest {
|
|
5
|
+
pendingOAuthCookies?: string[];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Authenticate requests using JWT tokens from any OAuth/OIDC provider. The token's
|
|
10
|
+
* issuer claim is used to auto-discover the provider's JWKS endpoint for signature
|
|
11
|
+
* verification.
|
|
12
|
+
*
|
|
13
|
+
* Expects JWT tokens (access tokens or ID tokens) in the Authorization Bearer header
|
|
14
|
+
* or in a cookie set by registerOAuthCookieRoutes.
|
|
15
|
+
*
|
|
16
|
+
* For providers like Google that issue opaque access tokens, have the client send the
|
|
17
|
+
* ID token instead — it's a standard JWT that proves the user's identity without
|
|
18
|
+
* requiring a round-trip to the provider on every request.
|
|
19
|
+
*/
|
|
20
|
+
export declare function oauthAuthenticate(req: FastifyRequest, options?: {
|
|
21
|
+
/** If true, all requests require authentication, except routes listed in exceptRoutes or optionalRoutes. */
|
|
22
|
+
authenticateAll?: boolean;
|
|
23
|
+
/** Routes that skip authentication entirely. They will not receive an auth object. */
|
|
24
|
+
exceptRoutes?: Set<string>;
|
|
25
|
+
/** Routes that do not require authentication, but will fill req.auth if a session is available. */
|
|
26
|
+
optionalRoutes?: Set<string>;
|
|
27
|
+
/** Set this true if you are using registerOAuthCookieRoutes and authenticateAll. */
|
|
28
|
+
usingOAuthCookieRoutes?: boolean;
|
|
29
|
+
/** Receives the full JWT payload and returns extra properties to merge into the auth object.
|
|
30
|
+
* If you use this, you should also set OAUTH_TRUSTED_AUDIENCES to prevent tokens from
|
|
31
|
+
* other applications carrying unexpected authorization claims. */
|
|
32
|
+
extraClaims?: (payload: Record<string, unknown>) => Record<string, unknown>;
|
|
33
|
+
}): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
34
|
+
/**
|
|
35
|
+
* Register cookie-based OAuth login/logout endpoints. Uses the authorization code flow
|
|
36
|
+
* with PKCE (S256) to exchange a code for tokens, then stores the ID token in an HttpOnly
|
|
37
|
+
* cookie. The access token and refresh token are stored in separate cookies (optionally
|
|
38
|
+
* encrypted via OAUTH_COOKIE_SECRET) so that the ID token can be transparently refreshed
|
|
39
|
+
* when it expires and the access token is available at `req.auth.accessToken` for calling
|
|
40
|
+
* provider APIs.
|
|
41
|
+
*
|
|
42
|
+
* Requires OAUTH_CLIENT_ID environment variable. OAUTH_CLIENT_SECRET is optional — PKCE
|
|
43
|
+
* provides the security for the code exchange, but some providers require a client secret
|
|
44
|
+
* even with PKCE. OAUTH_COOKIE_SECRET is optional — if set, the access token and refresh
|
|
45
|
+
* token cookies are encrypted with AES-256-GCM; if not, they are stored as plaintext
|
|
46
|
+
* (still HttpOnly and Secure).
|
|
47
|
+
*
|
|
48
|
+
* Registers three routes:
|
|
49
|
+
* - `/.oauthRedirect` - Redirects to the OAuth provider's login page. The client passes
|
|
50
|
+
* `requestedUrl` (required) which is sent to the provider as the `state` parameter,
|
|
51
|
+
* round-tripped back, and used as the redirect destination after login.
|
|
52
|
+
* - `/.oauthCallback` - Handles the provider's redirect, exchanges the code for tokens
|
|
53
|
+
* using the PKCE code verifier. Sets the ID token (or JWT access token as fallback),
|
|
54
|
+
* access token, and refresh token as cookies.
|
|
55
|
+
* - `/.oauthLogout` - Clears all OAuth cookies and redirects to the provider's logout
|
|
56
|
+
* endpoint if available.
|
|
57
|
+
*/
|
|
58
|
+
export interface IssuerChoice {
|
|
59
|
+
issuerUrl: string;
|
|
60
|
+
redirectHref: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function registerOAuthCookieRoutes(app: FastifyInstanceTyped, options?: {
|
|
63
|
+
/** Scopes to always include in the authorization request, merged with any scopes
|
|
64
|
+
* the client passes via the `scope` query parameter. */
|
|
65
|
+
scopes?: string[];
|
|
66
|
+
/** When multiple issuers are configured and the client doesn't specify one,
|
|
67
|
+
* this function is called to render a login selection page. It receives an array
|
|
68
|
+
* of issuer URLs with their corresponding redirect hrefs and should return an HTML
|
|
69
|
+
* string. If not provided, the first trusted issuer is used. */
|
|
70
|
+
loginPage?: (issuers: IssuerChoice[]) => string;
|
|
71
|
+
}): void;
|