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

boardgameio / boardgame.io / 15797955584

21 Jun 2025 05:20PM UTC coverage: 97.263% (-2.7%) from 100.0%
15797955584

Pull #1226

github

web-flow
Merge 8ac078c34 into 4f3c90df0
Pull Request #1226: Koa to express

1644 of 1714 branches covered (95.92%)

Branch coverage included in aggregate %.

347 of 349 new or added lines in 3 files covered. (99.43%)

228 existing lines in 10 files now uncovered.

9337 of 9576 relevant lines covered (97.5%)

6155.46 hits per line

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

99.42
/src/server/api.ts
1
/*
4✔
2
 * Copyright 2018 The boardgame.io Authors
4✔
3
 *
4✔
4
 * Use of this source code is governed by a MIT-style
4✔
5
 * license that can be found in the LICENSE file or at
4✔
6
 * https://opensource.org/licenses/MIT.
4✔
7
 */
4✔
8

4✔
9
import type { CorsOptions } from 'cors';
4✔
10
import type { Request, Response, NextFunction, Router } from 'express';
4✔
11
import express from 'express';
4✔
12
import cors from 'cors';
4✔
13
import { nanoid } from 'nanoid';
4✔
14
import { createMatch, getFirstAvailablePlayerID, getNumPlayers } from './util';
4✔
15
import type { Auth } from './auth';
4✔
16
import type { Server, LobbyAPI, Game, StorageAPI } from '../types';
4✔
17

4✔
18
/**
4✔
19
 * Creates a new match.
4✔
20
 *
4✔
21
 * @param {object} db - The storage API.
4✔
22
 * @param {object} game - The game config object.
4✔
23
 * @param {number} numPlayers - The number of players.
4✔
24
 * @param {object} setupData - User-defined object that's available
4✔
25
 *                             during game setup.
4✔
26
 * @param {object } lobbyConfig - Configuration options for the lobby.
4✔
27
 * @param {boolean} unlisted - Whether the match should be excluded from public listing.
4✔
28
 */
4✔
29
const CreateMatch = async ({
4✔
30
  res,
88✔
31
  db,
88✔
32
  uuid,
88✔
33
  ...opts
88✔
34
}: {
88✔
35
  db: StorageAPI.Sync | StorageAPI.Async;
88✔
36
  res: Response;
88✔
37
  uuid: () => string;
88✔
38
} & Parameters<typeof createMatch>[0]): Promise<string> => {
88✔
39
  const matchID = uuid();
88✔
40
  const match = createMatch(opts);
88✔
41

88✔
42
  if ('setupDataError' in match) {
88✔
43
    res.status(400).send(match.setupDataError);
4✔
44
    throw new Error(match.setupDataError);
4✔
45
  } else {
88✔
46
    await db.createMatch(matchID, match);
84✔
47
    return matchID;
84✔
48
  }
84✔
49
};
88✔
50

4✔
51
/**
4✔
52
 * Create a metadata object without secret credentials to return to the client.
4✔
53
 *
4✔
54
 * @param {string} matchID - The identifier of the match the metadata belongs to.
4✔
55
 * @param {object} metadata - The match metadata object to strip credentials from.
4✔
56
 * @return - A metadata object without player credentials.
4✔
57
 */
4✔
58
const createClientMatchData = (
4✔
59
  matchID: string,
148✔
60
  metadata: Server.MatchData
148✔
61
): LobbyAPI.Match => {
148✔
62
  return {
148✔
63
    ...metadata,
148✔
64
    matchID,
148✔
65
    players: Object.values(metadata.players).map((player) => {
148✔
66
      // strip away credentials
296✔
67
      const { credentials, ...strippedInfo } = player;
296✔
68
      return strippedInfo;
296✔
69
    }),
148✔
70
  };
148✔
71
};
148✔
72

4✔
73
/** Utility extracting `string` from a query if it is `string[]`. */
4✔
74
const unwrapQuery = (
4✔
75
  query: undefined | string | string[]
204✔
76
): string | undefined => (Array.isArray(query) ? query[0] : query);
4✔
77

