@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.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- 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
|
+
}
|