• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

u-wave / core / 20851208770

09 Jan 2026 11:59AM UTC coverage: 86.039% (-0.02%) from 86.058%
20851208770

Pull #740

github

web-flow
Merge fff37dca3 into 68a1abe2d
Pull Request #740: Replace custom rate limiter by express-rate-limit package

1004 of 1199 branches covered (83.74%)

Branch coverage included in aggregate %.

19 of 25 new or added lines in 3 files covered. (76.0%)

10539 of 12217 relevant lines covered (86.27%)

100.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.07
/src/HttpApi.js
1
import fs from 'node:fs';
1✔
2
import http from 'node:http';
1✔
3
import { randomUUID } from 'node:crypto';
1✔
4
import express from 'express';
1✔
5
import bodyParser from 'body-parser';
1✔
6
import cookieParser from 'cookie-parser';
1✔
7
import cors from 'cors';
1✔
8
import helmet from 'helmet';
1✔
9
import session from 'express-session';
1✔
10
import { rateLimit } from 'express-rate-limit';
1✔
11
import qs from 'qs';
1✔
12
import { pinoHttp } from 'pino-http';
1✔
13

1✔
14
// routes
1✔
15
import authenticate from './routes/authenticate.js';
1✔
16
import bans from './routes/bans.js';
1✔
17
import search from './routes/search.js';
1✔
18
import server from './routes/server.js';
1✔
19
import users from './routes/users.js';
1✔
20
import now from './routes/now.js';
1✔
21
import imports from './routes/import.js';
1✔
22

1✔
23
// middleware
1✔
24
import addFullUrl from './middleware/addFullUrl.js';
1✔
25
import attachUwaveMeta from './middleware/attachUwaveMeta.js';
1✔
26
import errorHandler from './middleware/errorHandler.js';
1✔
27

1✔
28
// utils
1✔
29
import AuthRegistry from './AuthRegistry.js';
1✔
30
import matchOrigin from './utils/matchOrigin.js';
1✔
31
import SqliteSessionStore from './utils/SqliteSessionStore.js';
1✔
32
import { MS_PER_WEEK } from './utils/date.js';
1✔
33
import { RateLimitError } from './errors/index.js';
1✔
34
import ms from 'ms';
1✔
35

1✔
36
const SESSION_DURATION = MS_PER_WEEK;
1✔
37

1✔
38
const optionsSchema = JSON.parse(
1✔
39
  fs.readFileSync(new URL('./schemas/httpApi.json', import.meta.url), 'utf8'),
1✔
40
);
1✔
41

1✔
42
/**
1✔
43
 * @param {{ token: string, requestUrl: string }} options
1✔
44
 * @returns {import('nodemailer').SendMailOptions}
1✔
45
 */
1✔
46
function defaultCreatePasswordResetEmail({ token, requestUrl }) {
1✔
47
  const parsed = new URL(requestUrl);
1✔
48
  const { hostname } = parsed;
1✔
49
  const resetLink = new URL(`/reset/${token}`, parsed);
1✔
50
  return {
1✔
51
    from: `noreply@${hostname}`,
1✔
52
    subject: 'üWave Password Reset Request',
1✔
53
    text: `
1✔
54
      Hello,
1✔
55

1✔
56
      To reset your password, please visit:
1✔
57
      ${resetLink}
1✔
58
    `,
1✔
59
  };
1✔
60
}
1✔
61

1✔
62
/**
1✔
63
 * @typedef {express.Router & { authRegistry: AuthRegistry }} HttpApi
1✔
64
 */
1✔
65

1✔
66
/**
1✔
67
 * @typedef {object} HttpApiOptions - Static options for the HTTP API.
1✔
68
 * @prop {string|Buffer} secret
1✔
69
 * @prop {boolean} [helmet]
1✔
70
 * @prop {boolean | number | string} [trustProxy]
1✔
71
 * @prop {(error: Error) => void} [onError]
1✔
72
 * @prop {{ secret: string }} [recaptcha]
1✔
73
 * @prop {import('nodemailer').Transport} [mailTransport]
1✔
74
 * @prop {(options: { token: string, requestUrl: string }) =>
1✔
75
 *   import('nodemailer').SendMailOptions} [createPasswordResetEmail]
1✔
76
 * @typedef {object} HttpApiSettings - Runtime options for the HTTP API.
1✔
77
 * @prop {string[]} allowedOrigins
1✔
78
 */
1✔
79

1✔
80
/**
1✔
81
 * @param {import('./Uwave.js').Boot} uw
1✔
82
 * @param {HttpApiOptions} options
1✔
83
 */