4✔
78
export const configureRouter = ({
4✔
79
  router,
410✔
80
  db,
410✔
81
  auth,
410✔
82
  games,
410✔
83
  uuid = () => nanoid(11),
410✔
84
}: {
410✔
85
  router: Router;
410✔
86
  auth: Auth;
410✔
87
  games: Game[];
410✔
88
  uuid?: () => string;
410✔
89
  db: StorageAPI.Sync | StorageAPI.Async;
410✔
90
}) => {
410✔
91
  /**
410✔
92
   * List available games.
410✔
93
   *
410✔
94
   * @return - Array of game names as string.
410✔
95
   */
410✔
96
  router.get('/games', async (req: Request, res: Response) => {
410✔
97
    const body: LobbyAPI.GameList = games.map((game) => game.name);
22✔
98
    res.json(body);
22✔
99
  });
410✔
100

410✔
101
  /**
410✔
102
   * Create a new match of a given game.
410✔
103
   *
410✔
104
   * @param {string} name - The name of the game of the new match.
410✔
105
   * @param {number} numPlayers - The number of players.
410✔
106
   * @param {object} setupData - User-defined object that's available
410✔
107
   *                             during game setup.
410✔
108
   * @param {boolean} unlisted - Whether the match should be excluded from public listing.
410✔
109
   * @return - The ID of the created match.
410✔
110
   */
410✔
111
  router.post(
410✔
112
    '/games/:name/create',
410✔
113
    express.json(),
410✔
114
    async (req: Request, res: Response) => {
410✔
115
      const gameName = req.params.name;
96✔
116
      const setupData = req.body?.setupData;
96✔
117
      const unlisted = req.body?.unlisted;
96✔
118
      const numPlayers = Number.parseInt(req.body?.numPlayers);
96✔
119

96✔
120
      const game = games.find((g) => g.name === gameName);
96✔
121
      if (!game) return res.status(404).send('Game ' + gameName + ' not found');
96✔
122

92✔
123
      if (
92✔
124
        req.body.numPlayers !== undefined &&
92✔
125
        (Number.isNaN(numPlayers) ||
72✔
126
          (game.minPlayers && numPlayers < game.minPlayers) ||
72✔
127
          (game.maxPlayers && numPlayers > game.maxPlayers))
72✔
128
      ) {
96✔
129
        return res.status(400).send('Invalid numPlayers');
12✔
130
      }
12✔
131

80✔
132
      try {
80✔
133
        const matchID = await CreateMatch({
80✔
134
          res,
80✔
135
          db,
80✔
136
          game,
80✔
137
          numPlayers,
80✔
138
          setupData,
80✔
139
          uuid,
80✔
140
          unlisted,
80✔
141
        });
80✔
142
        const body: LobbyAPI.CreatedMatch = { matchID };
76✔
143
        res.json(body);
76✔
144
      } catch {
96✔
145
        // Error already handled in CreateMatch
4✔
146
      }
4✔
147
    }
96✔
148
  );
410✔
149

410✔
150
  /**
410✔
151
   * List matches for a given game.
410✔
152
   *
410✔
153
   * This does not return matches that are marked as unlisted.
410✔
154
   *
410✔
155
   * @param {string} name - The name of the game.
410✔
156
   * @return - Array of match objects.
410✔
157
   */
410✔
158
  router.get('/games/:name', async (req: Request, res: Response) => {
410✔
159
    const gameName = req.params.name;
68✔
160
    const isGameoverString = unwrapQuery(
68✔
161
      req.query.isGameover as string | string[]
68✔
162
    );
68✔
163
    const updatedBeforeString = unwrapQuery(
68✔
164
      req.query.updatedBefore as string | string[]
68✔
165
    );
68✔
166
    const updatedAfterString = unwrapQuery(
68✔
167
      req.query.updatedAfter as string | string[]
68✔
168
    );
68✔
169

68✔
170
    let isGameover: boolean | undefined;
68✔
171
    if (isGameoverString) {
68✔
172
      if (isGameoverString.toLowerCase() === 'true') {
16✔
173
        isGameover = true;
8✔
174
      } else if (isGameoverString.toLowerCase() === 'false') {
8✔
175
        isGameover = false;
4✔
176
      }
4✔
177
    }
16✔
178
    let updatedBefore: number | undefined;
68✔
179
    if (updatedBeforeString) {
68✔
180
      const parsedNumber = Number.parseInt(updatedBeforeString, 10);
12✔
181
      if (parsedNumber > 0) {
12✔
182
        updatedBefore = parsedNumber;
8✔
183
      }
8✔
184
    }
12✔
185
    let updatedAfter: number | undefined;
68✔
186
    if (updatedAfterString) {
68✔
187
      const parsedNumber = Number.parseInt(updatedAfterString, 10);
12✔
188
      if (parsedNumber > 0) {
12✔
189
        updatedAfter = parsedNumber;
8✔
190
      }
8✔
191
    }
12✔
192
    const matchList = await db.listMatches({
68✔
193
      gameName,
68✔
194
      where: {
68✔
195
        isGameover,
68✔
196
        updatedAfter,
68✔
197
        updatedBefore,
68✔
198
      },
68✔
199
    });
68✔
200
    const matches = [];
68✔
201
    for (const matchID of matchList) {
68✔
202
      const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
204✔
203
        metadata: true,
204✔
204
      });
204✔
205
      if (!metadata.unlisted) {
204✔
206
        matches.push(createClientMatchData(matchID, metadata));
136✔
207
      }
136✔
208
    }
204✔
209
    const body: LobbyAPI.MatchList = { matches };
68✔
210
    res.json(body);
68✔
211
  });
