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

gittrends-app / github-proxy-server / 21078838775

16 Jan 2026 07:47PM UTC coverage: 78.144% (-2.4%) from 80.499%
21078838775

push

github

hsborges
v12.0.0

133 of 184 branches covered (72.28%)

Branch coverage included in aggregate %.

246 of 301 relevant lines covered (81.73%)

93.81 hits per line

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

90.91
/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.running}:${chunk.pending}`,
44
    remaining: chunk.remaining,
45
    reset: dayjs.unix(chunk.reset).fromNow(),
46
    budget: chunk.timeBudget !== undefined ? `${(chunk.timeBudget / 1000).toFixed(1)}s` : '-',
6!
47
    duration: `${chunk.duration / 1000}s`,
48
    status: statusFormatter(chunk.status || '-')
10✔
49
  };
50

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

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

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

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

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

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

110
  const app = express();
16✔
111

112
  app.disable('x-powered-by');
16✔
113

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

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

126
      const credentials = basicAuth(req);
7✔
127

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

137
      next();
2✔
138
    });
139
  }
140

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

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

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

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

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

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

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

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

189
  return app;
15✔
190
}
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