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/index.js
CHANGED
|
@@ -1,422 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 "./jwt-auth.js";
|
|
7
|
+
export * from "./unified-auth.js";
|
|
8
|
+
export * from "./oauth.js";
|
|
9
|
+
export * from "./postformdata.js";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import { type JWTPayload } from 'jose';
|
|
3
|
+
import type { FastifyTxStateAuthInfo, IssuerConfig } from './server.ts';
|
|
4
|
+
export interface OAuthDiscovery {
|
|
5
|
+
issuer: string;
|
|
6
|
+
jwks_uri: string;
|
|
7
|
+
authorization_endpoint?: string;
|
|
8
|
+
token_endpoint?: string;
|
|
9
|
+
end_session_endpoint?: string;
|
|
10
|
+
}
|
|
11
|
+
declare module 'fastify' {
|
|
12
|
+
interface FastifyRequest {
|
|
13
|
+
pendingOAuthCookies?: string[];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export declare const oauthCookieName: string;
|
|
17
|
+
export declare const refreshCookieName: string;
|
|
18
|
+
export declare const accessTokenCookieName: string;
|
|
19
|
+
export declare const uaCookieName: string;
|
|
20
|
+
export declare function wrapRefreshToken(token: string): string;
|
|
21
|
+
export declare function unwrapRefreshToken(value: string): string | undefined;
|
|
22
|
+
type JwtIssuerType = 'oauth' | 'jwks' | 'unified-auth' | 'publicKey' | 'secret';
|
|
23
|
+
export interface JwtIssuerConfigRaw {
|
|
24
|
+
iss: string;
|
|
25
|
+
/** Explicitly set the issuer type. Optional — when omitted, the type is inferred:
|
|
26
|
+
* iss === 'unified-auth' → 'unified-auth'; secret → 'secret'; publicKey → 'publicKey';
|
|
27
|
+
* url → 'jwks'. Set type: 'oauth' to enable OAuth/OIDC auto-discovery via
|
|
28
|
+
* .well-known/openid-configuration on the issuer URL. */
|
|
29
|
+
type?: JwtIssuerType;
|
|
30
|
+
/** Issuer URL. For 'oauth' this is the issuer (.well-known/openid-configuration is
|
|
31
|
+
* fetched relative to it). For 'jwks' this is the JWKS endpoint directly. For
|
|
32
|
+
* 'unified-auth' this is the UA service URL. */
|
|
33
|
+
url?: string;
|
|
34
|
+
/** PEM-encoded public key (publicKey type). */
|
|
35
|
+
publicKey?: string;
|
|
36
|
+
/** Symmetric HMAC secret (secret type). */
|
|
37
|
+
secret?: string;
|
|
38
|
+
/** Override URL for the unified-auth /validateToken poll. Resolved relative to `url`. */
|
|
39
|
+
validateUrl?: string;
|
|
40
|
+
/** End-session URL surfaced as `req.auth.issuerConfig.logoutUrl`. For 'oauth' issuers
|
|
41
|
+
* this is auto-discovered; set this only to override. Resolved relative to `url`. */
|
|
42
|
+
logoutUrl?: string;
|
|
43
|
+
/** Server-to-server URL prefix override for split-horizon DNS (e.g. talking to the
|
|
44
|
+
* issuer over a docker-internal hostname while the browser uses the public URL). */
|
|
45
|
+
internalUrl?: string;
|
|
46
|
+
/** If set, only accept tokens whose `aud` claim contains one of these values. */
|
|
47
|
+
audiences?: string[];
|
|
48
|
+
/** If set, only accept tokens whose `client_id` claim matches one of these values. */
|
|
49
|
+
clientIds?: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function toInternalUrl(url: string): string;
|
|
52
|
+
export declare function getOAuthIssuerUrls(): string[];
|
|
53
|
+
export declare function getIssuerConfig(iss: string): IssuerConfig | undefined;
|
|
54
|
+
export declare function getOAuthDiscovery(issuerUrl: string): Promise<OAuthDiscovery | undefined>;
|
|
55
|
+
export declare function init(): void;
|
|
56
|
+
export interface JwtAuthenticateOptions {
|
|
57
|
+
/** If true, all requests require authentication, except routes listed in exceptRoutes or optionalRoutes. */
|
|
58
|
+
authenticateAll?: boolean;
|
|
59
|
+
/** Routes that skip authentication entirely. They will not receive an auth object. */
|
|
60
|
+
exceptRoutes?: Set<string>;
|
|
61
|
+
/** Routes that do not require authentication, but will fill req.auth if a session is available. */
|
|
62
|
+
optionalRoutes?: Set<string>;
|
|
63
|
+
/** Receives the full JWT payload and returns extra properties to merge into the auth object.
|
|
64
|
+
* If you use this, set per-issuer `audiences` to prevent tokens from other applications
|
|
65
|
+
* carrying unexpected authorization claims. */
|
|
66
|
+
extraClaims?: (payload: JWTPayload) => Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
export declare const registeredExceptRoutes: Set<string>;
|
|
69
|
+
export declare const registeredOptionalRoutes: Set<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Build an `authenticate` function that validates JWTs from the Authorization Bearer
|
|
72
|
+
* header or a session cookie. Supports any mix of issuer types via the
|
|
73
|
+
* JWT_TRUSTED_ISSUERS env var:
|
|
74
|
+
*
|
|
75
|
+
* - 'oauth' — OAuth/OIDC provider with .well-known auto-discovery
|
|
76
|
+
* - 'jwks' — JWKS endpoint URL (no discovery)
|
|
77
|
+
* - 'unified-auth' — TxState Unified Auth (JWKS + /validateToken poll for deauth)
|
|
78
|
+
* - 'publicKey' — PEM-encoded asymmetric public key
|
|
79
|
+
* - 'secret' — symmetric HMAC secret
|
|
80
|
+
*
|
|
81
|
+
* Usage:
|
|
82
|
+
* new Server({ authenticate: jwtAuthenticate({ authenticateAll: true }) })
|
|
83
|
+
*
|
|
84
|
+
* Or with no options:
|
|
85
|
+
* new Server({ authenticate: jwtAuthenticate() })
|
|
86
|
+
*
|
|
87
|
+
* Calling `registerOAuthCookieRoutes` or `registerUaCookieRoutes` automatically excludes
|
|
88
|
+
* their callback/redirect routes from authentication requirements and marks their logout
|
|
89
|
+
* routes as optional, so you do not need to configure that here.
|
|
90
|
+
*
|
|
91
|
+
* If a refresh-token cookie is present (set by registerOAuthCookieRoutes) and the access
|
|
92
|
+
* token has expired, the returned authenticator transparently exchanges the refresh
|
|
93
|
+
* token for a new access token and queues the replacement cookies on
|
|
94
|
+
* `req.pendingOAuthCookies`. The onSend hook installed by registerOAuthCookieRoutes
|
|
95
|
+
* flushes those cookies onto the response.
|
|
96
|
+
*/
|
|
97
|
+
export declare function jwtAuthenticate(options?: JwtAuthenticateOptions): (req: FastifyRequest) => Promise<FastifyTxStateAuthInfo | undefined>;
|
|
98
|
+
export {};
|