410✔
212

410✔
213
  /**
410✔
214
   * Get data about a specific match.
410✔
215
   *
410✔
216
   * @param {string} name - The name of the game.
410✔
217
   * @param {string} id - The ID of the match.
410✔
218
   * @return - A match object.
410✔
219
   */
410✔
220
  router.get('/games/:name/:id', async (req: Request, res: Response) => {
410✔
221
    const matchID = req.params.id;
20✔
222
    const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
20✔
223
      metadata: true,
20✔
224
    });
20✔
225
    if (!metadata) {
20✔
226
      return res.status(404).send('Match ' + matchID + ' not found');
8✔
227
    }
8✔
228
    const body: LobbyAPI.Match = createClientMatchData(matchID, metadata);
12✔
229
    res.json(body);
12✔
230
  });
410✔
231

410✔
232
  /**
410✔
233
   * Join a given match.
410✔
234
   *
410✔
235
   * @param {string} name - The name of the game.
410✔
236
   * @param {string} id - The ID of the match.
410✔
237
   * @param {string} playerID - The ID of the player who joins. If not sent, will be assigned to the first index available.
410✔
238
   * @param {string} playerName - The name of the player who joins.
410✔
239
   * @param {object} data - The default data of the player in the match.
410✔
240
   * @return - Player ID and credentials to use when interacting in the joined match.
410✔
241
   */
410✔
242
  router.post(
410✔
243
    '/games/:name/:id/join',
410✔
244
    express.json(),
410✔
245
    async (req: Request, res: Response) => {
410✔
246
      let playerID = req.body.playerID;
60✔
247
      const playerName = req.body.playerName;
60✔
248
      const data = req.body.data;
60✔
249
      const matchID = req.params.id;
60✔
250
      if (!playerName) {
60✔
251
        return res.status(403).send('playerName is required');
4✔
252
      }
4✔
253

56✔
254
      const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
56✔
255
        metadata: true,
56✔
256
      });
56✔
257
      if (!metadata) {
60✔
258
        return res.status(404).send('Match ' + matchID + ' not found');
4✔
259
      }
4✔
260

52✔
261
      if (typeof playerID === 'undefined' || playerID === null) {
60✔
262
        playerID = getFirstAvailablePlayerID(metadata.players);
24✔
263
        if (playerID === undefined) {
24✔
264
          const numPlayers = getNumPlayers(metadata.players);
4✔
265
          return res
4✔
266
            .status(409)
4✔
267
            .send(
4✔
268
              `Match ${matchID} reached maximum number of players (${numPlayers})`
4✔
269
            );
4✔
270
        }
4✔
271
      }
24✔
272

48✔
273
      if (!metadata.players[playerID]) {
60✔
274
        return res.status(404).send('Player ' + playerID + ' not found');
4✔
275
      }
4✔
276
      if (metadata.players[playerID].name) {
60✔
277
        return res.status(409).send('Player ' + playerID + ' not available');
4✔
278
      }
4✔
279

40✔
280
      if (data) {
60✔
281
        metadata.players[playerID].data = data;
4✔
282
      }
4✔
283
      metadata.players[playerID].name = playerName;
40✔
284
      const playerCredentials = await auth.generateCredentials(req, res);
40✔
285
      metadata.players[playerID].credentials = playerCredentials;
40✔
286

40✔
287
      await db.setMetadata(matchID, metadata);
40✔
288

40✔
289
      const body: LobbyAPI.JoinedMatch = { playerID, playerCredentials };
40✔
290
      res.json(body);
40✔
291
    }
40✔
292
  );
410✔
293

410✔
294
  /**
410✔
295
   * Leave a given match.
410✔
296
   *
410✔
297
   * @param {string} name - The name of the game.
410✔
298
   * @param {string} id - The ID of the match.
410✔
299
   * @param {string} playerID - The ID of the player who leaves.
410✔
300
   * @param {string} credentials - The credentials of the player who leaves.
410✔
301
   * @return - Nothing.
410✔
302
   */
410✔
303
  router.post(
410✔
304
    '/games/:name/:id/leave',
410✔
305
    express.json(),
410✔
306
    async (req: Request, res: Response) => {
410✔
307
      const matchID = req.params.id;
36✔
308
      const playerID = req.body.playerID;
36✔
309
      const credentials = req.body.credentials;
36✔
310
      const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
36✔
311
        metadata: true,
36✔
312
      });
36✔
313
      if (typeof playerID === 'undefined' || playerID === null) {
36✔
314
        return res.status(403).send('playerID is required');
8✔
315
      }
8✔
316

28✔
317
      if (!metadata) {
36✔
318
        return res.status(404).send('Match ' + matchID + ' not found');
4✔
319
      }
4✔
320
      if (!metadata.players[playerID]) {
36✔
321
        return res.status(404).send('Player ' + playerID + ' not found');
4✔
322
      }
4✔
323
      const isAuthorized = await auth.authenticateCredentials({
20✔
324
        playerID,
20✔
325
        credentials,
20✔
326
        metadata,
20✔
327
      });
20✔
328
      if (!isAuthorized) {
36✔
329
        return res.status(403).send('Invalid credentials ' + credentials);
4✔
330
      }
4✔
331

16✔
332
      delete metadata.players[playerID].name;
16✔
333
      delete metadata.players[playerID].credentials;
16✔
334
      const hasPlayers = Object.values(metadata.players).some(
16✔
335
        ({ name }) => name
16✔
336
      );
16✔
337
      await (hasPlayers
16✔
338
        ? db.setMetadata(matchID, metadata) // Update metadata.
36✔
339
        : db.wipe(matchID)); // Delete match.
36✔
340
      res.json({});
16✔
341
    }
16✔
342
  );
410✔
343

410✔
344
  /**
410✔
345
   * Start a new match based on another existing match.
410✔
346
   *
410✔
347
   * @param {string} name - The name of the game.
410✔
348
   * @param {string} id - The ID of the match.
410✔
349
   * @param {string} playerID - The ID of the player creating the match.
410✔
350
   * @param {string} credentials - The credentials of the player creating the match.
410✔
351
   * @param {boolean} unlisted - Whether the match should be excluded from public listing.
410✔
352
   * @return - The ID of the new match.
410✔
353
   */
410✔
354
  router.post(
410✔
355
    '/games/:name/:id/playAgain',
410✔
356
    express.json(),
410✔
357
    async (req: Request, res: Response) => {
410✔
358
      const gameName = req.params.name;
28✔
359
      const matchID = req.params.id;
28✔
360
      const playerID = req.body.playerID;
28✔
361
      const credentials = req.body.credentials;
28✔
362
      const unlisted = req.body.unlisted;
28✔
363
      const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
28✔
364
        metadata: true,
28✔
365
      });
28✔
366

28✔
367
      if (typeof playerID === 'undefined' || playerID === null) {
28✔
368
        return res.status(403).send('playerID is required');
4✔
369
      }
4✔
370

24✔
371
      if (!metadata) {
28✔
372
        return res.status(404).send('Match ' + matchID + ' not found');
4✔
373
      }
4✔
374
      if (!metadata.players[playerID]) {
28✔
375
        return res.status(404).send('Player ' + playerID + ' not found');
4✔
376
      }
4✔
377
      const isAuthorized = await auth.authenticateCredentials({
16✔
378
        playerID,
16✔
379
        credentials,
16✔
380
        metadata,
16✔
381
      });
16✔
382
      if (!isAuthorized) {
28✔
383
        return res.status(403).send('Invalid credentials ' + credentials);
4✔
384
      }
4✔
385

12✔
386
      // Check if nextMatch is already set, if so, return that id.
12✔
387
      if (metadata.nextMatchID) {
28✔
388
        return res.json({ nextMatchID: metadata.nextMatchID });
4✔
389
      }
4✔
390

8✔
391
      const setupData = req.body.setupData || metadata.setupData;
28✔
392
      const numPlayers =
28✔
393
        Number.parseInt(req.body.numPlayers) ||
28✔
394
        // eslint-disable-next-line unicorn/explicit-length-check
4✔
395
        Object.keys(metadata.players).length;
28✔
396

28✔
397
      const game = games.find((g) => g.name === gameName);
28✔
398
      const nextMatchID = await CreateMatch({
28✔
399
        res,
28✔
400
        db,
28✔
401
        game,
28✔
402
        numPlayers,
28✔
403
        setupData,
28✔
404
        uuid,
28✔
405
        unlisted,
28✔
406
      });
28✔
407
      metadata.nextMatchID = nextMatchID;
8✔
408

8✔
409
      await db.setMetadata(matchID, metadata);
8✔
410

8✔
411
      const body: LobbyAPI.NextMatch = { nextMatchID };
8✔
412
      res.json(body);
8✔
413
    }
8✔
414
  );
410✔
415

410✔
416
  // Update player metadata
410✔
417
  const updatePlayerMetadata = async (req: Request, res: Response) => {
410✔
418
    const matchID = req.params.id;
100✔
419
    const playerID = req.body.playerID;
100✔
420
    const credentials = req.body.credentials;
100✔
421
    const newName = req.body.newName;
100✔
422
    const data = req.body.data;
100✔
423
    const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
100✔
424
      metadata: true,
100✔
425
    });
100✔
426
    if (typeof playerID === 'undefined') {
100✔
427
      return res.status(403).send('playerID is required');
12✔
428
    }
12✔
429
    if (data === undefined && !newName) {
100✔
430
      return res.status(403).send('newName or data is required');
12✔
431
    }
12✔
432
    if (newName && typeof newName !== 'string') {
100✔
433
      return res
8✔
434
        .status(403)
8✔
435
        .send(`newName must be a string, got ${typeof newName}`);
8✔
436
    }
8✔
437
    if (!metadata) {
100✔
438
      return res.status(404).send('Match ' + matchID + ' not found');
12✔
439
    }
12✔
440
    if (!metadata.players[playerID]) {
100✔
441
      return res.status(404).send('Player ' + playerID + ' not found');
12✔
442
    }
12✔
443
    const isAuthorized = await auth.authenticateCredentials({
44✔
444
      playerID,
44✔
445
      credentials,
44✔
446
      metadata,
44✔
447
    });
44✔
448
    if (!isAuthorized) {
100✔
449
      return res.status(403).send('Invalid credentials ' + credentials);
12✔
450
    }
12✔
451

32✔
452
    if (newName) {
100✔
453
      metadata.players[playerID].name = newName;
24✔
454
    }
24✔
455
    if (data) {
100✔
456
      metadata.players[playerID].data = data;
8✔
457
    }
8✔
458
    await db.setMetadata(matchID, metadata);
32✔
459
    res.json({});
32✔
460
  };
410✔
461

410✔
462
  /**
410✔
463
   * Change the name of a player in a given match.
410✔
464
   *
410✔
465
   * @param {string} name - The name of the game.
410✔
466
   * @param {string} id - The ID of the match.
410✔
467
   * @param {string} playerID - The ID of the player.
410✔
468
   * @param {string} credentials - The credentials of the player.
410✔
469
   * @param {object} newName - The new name of the player in the match.
410✔
470
   * @return - Nothing.
410✔
471
   */
410✔
472
  router.post(
410✔
473
    '/games/:name/:id/rename',
410✔
474
    express.json(),
410✔
475
    async (req: Request, res: Response) => {
410✔
476
      console.warn(
36✔
477
        'This endpoint /rename is deprecated. Please use /update instead.'
36✔
478
      );
36✔
479
      await updatePlayerMetadata(req, res);
36✔
480
    }
36✔
481
  );
410✔
482

