@magland/mochi 0.3.1

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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
package/dist/server.js ADDED
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createApp = createApp;
40
+ const compression_1 = __importDefault(require("compression"));
41
+ const express_1 = __importDefault(require("express"));
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const api_1 = require("./api");
45
+ const browse_1 = require("./browse");
46
+ const api_2 = require("./ci/api");
47
+ const engine_1 = require("./ci/engine");
48
+ const wake_1 = require("./ci/wake");
49
+ const web_1 = require("./ci/web");
50
+ const config_1 = require("./config");
51
+ const egress_1 = require("./egress");
52
+ const limit_1 = require("./limit");
53
+ const compare_1 = require("./compare");
54
+ const find_1 = require("./find");
55
+ const issueweb_1 = require("./issueweb");
56
+ const githttp_1 = require("./githttp");
57
+ const lfs_1 = require("./lfs");
58
+ const pullweb_1 = require("./pullweb");
59
+ const releases_1 = require("./releases");
60
+ const lfsstore_1 = require("./lfsstore");
61
+ const layout_1 = require("./layout");
62
+ const logo_1 = require("./logo");
63
+ const migrate_1 = require("./migrate");
64
+ const perms_1 = require("./perms");
65
+ const redirects_1 = require("./redirects");
66
+ const scan_1 = require("./scan");
67
+ const session_1 = require("./session");
68
+ const site_1 = require("./site");
69
+ const siteshost_1 = require("./siteshost");
70
+ const assets_1 = require("./assets");
71
+ const pagescript_1 = require("./pagescript");
72
+ const themes_1 = require("./themes");
73
+ const views = __importStar(require("./views"));
74
+ const webops_1 = require("./webops");
75
+ // Exempt from the coarse ceiling, checked before anything is charged.
76
+ //
77
+ // /api/runner/* because the runner's long poll and its log posts are
78
+ // legitimately high-volume from one address, and a runner sharing an address
79
+ // with a person must not throttle either. The assets because they are served
80
+ // from memory or a package directory and are cheaper to answer than to count.
81
+ //
82
+ // The asset prefixes are exempt on the forge's own hostname only: on a sites
83
+ // hostname those paths belong to the site being served, and are ordinary traffic.
84
+ // Responses that must reach the client exactly as written, so compression is
85
+ // not offered on them.
86
+ //
87
+ // The git wire protocol and LFS carry their own compression -- a packfile is
88
+ // already deflated -- and both stream, so wrapping them costs CPU to make the
89
+ // body no smaller and delays the first bytes of a clone. The backup stream is
90
+ // the same bargain over a much bigger body.
91
+ //
92
+ // Everything else is HTML, CSS, JSON or plain text, where the saving is large:
93
+ // a repository page goes from 133 KB to 11 KB.
94
+ const UNCOMPRESSED = /^\/[^/]+\/[^/]+\/(info\/refs|git-upload-pack|git-receive-pack|info\/lfs\/)|^\/api\/backup\//;
95
+ function isCompressible(req, res) {
96
+ if (UNCOMPRESSED.test(req.path))
97
+ return false;
98
+ return compression_1.default.filter(req, res);
99
+ }
100
+ /**
101
+ * What a forge page may load, and from where.
102
+ *
103
+ * The interface serves untrusted content from its own origin -- a repository's
104
+ * files, an issue somebody wrote, a rendered README -- so escaping and
105
+ * sanitizing are not the only line worth having. This one is the browser's:
106
+ * even a page that somehow carried injected markup could not run it.
107
+ *
108
+ * script-src 'self' is the directive that does the work, and it is why the
109
+ * pages carry no inline script at all: the one script they load is
110
+ * /assets/page.js, and everything page-specific reaches it through a data
111
+ * attribute (see src/pagescript.ts). A policy that allowed inline script would
112
+ * allow an injected one too, since the browser cannot tell them apart.
113
+ *
114
+ * Two directives are looser than the rest, both deliberately:
115
+ *
116
+ * style-src allows inline style, because the interface paints with values it
117
+ * computes -- a language bar's widths, a theme swatch's palette, an egress
118
+ * meter's fill. An inline style cannot execute anything, so this buys much
119
+ * less than script-src and costs nothing to keep.
120
+ *
121
+ * img-src allows any host, because markdown may already reference an
122
+ * external image and does today; narrowing it would silently stop READMEs
123
+ * rendering and needs an image proxy first, which is what GitHub built for
124
+ * the same reason.
125
+ *
126
+ * frame-ancestors keeps the forge's own pages out of somebody else's frame: a
127
+ * CSRF token and the Origin check in src/session.ts refuse a request forged
128
+ * from elsewhere, but neither refuses a real click on a real page inside a
129
+ * frame, which is what would let a merge, a close, or a branch deletion be
130
+ * obtained from a signed-in reader who thought they were clicking something of
131
+ * the framing page's. base-uri and form-action close the two ways injected
132
+ * markup could redirect a relative URL or a form post without running script.
133
+ */
134
+ const FORGE_CSP = [
135
+ "default-src 'self'",
136
+ "script-src 'self'",
137
+ "style-src 'self' 'unsafe-inline'",
138
+ 'img-src * data:',
139
+ "font-src 'self'",
140
+ "connect-src 'self'",
141
+ "object-src 'none'",
142
+ "base-uri 'none'",
143
+ "form-action 'self'",
144
+ "frame-ancestors 'self'",
145
+ ].join('; ');
146
+ function isRateExempt(root, req) {
147
+ if (req.path.startsWith('/api/runner/'))
148
+ return true;
149
+ if ((0, siteshost_1.isUnderSitesHost)((0, config_1.loadConfig)(root).sites.host, req.hostname))
150
+ return false;
151
+ return req.path.startsWith('/assets/') || req.path === '/favicon.svg' || req.path === '/favicon.ico';
152
+ }
153
+ function createApp(root) {
154
+ // Before anything is served: a vault written under the older layout, with
155
+ // its collections directly in the root, is moved to the current one. Renames
156
+ // only, and a no-op on a vault already laid out this way. A vault that could
157
+ // not be migrated is not served, since serving it would show an empty vault
158
+ // to someone whose repositories are all still there.
159
+ const migration = (0, migrate_1.migrateLayout)(root);
160
+ if (migration) {
161
+ console.log(`Migrated ${migration.collections.length} collection(s) to ${layout_1.COLLECTIONS_DIR}/<collection>/${layout_1.REPOS_DIR}/: ${migration.collections.join(', ')}`);
162
+ }
163
+ // Likewise a vault.json from before roles: its glob scopes become owners,
164
+ // collaborators, and the site-admin bit, and each rounding is said aloud.
165
+ const permNotes = (0, migrate_1.migratePermissions)(root);
166
+ if (permNotes) {
167
+ for (const note of permNotes)
168
+ console.log(`permissions: ${note}`);
169
+ }
170
+ const app = (0, express_1.default)();
171
+ app.disable('x-powered-by');
172
+ const config = (0, config_1.loadConfig)(root);
173
+ // Behind a TLS proxy (Caddy, Fly, etc.) X-Forwarded-Proto/Host must win, or
174
+ // clone URLs and Secure cookies would use the internal address. On a vault
175
+ // exposed directly they must not: req.ip would then be read from a
176
+ // client-supplied X-Forwarded-For, which defeats every per-address limit and
177
+ // makes the limiter's own key space unbounded.
178
+ //
179
+ // Read once at startup rather than per request, unlike the theme and the CI
180
+ // settings. Express resolves 'trust proxy' when the Request prototype is
181
+ // built, so it is not a per-request decision to make, and changing whether the
182
+ // internet is trusted is a restart-worthy event in a way that changing a
183
+ // colour scheme is not. The same goes for the limits below, which hold live
184
+ // counts that cannot be rebuilt per request without discarding them.
185
+ app.set('trust proxy', config.network.trustProxy);
186
+ // Outgoing bytes, counted per repository and capped per day. The cap is read
187
+ // through loadConfig on every call rather than captured here, unlike the rest
188
+ // of the limits block: it is the one limit whose value an operator needs to
189
+ // change while the vault is refusing to answer.
190
+ const egress = (0, egress_1.createEgress)(root, () => (0, config_1.loadConfig)(root).limits.egressGbPerDay);
191
+ // First of all, because it has to attach its listeners before anything can
192
+ // end the response. What it counts is what reaches the socket, so it is
193
+ // indifferent to what wraps the body after this point: whatever answers the
194
+ // request, and in whatever encoding, those bytes are charged to somebody.
195
+ app.use((req, res, next) => {
196
+ egress.watch(req, res);
197
+ next();
198
+ });
199
+ // Ahead of every route and every other middleware, because it works by
200
+ // wrapping the response: anything mounted earlier would answer uncompressed.
201
+ // That includes the sites served from this process and the error pages.
202
+ app.use((0, compression_1.default)({ filter: isCompressible }));
203
+ const gates = (0, limit_1.createGates)(config.limits);
204
+ const authLimiter = (0, limit_1.createAuthLimiter)(config.limits.authFailures);
205
+ const requestLimiter = (0, limit_1.createLimiter)({
206
+ limit: config.limits.requestsPerMinute,
207
+ windowMs: 60000,
208
+ maxKeys: 20000,
209
+ });
210
+ // The theme is vault state, so it is re-read (stat-cached) per request:
211
+ // hand-editing config.json takes effect without a restart.
212
+ app.use((_req, _res, next) => {
213
+ (0, themes_1.setActiveTheme)((0, config_1.loadConfig)(root).theme);
214
+ next();
215
+ });
216
+ // The bluntest of the three limits: a ceiling on ordinary traffic, so that one
217
+ // misbehaving crawler cannot saturate the process with cheap page renders. It
218
+ // is high on purpose. One page load of a static site can be dozens of requests,
219
+ // and a limit that makes a site feel broken will be turned off and take the
220
+ // useful limits with it.
221
+ //
222
+ // Registered before the sites middleware, so that traffic to a site's own
223
+ // hostname is counted too, which is exactly where a burst of requests per page
224
+ // comes from.
225
+ app.use((req, res, next) => {
226
+ if (isRateExempt(root, req))
227
+ return next();
228
+ const decision = requestLimiter.hit((0, limit_1.clientKey)(req));
229
+ if (decision.ok)
230
+ return next();
231
+ res.status(429).setHeader('Retry-After', String(decision.retryAfter));
232
+ res
233
+ .type('html')
234
+ .send(views.errorPage(429, 'Too many requests from this address. Try again in a moment.', { viewer: null }));
235
+ });
236
+ // The daily egress budget, checked before any route can produce a body.
237
+ //
238
+ // Registered after the counter and before the sites middleware, for the same
239
+ // reason the request limiter is: a site's own hostname is where the bytes
240
+ // mostly go, so it has to be inside the budget rather than in front of it.
241
+ //
242
+ // The refusal is HTML for every client, as the 429 above is. A git client
243
+ // shows the body it did not expect, which reads worse than a plain line but
244
+ // says the same thing, and one shape of refusal is one place to change it.
245
+ app.use((req, res, next) => {
246
+ const decision = egress.allow(req);
247
+ if (!decision)
248
+ return next();
249
+ res.status(503).setHeader('Retry-After', String(decision.retryAfter));
250
+ res.type('html').send(views.errorPage(503, (0, egress_1.egressMessage)(decision), { viewer: null }));
251
+ });
252
+ // Sites served from their own hostname are answered before every other route,
253
+ // the asset routes included: otherwise /assets/style.css would give a site the
254
+ // forge's stylesheet instead of its own, and /favicon.svg the forge's icon.
255
+ // Nothing here runs unless a sites host is configured.
256
+ (0, site_1.registerSiteHost)(app, root);
257
+ // The forge's own pages may only be framed by the forge. A CSRF token and the
258
+ // Origin check in src/session.ts refuse a request forged from somewhere else,
259
+ // but neither refuses a real click on a real page inside somebody else's
260
+ // frame, which is what would let a merge, a close, or a branch deletion be
261
+ // obtained from a signed-in reader who thought they were clicking something
262
+ // of the framing page's. Registered after the sites middleware, so a request
263
+ // on a sites hostname never reaches it.
264
+ //
265
+ // A Content-Security-Policy and not X-Frame-Options: site content served on
266
+ // the forge host replaces this header with its own sandbox policy (see
267
+ // setSiteHeaders in src/site.ts), and an X-Frame-Options it could not
268
+ // replace would stop a published site being embedded anywhere, which is
269
+ // ordinary for a static site and no business of the forge's.
270
+ app.use((_req, res, next) => {
271
+ res.setHeader('Content-Security-Policy', FORGE_CSP);
272
+ next();
273
+ });
274
+ // Then, before any route that resolves a name: an address a rename left
275
+ // behind is sent to where that name went. Only ever a name nothing answers
276
+ // to, so nothing that exists is affected; see src/redirects.ts. After the
277
+ // sites middleware, because a path on a sites hostname is a path inside a
278
+ // site and means nothing to the forge's own routes.
279
+ (0, redirects_1.registerRedirects)(app, root);
280
+ // ---- static assets ----
281
+ const hlCache = new Map();
282
+ app.get('/assets/style.css', (req, res) => {
283
+ const sheet = (0, assets_1.styleSheet)((0, themes_1.activeTheme)());
284
+ // A request that names the body it wants may keep it forever, because a
285
+ // different body would be a different tag and so a different URL. One that
286
+ // does not -- an old page still in a tab, or someone typing the path --
287
+ // gets the current sheet and no licence to hold on to it.
288
+ const fresh = String(req.query.v ?? '') === sheet.tag;
289
+ res
290
+ .type('text/css')
291
+ .set('Cache-Control', fresh ? 'public, max-age=31536000, immutable' : 'no-cache')
292
+ .send(sheet.body);
293
+ });
294
+ // The page script, on the same terms as the stylesheet above: a request that
295
+ // names the body it wants may keep it forever, since a different body would
296
+ // be a different tag and so a different URL.
297
+ app.get('/assets/page.js', (req, res) => {
298
+ const script = (0, pagescript_1.pageScript)();
299
+ const fresh = String(req.query.v ?? '') === script.tag;
300
+ res
301
+ .type('text/javascript')
302
+ .set('Cache-Control', fresh ? 'public, max-age=31536000, immutable' : 'no-cache')
303
+ .set('X-Content-Type-Options', 'nosniff')
304
+ .send(script.body);
305
+ });
306
+ app.get('/assets/hl.css', (req, res) => {
307
+ // Code colours are a whole stylesheet rather than a set of tokens, so the
308
+ // reader's theme picks a file instead of an attribute. The name still
309
+ // comes from the theme table and never from the request: ?t= selects a
310
+ // theme by name, and an unknown one falls back to the vault's.
311
+ const name = ((0, themes_1.findTheme)(String(req.query.t ?? '')) ?? (0, themes_1.activeTheme)()).hljs;
312
+ let css = hlCache.get(name);
313
+ if (css === undefined) {
314
+ try {
315
+ css = fs.readFileSync(require.resolve(`highlight.js/styles/${name}.css`), 'utf8');
316
+ }
317
+ catch {
318
+ css = '';
319
+ }
320
+ hlCache.set(name, css);
321
+ }
322
+ res.type('text/css').set('Cache-Control', 'public, max-age=86400').send(css);
323
+ });
324
+ // Every repository the interface would show this viewer anyway, as a list
325
+ // of names, for the jump box to search without a round trip per keystroke.
326
+ // It says no more than the front page already does to the same eyes: a
327
+ // private repository is listed only for a viewer with a role there, which
328
+ // is also why the answer is marked private to shared caches. It is not
329
+ // /api/repos, which is the authenticated interface for programs and carries
330
+ // much more per repository than a name.
331
+ app.get('/assets/repos.json', (req, res) => {
332
+ const viewer = (0, session_1.getViewer)(req, root);
333
+ const repos = [];
334
+ for (const c of (0, scan_1.listCollections)(root)) {
335
+ for (const d of (0, scan_1.listRepoDirs)(root, c.name)) {
336
+ const name = (0, scan_1.displayName)(d);
337
+ if ((0, perms_1.repoRole)(root, viewer?.auth ?? null, { collection: c.name, name, dir: (0, layout_1.repoPath)(root, c.name, d) }) === null) {
338
+ continue;
339
+ }
340
+ repos.push(`${c.name}/${name}`);
341
+ }
342
+ }
343
+ res.set('Cache-Control', 'private, no-cache').json(repos);
344
+ });
345
+ // KaTeX ships the stylesheet and fonts its output needs; serving them from
346
+ // the installed package keeps rendered math working with no external
347
+ // requests, which matters for vaults on closed networks.
348
+ const katexDir = path.dirname(require.resolve('katex/dist/katex.min.css'));
349
+ let katexCss = null;
350
+ app.get('/assets/katex/katex.css', (_req, res) => {
351
+ if (katexCss === null)
352
+ katexCss = fs.readFileSync(path.join(katexDir, 'katex.min.css'), 'utf8');
353
+ res.type('text/css').set('Cache-Control', 'public, max-age=86400').send(katexCss);
354
+ });
355
+ app.get('/assets/katex/fonts/:file', (req, res) => {
356
+ // The request never reaches the filesystem unless it names a KaTeX font.
357
+ if (!/^KaTeX_[A-Za-z0-9]+-[A-Za-z]+\.(woff2|woff|ttf)$/.test(req.params.file)) {
358
+ res.status(404).end();
359
+ return;
360
+ }
361
+ res.set('Cache-Control', 'public, max-age=31536000, immutable');
362
+ res.sendFile(path.join(katexDir, 'fonts', req.params.file));
363
+ });
364
+ // The favicon is the logo mark on a tile coloured from the active theme, so
365
+ // it changes with the vault's appearance. Browsers that will not take an SVG
366
+ // icon fall back to /favicon.ico, which stays empty.
367
+ app.get('/favicon.svg', (_req, res) => {
368
+ res.type('image/svg+xml').set('Cache-Control', 'public, max-age=86400').send((0, logo_1.faviconSvg)());
369
+ });
370
+ app.get('/favicon.ico', (_req, res) => {
371
+ res.status(204).end();
372
+ });
373
+ // The LFS store is built from the environment once at startup; a partial
374
+ // bucket configuration throws here, which the CLI turns into a non-zero
375
+ // exit naming the missing variables.
376
+ const lfs = (0, lfsstore_1.createLfsStore)(root);
377
+ console.log(`Git LFS storage backend: ${lfs.label}`);
378
+ // The CI engine plans and schedules workflow runs; jobs execute on runners
379
+ // elsewhere (mochi runner run), never in this process.
380
+ const engine = new engine_1.CiEngine(root, () => (0, config_1.loadConfig)(root).ci);
381
+ // A runner that stops when it has nothing to do cannot be told that it has
382
+ // something to do, so the vault starts it: this watches for a queued job
383
+ // whose runner is not connected and pokes that runner's wake address.
384
+ // Runners without one are unaffected, which is all of them by default.
385
+ (0, wake_1.startWakeDispatcher)(root, engine);
386
+ // Registration order matters: the API and the UI-owned top-level paths
387
+ // (/login, /new, /admin, ...) come before the generic /:collection and /:collection/:repo
388
+ // browse routes, and more-specific wildcard routes before their prefixes.
389
+ // LFS registers before git HTTP so its /info/lfs/* routes are matched ahead
390
+ // of any /info/refs handling.
391
+ (0, api_1.registerApi)(app, root, authLimiter, gates, lfs, engine, egress);
392
+ (0, api_2.registerCiApi)(app, root, engine, authLimiter);
393
+ (0, lfs_1.registerLfs)(app, root, lfs, authLimiter);
394
+ (0, githttp_1.registerGitHttp)(app, root, gates, authLimiter, engine);
395
+ (0, web_1.registerCiWeb)(app, root, engine);
396
+ (0, webops_1.registerWebOps)(app, root, authLimiter, lfs, engine, egress);
397
+ (0, compare_1.registerCompare)(app, root);
398
+ (0, issueweb_1.registerIssues)(app, root);
399
+ (0, pullweb_1.registerPulls)(app, root, engine);
400
+ (0, releases_1.registerReleases)(app, root);
401
+ (0, find_1.registerFind)(app, root, gates);
402
+ (0, find_1.registerSearch)(app, root, gates);
403
+ (0, browse_1.registerBrowse)(app, root, gates, lfs);
404
+ app.use((req, res) => {
405
+ res.status(404).type('html').send(views.errorPage(404, 'Page not found', { viewer: (0, session_1.getViewer)(req, root) }));
406
+ });
407
+ app.use((err, req, res, _next) => {
408
+ console.error(err);
409
+ if (!res.headersSent) {
410
+ let viewer = null;
411
+ try {
412
+ viewer = (0, session_1.getViewer)(req, root);
413
+ }
414
+ catch {
415
+ viewer = null;
416
+ }
417
+ res.status(500).type('html').send(views.errorPage(500, 'Internal server error', { viewer }));
418
+ }
419
+ else {
420
+ res.end();
421
+ }
422
+ });
423
+ return app;
424
+ }