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

gittrends-app / github-proxy-server / 21040483265

15 Jan 2026 05:34PM UTC coverage: 80.499% (-0.5%) from 81.014%
21040483265

push

github

hsborges
v11.0.1

126 of 166 branches covered (75.9%)

Branch coverage included in aggregate %.

229 of 275 relevant lines covered (83.27%)

94.67 hits per line

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

91.86
/src/server.ts
1
#!/usr/bin/env node
2
/* Author: Hudson S. Borges */
3
import { existsSync, readFileSync } from 'node:fs';
4
import { resolve } from 'node:path';
5

6
import basicAuth from 'basic-auth';
7
import chalk from 'chalk';
8
import compression from 'compression';
9
import dayjs from 'dayjs';
10
import relativeTime from 'dayjs/plugin/relativeTime.js';
11
import express, { type Express, type Request, type Response } from 'express';
12
import compact from 'lodash/compact.js';
13
import uniq from 'lodash/uniq.js';
14
import { pino } from 'pino';
15
import { pinoHttp } from 'pino-http';
16
import pinoPretty from 'pino-pretty';
17
import swaggerStats from 'swagger-stats';
18
import { getBorderCharacters, table } from 'table';
19

20
import ProxyRouter, {
21
  type ProxyRouterOpts,
22
  ProxyRouterResponse,
23
  type WorkerLogger
24
} from './router.js';
25

26
dayjs.extend(relativeTime);
2✔
27

28
function statusFormatter(status: number | string): string {
29
  switch (true) {
6!
30
    case /[23]\d{2}/.test(`${status}`):
31
      return chalk.green(status);
2✔
32
    case /[4]\d{2}/.test(`${status}`):
33
      return chalk.yellow(status);
×
34
    default:
35
      return chalk.red(status);
4✔
36
  }
37
}
38

39
function logTransform(chunk: WorkerLogger): string {
40
  const data = {
6✔
41
    resource: chunk.resource,
42
    token: chunk.token,
43
    pending: chunk.pending,
44
    remaining: chunk.remaining,
45
    reset: dayjs.unix(chunk.reset).fromNow(),
46
    duration: `${chunk.duration / 1000}s`,
47
    status: statusFormatter(chunk.status || '-')
10✔
48
  };
49

50
  return `${table([Object.values(data)], {
6✔
51
    columnDefault: { alignment: 'right', width: 5 },
52
    columns: {
53
      0: { width: 11 },
54
      1: { width: 5 },
55
      2: { width: 3 },
56
      3: { width: 5 },
57
      4: { width: 18 },
58
      5: { width: 7 },
59
      6: { width: `${chunk.status || '-'}`.length, alignment: 'left' }
10✔
60
    },
61
    border: getBorderCharacters('void'),
62
    singleLine: true
63
  }).trimEnd()}\n`;
64
}
65

66
// parse tokens from input
67
export function parseTokens(text: string): string[] {
68
  return text
21✔
69
    .split(/\n/g)
70
    .map((v) => v.replace(/\s/g, ''))
35✔
71
    .reduce((acc: string[], v: string) => {
72
      if (!v || /^(\/{2}|#).*/gi.test(v)) return acc;
35✔
73
      return acc.concat([v.replace(/.*:(.+)/i, '$1')]);
22✔
74
    }, [])
75
    .reduce((acc: string[], token: string) => concatTokens(token, acc), []);
22✔
76
}
77

78
// concat tokens in commander
79
export function concatTokens(token: string, list: string[]): string[] {
80
  if (token.length !== 40)
44✔
81
    throw new Error('Invalid access token detected (they have 40 characters)');
5✔
82
  return uniq([...list, token]);
39✔
83
}
84

85
// read tokens from a file
86
export function readTokensFile(filename: string): string[] {
87
  const filepath = resolve(process.cwd(), filename);
7✔
88
  if (!existsSync(filepath)) throw new Error(`File "${filename}" not found!`);
7✔
89
  return parseTokens(readFileSync(filepath, 'utf8'));
5✔
90
}
91

92
export type CliOpts = ProxyRouterOpts & {
93
  tokens: string[];
94
  silent?: boolean;
95
  statusMonitor?: boolean;
96
  auth?: {
97
    username: string;
98
    password: string;
99
  };
100
};
101

102
export function createProxyServer(options: CliOpts): Express {
103
  const tokens = compact(options.tokens).reduce(
16✔
104
    (memo: string[], token: string) => concatTokens(token, memo),
16✔
105
    []
106
  );
107

108
  const app = express();
16✔
109

110
  app.disable('x-powered-by');
16✔
111

112
  app.use(
16✔
113
    compression({
114
      filter: (req, res) =>
115
        req.headers['x-no-compression'] ? false : compression.filter(req, res),
23!
116
      level: 6
117
    })
118
  );
119

120
  if (options.auth) {
16✔
121
    app.use((req: Request, res: Response, next) => {
7✔
122
      if (req.path.startsWith('/status')) return next();
9✔
123

124
      const credentials = basicAuth(req);
7✔
125

126
      if (
7✔
127
        !credentials ||
14✔
128
        credentials.name !== options.auth?.username ||
129
        credentials.pass !== options.auth?.password
130
      ) {
131
        res.set('WWW-Authenticate', 'Basic realm="GitHub Proxy Server"');
5✔
132
        return res.status(401).send({ message: 'Unauthorized' });
5✔
133
      }
134

135
      next();
2✔
136
    });
137
  }
138

139
  if (process.env.DEBUG === 'true') {
15!
140
    app.use(
×
141
      pinoHttp({
142
        level: 'info',
143
        serializers: {
144
          req: (req) => ({ method: req.method, url: req.url }),
×
145
          res: ({ statusCode }) => ({ statusCode })
×
146
        },
147
        logger: pino(pinoPretty({ colorize: true }))
148
      }) as never
149
    );
150
  }
151

152
  if (options.statusMonitor) {
15✔
153
    app.use(
1✔
154
      swaggerStats.getMiddleware({
155
        name: 'GitHub Proxy Server',
156
        version: process.env.npm_package_version,
157
        uriPath: '/status'
158
      })
159
    );
160
  }
161

162
  const proxy = new ProxyRouter(tokens, {
15✔
163
    overrideAuthorization: options.overrideAuthorization ?? true,
29✔
164
    ...options
165
  });
166

167
  proxy.on('error', (message) => app.emit('error', message));
16✔
168

169
  if (!options.silent) {
16✔
170
    proxy.on('log', (data) => app.emit('log', logTransform(data)));
6✔
171
    proxy.on('warn', (message) => app.emit('warn', message));
1✔
172
  }
173

174
  function notSupported(req: Request, res: Response) {
175
    res.status(ProxyRouterResponse.PROXY_ERROR).send({ message: 'Endpoint not supported' });
4✔
176
  }
177

178
  app
15✔
179
    .post('/graphql', (req: Request, reply: Response) => proxy.schedule(req, reply))
2✔
180
    .get('{/*path}', (req: Request, reply: Response) => proxy.schedule(req, reply));
10✔
181

182
  app.delete('{/*path}', notSupported);
15✔
183
  app.patch('{/*path}', notSupported);
15✔
184
  app.put('{/*path}', notSupported);
15✔
185
  app.post('{/*path}', notSupported);
15✔
186

187
  return app;
15✔
188
}
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