1✔
84
async function httpApi(uw, options) {
161✔
85
  if (!options.secret) {
161!
86
    throw new TypeError('"options.secret" is empty. This option is used to sign authentication '
×
87
      + 'keys, and is required for security reasons.');
×
88
  }
×
89

161✔
90
  if (options.onError != null && typeof options.onError !== 'function') {
161!
91
    throw new TypeError('"options.onError" must be a function.');
×
92
  }
×
93

161✔
94
  const logger = uw.logger.child({
161✔
95
    ns: 'uwave:http-api',
161✔
96
    level: 'warn',
161✔
97
  });
161✔
98

161✔
99
  uw.config.register(optionsSchema['uw:key'], optionsSchema);
161✔
100

161✔
101
  /** @type {HttpApiSettings} */
161✔
102
  // @ts-expect-error TS2322: get() always returns a validated object here
161✔
103
  let runtimeOptions = await uw.config.get(optionsSchema['uw:key']);
161✔
104
  const unsubscribe = uw.config.subscribe('u-wave:api', /** @param {HttpApiSettings} settings */ (settings) => {
161✔
105
    runtimeOptions = settings;
×
106
  });
161✔
107

161✔
108
  logger.debug(runtimeOptions, 'start HttpApi');
161✔
109
  uw.httpApi = Object.assign(express.Router(), {
161✔
110
    authRegistry: new AuthRegistry(uw.redis),
161✔
111
  });
161✔
112

161✔
113
  uw.express = express();
161✔
114
  uw.express.set('query parser', /** @param {string} str */ (str) => qs.parse(str, { depth: 1 }));
161✔
115
  if (options.trustProxy != null) {
161!
116
    uw.express.set('trust proxy', options.trustProxy);
×
117
  }
×
118

161✔
119
  uw.httpApi
161✔
120
    .use(pinoHttp({
161✔
121
      genReqId: () => randomUUID(),
161✔
122
      quietReqLogger: true,
161✔
123
      logger,
161✔
124
    }))
161✔
125
    .use(bodyParser.json())
161✔
126
    .use(cookieParser())
161✔
127
    .use(session({
161✔
128
      secret: options.secret,
161✔
129
      resave: false,
161✔
130
      saveUninitialized: false,
161✔
131
      rolling: true,
161✔
132
      cookie: {
161✔
133
        secure: uw.express.get('env') === 'production',
161✔
134
        httpOnly: true,
161✔
135
        maxAge: SESSION_DURATION,
161✔
136
      },
161✔
137
      store: new SqliteSessionStore(uw.db, uw.logger.child({ ns: 'uwave:sessions' })),
161✔
138
    }))
161✔
139
    .use(uw.passport.initialize())
161✔
140
    .use(addFullUrl())
161✔
141
    .use(attachUwaveMeta(uw.httpApi, uw))
161✔
142
    .use(uw.passport.authenticate('jwt', { session: false }))
161✔
143
    .use(uw.passport.session())
161✔
144
    .use(rateLimit({
161✔
145
      limit: 500,
161✔
146
      windowMs: 60_000,
161✔
147
      handler: (_req, res, next) => {
161✔
NEW
148
        next(new RateLimitError({
×
NEW
149
          'retry-after': ms(Number(res.get('Retry-After')) * 1_000, { long: true }),
×
NEW
150
        }));
×
151
      },
161✔
152
    }));
161✔
153

161✔
154
  uw.httpApi
161✔
155
    .use('/auth', authenticate(uw.passport, {
161✔
156
      secret: options.secret,
161✔
157
      mailTransport: options.mailTransport,
161✔
158
      recaptcha: options.recaptcha,
161✔
159
      createPasswordResetEmail:
161✔
160
        options.createPasswordResetEmail ?? defaultCreatePasswordResetEmail,
161✔
161
    }))
161✔
162
    .use('/bans', bans())
161✔
163
    .use('/import', imports())
161✔
164
    .use('/now', now())
161✔
165
    .use('/search', search())
161✔
166
    .use('/server', server())
161✔
167
    .use('/users', users());
161✔
168

161✔
169
  uw.server = http.createServer(uw.express);
161✔
170
  if (options.helmet !== false) {
161✔
171
    uw.express.use(helmet({
161✔
172
      referrerPolicy: {
161✔
173
        policy: ['origin-when-cross-origin'],
161✔
174
      },
161✔
175
    }));
161✔
176
  }
161✔
177

161✔
178
  /** @type {import('cors').CorsOptions} */
161✔
179
  const corsOptions = {
161✔
180
    origin(origin, callback) {
161✔
181
      callback(null, matchOrigin(origin, runtimeOptions.allowedOrigins));
290✔
182
    },
161✔
183
  };
161✔
184
  uw.express.options('/api/*path', cors(corsOptions));
161✔
185
  uw.express.use('/api', cors(corsOptions), uw.httpApi);
161✔
186
  // An older name
161✔
187
  uw.express.use('/v1', cors(corsOptions), uw.httpApi);
161✔
188

161✔
189
  uw.onClose(() => {
161✔
190
    unsubscribe();
161✔
191
    uw.server.close();
161✔
192
  });
161✔
193
}
161✔
194

1✔
195
/**
1✔
196
 * @param {import('./Uwave.js').Boot} uw
1✔
197
 */
1✔
198
async function errorHandling(uw) {
161✔
199
  uw.logger.debug({ ns: 'uwave:http-api' }, 'setup HTTP error handling');
161✔
200
  uw.httpApi.use(errorHandler({
161✔
201
    onError(_req, error) {
161✔
202
      if ('status' in error && typeof error.status === 'number' && error.status >= 400 && error.status < 500) {
129✔
203
        return;
129✔
204
      }
129✔
205

×
206
      uw.logger.error({ err: error, ns: 'uwave:http-api' });
×
207
    },
161✔
208
  }));
161✔
209
}
161✔
210

1✔
211
export default httpApi;
1✔
212
export { errorHandling };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc