fastify-txstate 3.2.10 → 3.2.12

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 CHANGED
@@ -17,6 +17,7 @@ class AnalyticsClient {
17
17
  }
18
18
  exports.AnalyticsClient = AnalyticsClient;
19
19
  class LoggingAnalyticsClient extends AnalyticsClient {
20
+ logger;
20
21
  constructor(logger) {
21
22
  super();
22
23
  this.logger = logger;
@@ -28,30 +29,29 @@ class LoggingAnalyticsClient extends AnalyticsClient {
28
29
  }
29
30
  exports.LoggingAnalyticsClient = LoggingAnalyticsClient;
30
31
  class ElasticAnalyticsClient extends AnalyticsClient {
32
+ elasticClient;
31
33
  constructor() {
32
- var _a, _b;
33
34
  super();
34
35
  this.elasticClient = new elasticsearch_1.default.Client({
35
36
  node: process.env.ELASTICSEARCH_URL,
36
37
  auth: {
37
- username: (_a = process.env.ELASTICSEARCH_USER) !== null && _a !== void 0 ? _a : 'elastic',
38
- password: (_b = process.env.ELASTICSEARCH_PASS) !== null && _b !== void 0 ? _b : 'not_provided'
38
+ username: process.env.ELASTICSEARCH_USER ?? 'elastic',
39
+ password: process.env.ELASTICSEARCH_PASS ?? 'not_provided'
39
40
  }
40
41
  });
41
42
  }
42
43
  async push(events) {
43
44
  if (events.length)
44
- await this.elasticClient.bulk({ body: events.reduce((acc, event) => { var _a; acc.push({ index: { _index: (_a = process.env.ELASTICSEARCH_USEREVENTS_INDEX) !== null && _a !== void 0 ? _a : 'interaction-analytics' } }, event); return acc; }, []) });
45
+ await this.elasticClient.bulk({ body: events.reduce((acc, event) => { acc.push({ index: { _index: process.env.ELASTICSEARCH_USEREVENTS_INDEX ?? 'interaction-analytics' } }, event); return acc; }, []) });
45
46
  }
46
47
  }
47
48
  exports.ElasticAnalyticsClient = ElasticAnalyticsClient;
48
49
  function analyticsPlugin(fastify, opts, done) {
49
- var _a;
50
50
  const environment = process.env.NODE_ENV;
51
51
  if ((0, txstate_utils_1.isBlank)(environment))
52
52
  throw new Error('Must set NODE_ENV when reporting analytics.');
53
53
  const eventQueue = [];
54
- const analyticsClient = (_a = opts.analyticsClient) !== null && _a !== void 0 ? _a : ((0, txstate_utils_1.isBlank)(process.env.ELASTICSEARCH_URL)
54
+ const analyticsClient = opts.analyticsClient ?? ((0, txstate_utils_1.isBlank)(process.env.ELASTICSEARCH_URL)
55
55
  ? environment === 'development'
56
56
  ? new AnalyticsClient()
57
57
  : new LoggingAnalyticsClient(fastify.log)
@@ -93,14 +93,13 @@ function analyticsPlugin(fastify, opts, done) {
93
93
  }
94
94
  setTimeout(() => { void flushQueue(); }, 5000);
95
95
  function queueEvents(auth, headers, remoteIp, events) {
96
- var _a, _b, _c;
97
96
  for (const event of events) {
98
97
  eventQueue.push({
99
98
  event,
100
99
  remoteIp,
101
- ua: (_a = headers['user-agent']) !== null && _a !== void 0 ? _a : '',
100
+ ua: headers['user-agent'] ?? '',
102
101
  time: new Date().toISOString(),
103
- gaCookie: (_c = (_b = headers.cookie) === null || _b === void 0 ? void 0 : _b.replace(/^.*?(?:_ga=([^;]+))?.*$/, '$1')) !== null && _c !== void 0 ? _c : '',
102
+ gaCookie: headers.cookie?.replace(/^.*?(?:_ga=([^;]+))?.*$/, '$1') ?? '',
104
103
  auth
105
104
  });
106
105
  }
@@ -109,7 +108,7 @@ function analyticsPlugin(fastify, opts, done) {
109
108
  const { auth } = req;
110
109
  if (opts.authorize && !opts.authorize(req))
111
110
  throw new _1.HttpError(401);
112
- queueEvents(auth !== null && auth !== void 0 ? auth : { username: 'unauthenticated' }, req.headers, req.ip, req.body);
111
+ queueEvents(auth ?? { username: 'unauthenticated' }, req.headers, req.ip, req.body);
113
112
  res.statusCode = 202;
114
113
  return 'OK';
115
114
  });
package/lib/error.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fstValidationToMessage = exports.FailedValidationError = exports.HttpError = void 0;
4
4
  const http_status_codes_1 = require("http-status-codes");
5
5
  class HttpError extends Error {
6
+ statusCode;
6
7
  constructor(statusCode, message) {
7
8
  if (!message) {
8
9
  if (statusCode === 401)
@@ -18,6 +19,7 @@ class HttpError extends Error {
18
19
  }
19
20
  exports.HttpError = HttpError;
20
21
  class FailedValidationError extends HttpError {
22
+ errors;
21
23
  constructor(errors) {
22
24
  super(422, 'Validation failure.');
23
25
  this.errors = errors;
package/lib/index.js CHANGED
@@ -28,7 +28,6 @@ const node_fs_1 = __importDefault(require("node:fs"));
28
28
  const node_http_1 = __importDefault(require("node:http"));
29
29
  const txstate_utils_1 = require("txstate-utils");
30
30
  const error_1 = require("./error");
31
- const unified_auth_1 = require("./unified-auth");
32
31
  exports.devLogger = {
33
32
  level: 'info',
34
33
  info: (msg) => { console.info(msg.req ? `${msg.req.method} ${msg.req.url}` : msg.res ? `${msg.res.statusCode} - ${msg.responseTime}` : msg); },
@@ -52,27 +51,30 @@ exports.prodLogger = {
52
51
  };
53
52
  },
54
53
  res(res) {
55
- var _a, _b, _c, _d;
56
54
  return {
57
55
  statusCode: res.statusCode,
58
- url: (_a = res.request) === null || _a === void 0 ? void 0 : _a.url.replace(/(token|unifiedJwt)=[\w.]+/i, '$1=redacted'),
59
- length: Number((0, txstate_utils_1.toArray)((_b = res.getHeader) === null || _b === void 0 ? void 0 : _b.call(res, 'content-length'))[0]),
56
+ url: res.request?.url.replace(/(token|unifiedJwt)=[\w.]+/i, '$1=redacted'),
57
+ length: Number((0, txstate_utils_1.toArray)(res.getHeader?.('content-length'))[0]),
60
58
  ...res.extraLogInfo,
61
- auth: (_d = (_c = res.request) === null || _c === void 0 ? void 0 : _c.auth) !== null && _d !== void 0 ? _d : res.extraLogInfo.auth
59
+ auth: res.request?.auth ?? res.extraLogInfo.auth
62
60
  };
63
61
  }
64
62
  }
65
63
  };
66
64
  class Server {
65
+ config;
66
+ https = false;
67
+ errorHandlers = [];
68
+ healthMessage;
69
+ healthCallback;
70
+ shuttingDown = false;
71
+ sigHandler;
72
+ validOrigins = {};
73
+ validOriginHosts = {};
74
+ validOriginSuffixes = new Set();
75
+ app;
67
76
  constructor(config = {}) {
68
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
69
77
  this.config = config;
70
- this.https = false;
71
- this.errorHandlers = [];
72
- this.shuttingDown = false;
73
- this.validOrigins = {};
74
- this.validOriginHosts = {};
75
- this.validOriginSuffixes = new Set();
76
78
  try {
77
79
  const key = node_fs_1.default.readFileSync('/securekeys/private.key');
78
80
  const cert = node_fs_1.default.readFileSync('/securekeys/cert.pem');
@@ -101,16 +103,15 @@ class Server {
101
103
  else
102
104
  config.trustProxy = process.env.TRUST_PROXY;
103
105
  }
104
- config.ajv = { ...config.ajv, plugins: [...((_b = (_a = config.ajv) === null || _a === void 0 ? void 0 : _a.plugins) !== null && _b !== void 0 ? _b : []), ajv_errors_1.default, [ajv_formats_1.default, { mode: 'fast' }]], customOptions: { ...(_c = config.ajv) === null || _c === void 0 ? void 0 : _c.customOptions, allErrors: true, strictSchema: false } };
106
+ 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 } };
105
107
  this.healthCallback = config.checkHealth;
106
108
  this.app = (0, fastify_1.fastify)(config);
107
109
  this.app.addHook('onRoute', route => {
108
- var _a, _b, _c;
109
- if (!((_a = route.schema) === null || _a === void 0 ? void 0 : _a.body))
110
+ if (!route.schema?.body)
110
111
  return;
111
- const missingResponse = ((_b = route.schema) === null || _b === void 0 ? void 0 : _b.response) == null;
112
+ const missingResponse = route.schema?.response == null;
112
113
  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.');
113
- let newSchema = (0, txstate_utils_1.set)((_c = route.schema) !== null && _c !== void 0 ? _c : {}, 'response.400', response400);
114
+ let newSchema = (0, txstate_utils_1.set)(route.schema ?? {}, 'response.400', response400);
114
115
  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.');
115
116
  newSchema = (0, txstate_utils_1.set)(newSchema, 'response.422', response422);
116
117
  if (missingResponse) {
@@ -130,15 +131,13 @@ class Server {
130
131
  * convert all nulls to undefined before fastify validates.
131
132
  */
132
133
  this.app.addHook('preSerialization', async (req, res, payload) => {
133
- var _a;
134
- return ((_a = req.routeSchema) === null || _a === void 0 ? void 0 : _a.response) ? (0, txstate_utils_1.destroyNulls)(payload) : payload;
134
+ return req.routeSchema?.response ? (0, txstate_utils_1.destroyNulls)(payload) : payload;
135
135
  });
136
136
  if (!config.skipOriginCheck && !process.env.SKIP_ORIGIN_CHECK) {
137
- this.setValidOrigins([...((_d = config.validOrigins) !== null && _d !== void 0 ? _d : []), ...((_f = (_e = process.env.VALID_ORIGINS) === null || _e === void 0 ? void 0 : _e.split(',')) !== null && _f !== void 0 ? _f : [])]);
138
- this.setValidOriginHosts([...((_g = config.validOriginHosts) !== null && _g !== void 0 ? _g : []), ...((_j = (_h = process.env.VALID_ORIGIN_HOSTS) === null || _h === void 0 ? void 0 : _h.split(',')) !== null && _j !== void 0 ? _j : [])]);
139
- this.setValidOriginSuffixes([...((_k = config.validOriginSuffixes) !== null && _k !== void 0 ? _k : []), ...((_m = (_l = process.env.VALID_ORIGIN_SUFFIXES) === null || _l === void 0 ? void 0 : _l.split(',')) !== null && _m !== void 0 ? _m : [])]);
137
+ this.setValidOrigins([...(config.validOrigins ?? []), ...(process.env.VALID_ORIGINS?.split(',') ?? [])]);
138
+ this.setValidOriginHosts([...(config.validOriginHosts ?? []), ...(process.env.VALID_ORIGIN_HOSTS?.split(',') ?? [])]);
139
+ this.setValidOriginSuffixes([...(config.validOriginSuffixes ?? []), ...(process.env.VALID_ORIGIN_SUFFIXES?.split(',') ?? [])]);
140
140
  this.app.addHook('preHandler', async (req, res) => {
141
- var _a;
142
141
  res.extraLogInfo = {};
143
142
  if (!req.headers.origin)
144
143
  return;
@@ -160,7 +159,7 @@ class Server {
160
159
  }
161
160
  }
162
161
  }
163
- if (!passed && ((_a = config.checkOrigin) === null || _a === void 0 ? void 0 : _a.call(config, req)))
162
+ if (!passed && config.checkOrigin?.(req))
164
163
  passed = true;
165
164
  if (!passed) {
166
165
  await res.status(403).send('Origin check failed. Suspected XSRF attack.');
@@ -169,6 +168,8 @@ class Server {
169
168
  else {
170
169
  void res.header('Access-Control-Allow-Origin', req.headers.origin);
171
170
  void res.header('Access-Control-Max-Age', '600'); // ask browser to skip pre-flights for 10 minutes after a yes
171
+ if (req.headers['access-control-request-method'])
172
+ void res.header('access-control-allow-methods', req.headers['access-control-request-method']);
172
173
  if (req.headers['access-control-request-headers'])
173
174
  void res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
174
175
  }
@@ -211,7 +212,6 @@ class Server {
211
212
  });
212
213
  this.app.setNotFoundHandler((req, res) => { void res.status(404).send('Not Found.'); });
213
214
  this.app.setErrorHandler(async (err, req, res) => {
214
- var _a;
215
215
  req.log.warn(err);
216
216
  for (const errorHandler of this.errorHandlers) {
217
217
  if (!res.sent)
@@ -227,7 +227,7 @@ class Server {
227
227
  else if (err.code === 'FST_ERR_VALIDATION') {
228
228
  const developerErrors = [];
229
229
  const userErrors = [];
230
- for (const v of (_a = err.validation) !== null && _a !== void 0 ? _a : []) {
230
+ for (const v of err.validation ?? []) {
231
231
  if (v.keyword === 'errorMessage') {
232
232
  for (const ov of v.params.errors) {
233
233
  if (['type', 'additionalProperties'].includes(ov.keyword))
@@ -269,11 +269,11 @@ class Server {
269
269
  }
270
270
  else if (this.healthCallback) {
271
271
  const resp = await this.healthCallback();
272
- const [status, msg] = typeof resp === 'string' ? [500, resp] : [resp === null || resp === void 0 ? void 0 : resp.status, resp === null || resp === void 0 ? void 0 : resp.message];
272
+ const [status, msg] = typeof resp === 'string' ? [500, resp] : [resp?.status, resp?.message];
273
273
  if (!!msg || !!status) {
274
274
  res.log.info(resp, 'Health check callback failed.');
275
- void res.status(status !== null && status !== void 0 ? status : 500);
276
- return msg !== null && msg !== void 0 ? msg : 'FAIL';
275
+ void res.status(status ?? 500);
276
+ return msg ?? 'FAIL';
277
277
  }
278
278
  }
279
279
  return 'OK';
@@ -287,18 +287,16 @@ class Server {
287
287
  process.on('SIGINT', this.sigHandler);
288
288
  }
289
289
  async start(port) {
290
- var _a, _b, _c;
291
- const customPort = port !== null && port !== void 0 ? port : parseInt((_a = process.env.PORT) !== null && _a !== void 0 ? _a : '0');
290
+ const customPort = port ?? parseInt(process.env.PORT ?? '0');
292
291
  await this.app.ready();
293
- (_c = (_b = this.app).swagger) === null || _c === void 0 ? void 0 : _c.call(_b);
292
+ this.app.swagger?.();
294
293
  if (customPort) {
295
294
  await this.app.listen({ port: customPort, host: '0.0.0.0' });
296
295
  }
297
296
  else if (this.https) {
298
297
  // redirect 80 to 443
299
298
  node_http_1.default.createServer((req, res) => {
300
- var _a, _b, _c, _d;
301
- res.writeHead(301, { Location: 'https://' + ((_c = (_b = (_a = req === null || req === void 0 ? void 0 : req.headers) === null || _a === void 0 ? void 0 : _a.host) === null || _b === void 0 ? void 0 : _b.replace(/:\d+$/, '')) !== null && _c !== void 0 ? _c : '') + ((_d = req.url) !== null && _d !== void 0 ? _d : '') });
299
+ res.writeHead(301, { Location: 'https://' + (req?.headers?.host?.replace(/:\d+$/, '') ?? '') + (req.url ?? '') });
302
300
  res.end();
303
301
  }).listen(80);
304
302
  await this.app.listen({ port: 443, host: '0.0.0.0' });
@@ -328,9 +326,8 @@ class Server {
328
326
  this.validOriginSuffixes.add(s);
329
327
  }
330
328
  async swagger(opts) {
331
- var _a, _b, _c, _d;
332
- let openapi = (_a = opts === null || opts === void 0 ? void 0 : opts.openapi) !== null && _a !== void 0 ? _a : {};
333
- if (this.config.authenticate === unified_auth_1.unifiedAuthenticate) {
329
+ let openapi = opts?.openapi ?? {};
330
+ if (this.config.authenticate != null) {
334
331
  openapi = (0, txstate_utils_1.set)(openapi, 'components.securitySchemes', {
335
332
  unifiedAuth: {
336
333
  type: 'http',
@@ -344,12 +341,11 @@ this is log into this application and use dev tools to pull your token from the
344
341
  openapi.security = [{ unifiedAuth: [] }];
345
342
  }
346
343
  await this.app.register(swagger_1.default, { openapi });
347
- await this.app.register(swagger_ui_1.default, { ...opts === null || opts === void 0 ? void 0 : opts.ui, routePrefix: (_d = (_b = opts === null || opts === void 0 ? void 0 : opts.path) !== null && _b !== void 0 ? _b : (_c = opts === null || opts === void 0 ? void 0 : opts.ui) === null || _c === void 0 ? void 0 : _c.routePrefix) !== null && _d !== void 0 ? _d : '/docs' });
344
+ await this.app.register(swagger_ui_1.default, { ...opts?.ui, routePrefix: opts?.path ?? opts?.ui?.routePrefix ?? '/docs' });
348
345
  }
349
346
  async close(softSeconds) {
350
- var _a;
351
347
  if (typeof softSeconds === 'undefined')
352
- softSeconds = parseInt((_a = process.env.LOAD_BALANCE_TIMEOUT) !== null && _a !== void 0 ? _a : '0');
348
+ softSeconds = parseInt(process.env.LOAD_BALANCE_TIMEOUT ?? '0');
353
349
  process.removeListener('SIGTERM', this.sigHandler);
354
350
  process.removeListener('SIGINT', this.sigHandler);
355
351
  if (softSeconds) {
@@ -1,14 +1,14 @@
1
1
  "use strict";
2
- var _a;
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.unifiedAuthenticateAll = exports.unifiedAuthenticate = void 0;
5
4
  const crypto_1 = require("crypto");
6
5
  const jose_1 = require("jose");
7
6
  const txstate_utils_1 = require("txstate-utils");
8
7
  function cleanPem(secretOrPem) {
9
- return secretOrPem === null || secretOrPem === void 0 ? void 0 : secretOrPem.replace(/(-+BEGIN [\w\s]+ KEY-+)\s*(.*?)\s*(-+END [\w\s]+ KEY-+)/, '$1\n$2\n$3');
8
+ return secretOrPem?.replace(/(-+BEGIN [\w\s]+ KEY-+)\s*(.*?)\s*(-+END [\w\s]+ KEY-+)/, '$1\n$2\n$3');
10
9
  }
11
10
  class MockContext {
11
+ auth;
12
12
  constructor(auth) {
13
13
  this.auth = auth;
14
14
  }
@@ -18,6 +18,36 @@ class MockContext {
18
18
  static init() { }
19
19
  }
20
20
  class Context extends MockContext {
21
+ authPromise;
22
+ static jwtVerifyKey;
23
+ static issuerKeys = new Map();
24
+ static issuerConfig = new Map();
25
+ static tokenCache = new txstate_utils_1.Cache(async (token, { req, ctx }) => {
26
+ // `this` is always the Context class, even if we are making instances of a subclass of Context
27
+ // we need to get the instance's constructor instead in case it has overridden one of our
28
+ // static methods/variables
29
+ const ctxStatic = ctx.constructor;
30
+ const logger = req?.log ?? console;
31
+ let verifyKey = Context.jwtVerifyKey;
32
+ try {
33
+ const claims = (0, jose_1.decodeJwt)(token);
34
+ if (claims.iss && ctxStatic.issuerKeys.has(claims.iss))
35
+ verifyKey = ctxStatic.issuerKeys.get(claims.iss);
36
+ if (!verifyKey) {
37
+ logger.info(`Received token with issuer: ${claims.iss} but JWT secret could not be found. The server may be misconfigured or the user may have presented a JWT from an untrusted issuer.`);
38
+ return undefined;
39
+ }
40
+ await ctxStatic.validateToken?.(token, ctxStatic.issuerConfig.get(claims.iss), claims);
41
+ const { payload } = await (0, jose_1.jwtVerify)(token, verifyKey);
42
+ return await ctx.authFromPayload(payload);
43
+ }
44
+ catch (e) {
45
+ // squelch errors about bad tokens, we can already see the 401 in the log
46
+ if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED')
47
+ logger.error(e);
48
+ return undefined;
49
+ }
50
+ }, { freshseconds: 10 });
21
51
  constructor(req) {
22
52
  super(undefined);
23
53
  this.authPromise = this.authFromReq(req);
@@ -26,31 +56,31 @@ class Context extends MockContext {
26
56
  this.auth = await this.authPromise;
27
57
  return this.auth;
28
58
  }
59
+ static hasInitialized = false;
29
60
  static init() {
30
- var _b;
31
61
  if (this.hasInitialized)
32
62
  return;
33
63
  this.hasInitialized = true;
34
64
  let secret = cleanPem(process.env.JWT_SECRET_VERIFY);
35
65
  if (secret != null) {
36
- _a.jwtVerifyKey = (0, crypto_1.createPublicKey)(secret);
66
+ Context.jwtVerifyKey = (0, crypto_1.createPublicKey)(secret);
37
67
  }
38
68
  else {
39
69
  secret = cleanPem(process.env.JWT_SECRET);
40
70
  if (secret != null) {
41
71
  try {
42
- _a.jwtVerifyKey = (0, crypto_1.createPublicKey)(secret);
72
+ Context.jwtVerifyKey = (0, crypto_1.createPublicKey)(secret);
43
73
  }
44
74
  catch (e) {
45
75
  console.info('JWT_SECRET was not a private key, treating it as symmetric.');
46
- _a.jwtVerifyKey = (0, crypto_1.createSecretKey)(Buffer.from(secret, 'ascii'));
76
+ Context.jwtVerifyKey = (0, crypto_1.createSecretKey)(Buffer.from(secret, 'ascii'));
47
77
  }
48
78
  }
49
79
  }
50
80
  if (process.env.JWT_TRUSTED_ISSUERS) {
51
81
  const issuers = (0, txstate_utils_1.toArray)(JSON.parse(process.env.JWT_TRUSTED_ISSUERS));
52
82
  for (const issuer of issuers) {
53
- this.issuerConfig.set(issuer.iss, (_b = this.processIssuerConfig) === null || _b === void 0 ? void 0 : _b.call(this, (0, txstate_utils_1.omit)(issuer, 'publicKey', 'secret')));
83
+ this.issuerConfig.set(issuer.iss, this.processIssuerConfig?.((0, txstate_utils_1.omit)(issuer, 'publicKey', 'secret')));
54
84
  if (issuer.url)
55
85
  this.issuerKeys.set(issuer.iss, (0, jose_1.createRemoteJWKSet)(new URL(issuer.url)));
56
86
  else if (issuer.publicKey)
@@ -60,10 +90,27 @@ class Context extends MockContext {
60
90
  }
61
91
  }
62
92
  }
93
+ /**
94
+ * If implemented, this method will be called on startup, once per configured issuer. It receives
95
+ * the issuer configuration from the JWT_TRUSTED_ISSUERS environment variable and allows you to manipulate
96
+ * the configuration before storing it.
97
+ *
98
+ * Once stored, whatever you create may be used in your custom validateToken method. For example,
99
+ * you might want to create an in-memory URL object with an issuer's URL so that it can be manipulated
100
+ * easily to send validation checks to the issuer.
101
+ */
102
+ static processIssuerConfig;
103
+ /**
104
+ * If implemented, this method is called after a token's signature is checked and passes. You would
105
+ * typically implement this method to check whether the user has manually signed out, or the token has
106
+ * been otherwise deauthorized before its expiration date.
107
+ *
108
+ * If the token is not valid, this method should throw an error with an appropriate message.
109
+ */
110
+ static validateToken;
63
111
  tokenFromReq(req) {
64
- var _b;
65
- const m = (_b = req === null || req === void 0 ? void 0 : req.headers.authorization) === null || _b === void 0 ? void 0 : _b.match(/^bearer (.*)$/i);
66
- return m === null || m === void 0 ? void 0 : m[1];
112
+ const m = req?.headers.authorization?.match(/^bearer (.*)$/i);
113
+ return m?.[1];
67
114
  }
68
115
  async authFromReq(req) {
69
116
  const token = this.tokenFromReq(req);
@@ -75,55 +122,22 @@ class Context extends MockContext {
75
122
  return payload;
76
123
  }
77
124
  }
78
- _a = Context;
79
- Context.issuerKeys = new Map();
80
- Context.issuerConfig = new Map();
81
- Context.tokenCache = new txstate_utils_1.Cache(async (token, { req, ctx }) => {
82
- var _b, _c;
83
- // `this` is always the Context class, even if we are making instances of a subclass of Context
84
- // we need to get the instance's constructor instead in case it has overridden one of our
85
- // static methods/variables
86
- const ctxStatic = ctx.constructor;
87
- const logger = (_b = req === null || req === void 0 ? void 0 : req.log) !== null && _b !== void 0 ? _b : console;
88
- let verifyKey = _a.jwtVerifyKey;
89
- try {
90
- const claims = (0, jose_1.decodeJwt)(token);
91
- if (claims.iss && ctxStatic.issuerKeys.has(claims.iss))
92
- verifyKey = ctxStatic.issuerKeys.get(claims.iss);
93
- if (!verifyKey) {
94
- logger.info(`Received token with issuer: ${claims.iss} but JWT secret could not be found. The server may be misconfigured or the user may have presented a JWT from an untrusted issuer.`);
95
- return undefined;
96
- }
97
- await ((_c = ctxStatic.validateToken) === null || _c === void 0 ? void 0 : _c.call(ctxStatic, token, ctxStatic.issuerConfig.get(claims.iss), claims));
98
- const { payload } = await (0, jose_1.jwtVerify)(token, verifyKey);
99
- return await ctx.authFromPayload(payload);
100
- }
101
- catch (e) {
102
- // squelch errors about bad tokens, we can already see the 401 in the log
103
- if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED')
104
- logger.error(e);
105
- return undefined;
106
- }
107
- }, { freshseconds: 10 });
108
- Context.hasInitialized = false;
109
125
  class TxStateUAuthContext extends Context {
110
126
  static processIssuerConfig(config) {
111
- var _b;
112
127
  if (config.iss === 'unified-auth') {
113
- config.validateUrl = new URL((_b = config.url) !== null && _b !== void 0 ? _b : '');
128
+ config.validateUrl = new URL(config.url ?? '');
114
129
  config.validateUrl.pathname = '/validateToken';
115
130
  }
116
131
  return config;
117
132
  }
118
133
  static async validateToken(token, issuerConfig, claims) {
119
- var _b;
120
134
  if (claims.iss === 'unified-auth') {
121
135
  const validateUrl = new URL(issuerConfig.validateUrl);
122
136
  validateUrl.searchParams.set('unifiedJwt', token);
123
137
  const resp = await fetch(validateUrl);
124
138
  const validate = await resp.json();
125
139
  if (!validate.valid)
126
- throw new Error((_b = validate.reason) !== null && _b !== void 0 ? _b : 'Your session has been ended on another device or in another browser tab/window. It\'s also possible your NetID is no longer active.');
140
+ throw new Error(validate.reason ?? 'Your session has been ended on another device or in another browser tab/window. It\'s also possible your NetID is no longer active.');
127
141
  }
128
142
  }
129
143
  async authFromPayload(payload) {
@@ -142,11 +156,10 @@ async function unifiedAuthenticate(req, ContextClass = TxStateUAuthContext) {
142
156
  }
143
157
  exports.unifiedAuthenticate = unifiedAuthenticate;
144
158
  async function unifiedAuthenticateAll(req, ContextClass = TxStateUAuthContext) {
145
- var _b;
146
159
  ContextClass.init();
147
160
  const ctx = new ContextClass(req);
148
161
  const auth = await ctx.waitForAuth();
149
- if (!(auth === null || auth === void 0 ? void 0 : auth.username.length) && !((_b = req.routeOptions.url) === null || _b === void 0 ? void 0 : _b.startsWith('/docs')))
162
+ if (!auth?.username.length && !req.routeOptions.url?.startsWith('/docs'))
150
163
  throw new Error('All requests require authentication.');
151
164
  return auth;
152
165
  }
@@ -77,7 +77,7 @@ export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
77
77
  *
78
78
  * If this function throws, the client will receive a 401 response.
79
79
  */
80
- authenticate?: <T extends FastifyTxStateAuthInfo>(req: FastifyRequest) => Promise<T | undefined>;
80
+ authenticate?: (req: FastifyRequest) => Promise<FastifyTxStateAuthInfo | undefined>;
81
81
  }
82
82
  declare module 'fastify' {
83
83
  interface FastifyRequest {
@@ -114,6 +114,8 @@ export declare const devLogger: {
114
114
  child(bindings: any, options?: any): any;
115
115
  };
116
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;
117
119
  export default class Server {
118
120
  protected config: FastifyTxStateOptions & {
119
121
  http2?: true;
@@ -130,7 +132,7 @@ export default class Server {
130
132
  protected validOrigins: Record<string, boolean>;
131
133
  protected validOriginHosts: Record<string, boolean>;
132
134
  protected validOriginSuffixes: Set<string>;
133
- app: FastifyInstance<RawServerDefault, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, FastifyBaseLogger, JsonSchemaToTsProvider>;
135
+ app: FastifyInstanceTyped;
134
136
  constructor(config?: FastifyTxStateOptions & {
135
137
  http2?: true;
136
138
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-txstate",
3
- "version": "3.2.10",
3
+ "version": "3.2.12",
4
4
  "description": "A small wrapper for fastify providing a set of common conventions & utility functions we use.",
5
5
  "exports": {
6
6
  ".": {
@@ -23,6 +23,7 @@
23
23
  "@fastify/swagger-ui": "^3.0.0",
24
24
  "@fastify/type-provider-json-schema-to-ts": "^3.0.0",
25
25
  "@txstate-mws/fastify-shared": "^1.0.9",
26
+ "@types/ua-parser-js": ">=0.7.39",
26
27
  "ajv-errors": "^3.0.0",
27
28
  "ajv-formats": "^2.1.1",
28
29
  "fastify": "^4.9.2",
@@ -36,7 +37,6 @@
36
37
  "@types/chai": "^4.2.14",
37
38
  "@types/mocha": "^10.0.0",
38
39
  "@types/node": "^20.0.0",
39
- "@types/ua-parser-js": ">=0.7.39",
40
40
  "axios": "^1.6.8",
41
41
  "chai": "^4.2.0",
42
42
  "eslint-config-standard-with-typescript": "^43.0.0",