fastify-txstate 3.2.10 → 3.2.11

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
@@ -52,27 +52,30 @@ exports.prodLogger = {
52
52
  };
53
53
  },
54
54
  res(res) {
55
- var _a, _b, _c, _d;
56
55
  return {
57
56
  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]),
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]),
60
59
  ...res.extraLogInfo,
61
- auth: (_d = (_c = res.request) === null || _c === void 0 ? void 0 : _c.auth) !== null && _d !== void 0 ? _d : res.extraLogInfo.auth
60
+ auth: res.request?.auth ?? res.extraLogInfo.auth
62
61
  };
63
62
  }
64
63
  }
65
64
  };
66
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
+ app;
67
77
  constructor(config = {}) {
68
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
69
78
  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
79
  try {
77
80
  const key = node_fs_1.default.readFileSync('/securekeys/private.key');
78
81
  const cert = node_fs_1.default.readFileSync('/securekeys/cert.pem');
@@ -101,16 +104,15 @@ class Server {
101
104
  else
102
105
  config.trustProxy = process.env.TRUST_PROXY;
103
106
  }
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 } };
107
+ 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
108
  this.healthCallback = config.checkHealth;
106
109
  this.app = (0, fastify_1.fastify)(config);
107
110
  this.app.addHook('onRoute', route => {
108
- var _a, _b, _c;
109
- if (!((_a = route.schema) === null || _a === void 0 ? void 0 : _a.body))
111
+ if (!route.schema?.body)
110
112
  return;
111
- const missingResponse = ((_b = route.schema) === null || _b === void 0 ? void 0 : _b.response) == null;
113
+ const missingResponse = route.schema?.response == null;
112
114
  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);
115
+ let newSchema = (0, txstate_utils_1.set)(route.schema ?? {}, 'response.400', response400);
114
116
  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
117
  newSchema = (0, txstate_utils_1.set)(newSchema, 'response.422', response422);
116
118
  if (missingResponse) {
@@ -130,15 +132,13 @@ class Server {
130
132
  * convert all nulls to undefined before fastify validates.
131
133
  */
132
134
  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;
135
+ return req.routeSchema?.response ? (0, txstate_utils_1.destroyNulls)(payload) : payload;
135
136
  });
136
137
  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 : [])]);
138
+ this.setValidOrigins([...(config.validOrigins ?? []), ...(process.env.VALID_ORIGINS?.split(',') ?? [])]);
139
+ this.setValidOriginHosts([...(config.validOriginHosts ?? []), ...(process.env.VALID_ORIGIN_HOSTS?.split(',') ?? [])]);
140
+ this.setValidOriginSuffixes([...(config.validOriginSuffixes ?? []), ...(process.env.VALID_ORIGIN_SUFFIXES?.split(',') ?? [])]);
140
141
  this.app.addHook('preHandler', async (req, res) => {
141
- var _a;
142
142
  res.extraLogInfo = {};
143
143
  if (!req.headers.origin)
144
144
  return;
@@ -160,7 +160,7 @@ class Server {
160
160
  }
161
161
  }
162
162
  }
163
- if (!passed && ((_a = config.checkOrigin) === null || _a === void 0 ? void 0 : _a.call(config, req)))
163
+ if (!passed && config.checkOrigin?.(req))
164
164
  passed = true;
165
165
  if (!passed) {
166
166
  await res.status(403).send('Origin check failed. Suspected XSRF attack.');
@@ -211,7 +211,6 @@ class Server {
211
211
  });
212
212
  this.app.setNotFoundHandler((req, res) => { void res.status(404).send('Not Found.'); });
213
213
  this.app.setErrorHandler(async (err, req, res) => {
214
- var _a;
215
214
  req.log.warn(err);
216
215
  for (const errorHandler of this.errorHandlers) {
217
216
  if (!res.sent)
@@ -227,7 +226,7 @@ class Server {
227
226
  else if (err.code === 'FST_ERR_VALIDATION') {
228
227
  const developerErrors = [];
229
228
  const userErrors = [];
230
- for (const v of (_a = err.validation) !== null && _a !== void 0 ? _a : []) {
229
+ for (const v of err.validation ?? []) {
231
230
  if (v.keyword === 'errorMessage') {
232
231
  for (const ov of v.params.errors) {
233
232
  if (['type', 'additionalProperties'].includes(ov.keyword))
@@ -269,11 +268,11 @@ class Server {
269
268
  }
270
269
  else if (this.healthCallback) {
271
270
  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];
271
+ const [status, msg] = typeof resp === 'string' ? [500, resp] : [resp?.status, resp?.message];
273
272
  if (!!msg || !!status) {
274
273
  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';
274
+ void res.status(status ?? 500);
275
+ return msg ?? 'FAIL';
277
276
  }
278
277
  }
279
278
  return 'OK';
@@ -287,18 +286,16 @@ class Server {
287
286
  process.on('SIGINT', this.sigHandler);
288
287
  }
289
288
  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');
289
+ const customPort = port ?? parseInt(process.env.PORT ?? '0');
292
290
  await this.app.ready();
293
- (_c = (_b = this.app).swagger) === null || _c === void 0 ? void 0 : _c.call(_b);
291
+ this.app.swagger?.();
294
292
  if (customPort) {
295
293
  await this.app.listen({ port: customPort, host: '0.0.0.0' });
296
294
  }
297
295
  else if (this.https) {
298
296
  // redirect 80 to 443
299
297
  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 : '') });
298
+ res.writeHead(301, { Location: 'https://' + (req?.headers?.host?.replace(/:\d+$/, '') ?? '') + (req.url ?? '') });
302
299
  res.end();
303
300
  }).listen(80);
304
301
  await this.app.listen({ port: 443, host: '0.0.0.0' });
@@ -328,8 +325,7 @@ class Server {
328
325
  this.validOriginSuffixes.add(s);
329
326
  }
330
327
  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 : {};
328
+ let openapi = opts?.openapi ?? {};
333
329
  if (this.config.authenticate === unified_auth_1.unifiedAuthenticate) {
334
330
  openapi = (0, txstate_utils_1.set)(openapi, 'components.securitySchemes', {
335
331
  unifiedAuth: {
@@ -344,12 +340,11 @@ this is log into this application and use dev tools to pull your token from the
344
340
  openapi.security = [{ unifiedAuth: [] }];
345
341
  }
346
342
  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' });
343
+ await this.app.register(swagger_ui_1.default, { ...opts?.ui, routePrefix: opts?.path ?? opts?.ui?.routePrefix ?? '/docs' });
348
344
  }
349
345
  async close(softSeconds) {
350
- var _a;
351
346
  if (typeof softSeconds === 'undefined')
352
- softSeconds = parseInt((_a = process.env.LOAD_BALANCE_TIMEOUT) !== null && _a !== void 0 ? _a : '0');
347
+ softSeconds = parseInt(process.env.LOAD_BALANCE_TIMEOUT ?? '0');
353
348
  process.removeListener('SIGTERM', this.sigHandler);
354
349
  process.removeListener('SIGINT', this.sigHandler);
355
350
  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.11",
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",