410✔
483
  /**
410✔
484
   * Update the player's data for a given match.
410✔
485
   *
410✔
486
   * @param {string} name - The name of the game.
410✔
487
   * @param {string} id - The ID of the match.
410✔
488
   * @param {string} playerID - The ID of the player.
410✔
489
   * @param {string} credentials - The credentials of the player.
410✔
490
   * @param {object} newName - The new name of the player in the match.
410✔
491
   * @param {object} data - The new data of the player in the match.
410✔
492
   * @return - Nothing.
410✔
493
   */
410✔
494
  router.post('/games/:name/:id/update', express.json(), updatePlayerMetadata);
410✔
495

410✔
496
  return router;
410✔
497
};
410✔
498

4✔
499
export const configureApp = (
4✔
500
  app: express.Application,
410✔
501
  router: Router,
410✔
502
  origins: CorsOptions['origin']
410✔
503
): void => {
410✔
504
  app.use(
410✔
505
    // cors({
410✔
506
    //   origin: (origin, callback) => {
410✔
507
    //     console.log('cors', { origin, origins });
410✔
508
    //     if (!origin || isOriginAllowed(origin, origins)) {
410✔
509
    //       console.log('cors ok');
410✔
510
    //       callback(null, origin);
410✔
511
    //     } else {
410✔
512
    //       console.log('cors bad');
410✔
513
    //       callback(new Error('Not allowed by CORS'), origin);
410✔
514
    //     }
410✔
515
    //   },
410✔
516
    // })
410✔
517
    cors({ origin: origins })
410✔
518
  );
410✔
519

410✔
520
  // If API_SECRET is set, then require that requests set an
410✔
521
  // api-secret header that is set to the same value.
410✔
522
  app.use((req: Request, res: Response, next: NextFunction) => {
410✔
523
    if (
438✔
524
      !!process.env.API_SECRET &&
438✔
525
      req.headers['api-secret'] !== process.env.API_SECRET
8✔
526
    ) {
438✔
527
      return res.status(403).send('Invalid API secret');
4✔
528
    }
4✔
529
    next();
434✔
530
  });
410✔
531

410✔
532
  app.use(router);
410✔
533

410✔
534
  // If the request is not handled by the router, throw error.
410✔
535
  app.use((req: Request, res: Response) => {
410✔
536
    const error = new Error(`Not Found: ${req.method} ${req.originalUrl}`);
4✔
537
    (error as any).status = 404;
4✔
538
    throw error;
4✔
539
  });
410✔
540

410✔
541
  // Error handler
410✔
542
  app.use((err: any, req: Request, res: Response, next: NextFunction) => {
410✔
543
    console.error(err.stack);
8✔
544
    if (res.headersSent) {
8!
NEW
545
      return next(err);
×
NEW
546
    }
×
547
    if (err.status) {
8✔
548
      res.status(err.status).send(err.message || err.msg);
4!
549
    } else {
4✔
550
      res.status(500).send('Internal Server Error');
4✔
551
    }
4✔
552
  });
410✔
553
};
410✔
554

4✔
555
// /**
4✔
556
//  * Check if a request’s origin header is allowed for CORS.
4✔
557
//  * Adapted from `cors` package: https://github.com/expressjs/cors
4✔
558
//  * @param origin Request origin to test.
4✔
559
//  * @param allowedOrigin Origin(s) that are allowed to connect via CORS.
4✔
560
//  * @returns `true` if the origin matched at least one of the allowed origins.
4✔
561
//  */
4✔
562
// function isOriginAllowed(
4✔
563
//   origin: string,
4✔
564
//   allowedOrigin: CorsOptions['origin']
4✔
565
// ): boolean {
4✔
566
//   if (Array.isArray(allowedOrigin)) {
4✔
567
//     for (const entry of allowedOrigin) {
4✔
568
//       if (isOriginAllowed(origin, entry)) {
4✔
569
//         return true;
4✔
570
//       }
4✔
571
//     }
4✔
572
//     return false;
4✔
573
//   } else if (typeof allowedOrigin === 'string') {
4✔
574
//     return origin === allowedOrigin;
4✔
575
//   } else if (allowedOrigin instanceof RegExp) {
4✔
576
//     return allowedOrigin.test(origin);
4✔
577
//   } else {
4✔
578
//     return !!allowedOrigin;
4✔
579
//   }
4✔
580
// }
4✔
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