@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/compare.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCompare = registerCompare;
|
|
4
|
+
const diff_1 = require("./diff");
|
|
5
|
+
const git_1 = require("./git");
|
|
6
|
+
const html_1 = require("./html");
|
|
7
|
+
const icons_1 = require("./icons");
|
|
8
|
+
const render_1 = require("./render");
|
|
9
|
+
const session_1 = require("./session");
|
|
10
|
+
const views_1 = require("./views");
|
|
11
|
+
const web_1 = require("./web");
|
|
12
|
+
// Comparing two revisions: what one branch has that another does not, and the
|
|
13
|
+
// patch between them. Without it, seeing how a branch differs from main means
|
|
14
|
+
// cloning the repository, which is a strange thing for a web interface to
|
|
15
|
+
// require of a reader.
|
|
16
|
+
//
|
|
17
|
+
// The address is GitHub's: /compare/<base>...<head>, three dots for the
|
|
18
|
+
// comparison taken from the merge base. The form posts to /compare with the
|
|
19
|
+
// two refs as query parameters and the handler accepts either shape, since a
|
|
20
|
+
// GET form cannot build a path.
|
|
21
|
+
/** The most commits a comparison lists before saying there are more. */
|
|
22
|
+
const MAX_COMMITS = 250;
|
|
23
|
+
function refOptions(refs, selected) {
|
|
24
|
+
return (0, html_1.joinHtml)(refs.map((r) => (0, html_1.html) `<option value="${r.name}"${r.name === selected ? (0, html_1.raw)(' selected') : ''}>${r.name}</option>`));
|
|
25
|
+
}
|
|
26
|
+
function picker(ctx, name, label, selected) {
|
|
27
|
+
const groups = [
|
|
28
|
+
ctx.branches.length ? (0, html_1.html) `<optgroup label="Branches">${refOptions(ctx.branches, selected)}</optgroup>` : '',
|
|
29
|
+
ctx.tags.length ? (0, html_1.html) `<optgroup label="Tags">${refOptions(ctx.tags, selected)}</optgroup>` : '',
|
|
30
|
+
];
|
|
31
|
+
const known = [...ctx.branches, ...ctx.tags].some((r) => r.name === selected);
|
|
32
|
+
const loose = known ? '' : (0, html_1.html) `<option value="${selected}" selected>${selected}</option>`;
|
|
33
|
+
return (0, html_1.html) `<label class="cmp-picker">${label}
|
|
34
|
+
<select name="${name}">${loose}${groups}</select></label>`;
|
|
35
|
+
}
|
|
36
|
+
function commitRows(ctx, commits) {
|
|
37
|
+
const base = (0, views_1.repoUrl)(ctx);
|
|
38
|
+
return (0, html_1.joinHtml)(commits.map((c) => (0, html_1.html) `<div class="commit-row"><span class="commit-main"><a class="title" href="${base}/commit/${c.sha}">${c.subject}</a><div class="muted small">${c.author} committed ${(0, render_1.timeTag)(c.date)}</div></span><span class="commit-actions"><a class="sha" href="${base}/commit/${c.sha}">${c.sha.slice(0, 7)}</a></span></div>`));
|
|
39
|
+
}
|
|
40
|
+
function comparePage(ctx, baseRef, headRef, result) {
|
|
41
|
+
const base = (0, views_1.repoUrl)(ctx);
|
|
42
|
+
const url = `${base}/compare/${(0, views_1.encPath)(baseRef)}...${(0, views_1.encPath)(headRef)}`;
|
|
43
|
+
const form = (0, html_1.html) `<form class="cmp-form" method="get" action="${base}/compare">
|
|
44
|
+
${(0, icons_1.icon)('git-branch')}${picker(ctx, 'base', 'base', baseRef)}<span class="muted">…</span>${picker(ctx, 'head', 'compare', headRef)}
|
|
45
|
+
<a class="btn" href="${base}/compare/${(0, views_1.encPath)(headRef)}...${(0, views_1.encPath)(baseRef)}" title="Swap the two revisions">${(0, icons_1.icon)('sync')}<span>Swap</span></a>
|
|
46
|
+
<button class="btn btn-primary" type="submit">Compare</button>
|
|
47
|
+
</form>`;
|
|
48
|
+
let body;
|
|
49
|
+
if (!result) {
|
|
50
|
+
body = (0, html_1.html) `<div class="empty-state">Choose two revisions to compare.</div>`;
|
|
51
|
+
}
|
|
52
|
+
else if (result.commits.length === 0 && result.patch === '') {
|
|
53
|
+
body = (0, html_1.html) `<div class="empty-state">These two revisions are identical.</div>`;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const behind = result.behind > 0
|
|
57
|
+
? (0, html_1.html) ` <span class="muted">and is ${result.behind} commit${result.behind === 1 ? '' : 's'} behind</span>`
|
|
58
|
+
: '';
|
|
59
|
+
const more = result.ahead > result.commits.length
|
|
60
|
+
? (0, html_1.html) `<p class="muted small">Showing the newest ${result.commits.length} of ${result.ahead} commits.</p>`
|
|
61
|
+
: '';
|
|
62
|
+
// A comparison with something on it is a pull request waiting to be
|
|
63
|
+
// written, which is the path GitHub puts here too.
|
|
64
|
+
const propose = result.ahead > 0 && ctx.branches.some((b) => b.name === headRef) && ctx.branches.some((b) => b.name === baseRef)
|
|
65
|
+
? (0, html_1.html) `<a class="btn btn-primary" href="${base}/pulls/new?base=${encodeURIComponent(baseRef)}&head=${encodeURIComponent(headRef)}">${(0, icons_1.icon)('git-pull-request')}<span>Create pull request</span></a>`
|
|
66
|
+
: '';
|
|
67
|
+
body = (0, html_1.html) `<div class="cmp-status">
|
|
68
|
+
<span><b>${headRef}</b> is <b>${result.ahead} commit${result.ahead === 1 ? '' : 's'}</b> ahead of <b>${baseRef}</b>${behind}.</span>
|
|
69
|
+
${propose}
|
|
70
|
+
</div>
|
|
71
|
+
${more}
|
|
72
|
+
<div class="cmp-commits">${commitRows(ctx, result.commits)}</div>
|
|
73
|
+
<div class="cmp-diff">${(0, html_1.raw)((0, diff_1.renderDiff)(result.patch, { blobBase: `${base}/blob/${(0, views_1.encPath)(headRef)}` }))}</div>`;
|
|
74
|
+
}
|
|
75
|
+
const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'branches')}
|
|
76
|
+
<div class="page-head"><h2>Comparing changes</h2></div>
|
|
77
|
+
${form}
|
|
78
|
+
${body}`;
|
|
79
|
+
return (0, views_1.layout)(`Comparing ${baseRef}...${headRef} - ${ctx.collection}/${ctx.repo}`, content, (0, views_1.repoOpts)(ctx, url));
|
|
80
|
+
}
|
|
81
|
+
function registerCompare(app, root) {
|
|
82
|
+
const handler = (0, web_1.ah)(async (req, res) => {
|
|
83
|
+
const viewer = (0, session_1.getViewer)(req, root);
|
|
84
|
+
const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
|
|
85
|
+
if (!loaded)
|
|
86
|
+
return;
|
|
87
|
+
const fallback = loaded.defaultBranch ?? '';
|
|
88
|
+
// Either shape: /compare/<base>...<head> from a link, or ?base=&head=
|
|
89
|
+
// from the form. Two dots are accepted as well, since people type them.
|
|
90
|
+
const spec = (0, web_1.wildcard)(req);
|
|
91
|
+
const split = spec.includes('...') ? spec.split('...') : spec.includes('..') ? spec.split('..') : null;
|
|
92
|
+
const baseRef = (split ? split[0] : String(req.query.base ?? '')) || fallback;
|
|
93
|
+
const headRef = (split ? split[1] : String(req.query.head ?? '')) || fallback;
|
|
94
|
+
const ctx = await (0, web_1.makeCtx)(root, req, loaded, headRef || fallback, viewer);
|
|
95
|
+
// Only revisions this repository has: a comparison takes two arguments
|
|
96
|
+
// from a URL and hands both to git.
|
|
97
|
+
const known = (r) => loaded.refNames.includes(r) || /^[0-9a-f]{7,40}$/.test(r);
|
|
98
|
+
if (baseRef === '' || headRef === '') {
|
|
99
|
+
res.type('html').send(comparePage(ctx, baseRef, headRef, null));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!(0, git_1.isValidRefName)(baseRef) || !(0, git_1.isValidRefName)(headRef) || !known(baseRef) || !known(headRef)) {
|
|
103
|
+
(0, web_1.send404)(res, `No revision ${known(baseRef) ? headRef : baseRef} here`, viewer);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const result = await loaded.repo.compare(baseRef, headRef, MAX_COMMITS);
|
|
107
|
+
res.type('html').send(comparePage(ctx, baseRef, headRef, result));
|
|
108
|
+
});
|
|
109
|
+
app.get('/:collection/:repo/compare/*', handler);
|
|
110
|
+
app.get('/:collection/:repo/compare', handler);
|
|
111
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CONFIG_FILE = void 0;
|
|
37
|
+
exports.isPlausibleHostname = isPlausibleHostname;
|
|
38
|
+
exports.configFilePath = configFilePath;
|
|
39
|
+
exports.loadConfig = loadConfig;
|
|
40
|
+
exports.saveConfig = saveConfig;
|
|
41
|
+
exports.seedTrustProxy = seedTrustProxy;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const atomic_1 = require("./atomic");
|
|
45
|
+
const filecache_1 = require("./filecache");
|
|
46
|
+
const siteshost_1 = require("./siteshost");
|
|
47
|
+
const themes_1 = require("./themes");
|
|
48
|
+
// Vault-level settings, kept in <vault>/config.json next to vault.json. Like
|
|
49
|
+
// everything else, this is a plain file in the vault: hand-editing it is
|
|
50
|
+
// legitimate, and a missing or unreadable file simply means defaults.
|
|
51
|
+
exports.CONFIG_FILE = 'config.json';
|
|
52
|
+
// A hostname of at least two labels, each of letters, digits, and interior
|
|
53
|
+
// hyphens. A value carrying a scheme, a port, or a single label is not one, and
|
|
54
|
+
// is treated as a typo rather than obeyed.
|
|
55
|
+
const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
56
|
+
/**
|
|
57
|
+
* Whether a string is usable as a hostname in this file's settings. Exported so
|
|
58
|
+
* that a route accepting one refuses a typo with a message, rather than storing
|
|
59
|
+
* it and leaving loadConfig to fall back to the default silently: a setting
|
|
60
|
+
* written through the API is being watched by someone who wants to know.
|
|
61
|
+
*/
|
|
62
|
+
function isPlausibleHostname(value) {
|
|
63
|
+
return HOSTNAME_RE.test(value);
|
|
64
|
+
}
|
|
65
|
+
function configFilePath(root) {
|
|
66
|
+
return path.join(root, exports.CONFIG_FILE);
|
|
67
|
+
}
|
|
68
|
+
const DEFAULTS = {
|
|
69
|
+
theme: themes_1.DEFAULT_THEME,
|
|
70
|
+
ci: { runs: 100, days: 0, artifactMb: 500 },
|
|
71
|
+
sites: { host: '' },
|
|
72
|
+
network: { trustProxy: false },
|
|
73
|
+
// 20 GB a day is generous for a personal forge and cheap if it is ever all
|
|
74
|
+
// sent: it is the number that stops a crawler or a CI loop turning a host
|
|
75
|
+
// that bills for egress and does not cap it into a bill nobody chose.
|
|
76
|
+
limits: { requestsPerMinute: 600, authFailures: 10, clone: 4, push: 4, search: 2, tree: 4, egressGbPerDay: 20 },
|
|
77
|
+
};
|
|
78
|
+
function defaults() {
|
|
79
|
+
return {
|
|
80
|
+
...DEFAULTS,
|
|
81
|
+
ci: { ...DEFAULTS.ci },
|
|
82
|
+
sites: { ...DEFAULTS.sites },
|
|
83
|
+
network: { ...DEFAULTS.network },
|
|
84
|
+
limits: { ...DEFAULTS.limits },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const cache = (0, filecache_1.fileCache)({ read: readConfig, missing: () => defaults() });
|
|
88
|
+
function loadConfig(root) {
|
|
89
|
+
return cache.get(configFilePath(root));
|
|
90
|
+
}
|
|
91
|
+
function readConfig(file) {
|
|
92
|
+
let config = defaults();
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
95
|
+
// An unknown theme name falls back to the default rather than failing the
|
|
96
|
+
// request: a typo in config.json should not take the vault down.
|
|
97
|
+
if (typeof parsed.theme === 'string' && (0, themes_1.findTheme)(parsed.theme))
|
|
98
|
+
config.theme = parsed.theme;
|
|
99
|
+
// Run retention: a vault's run history is the one part of its state that
|
|
100
|
+
// grows without bound, so it is bounded by default and tunable here.
|
|
101
|
+
if (typeof parsed.ci === 'object' && parsed.ci !== null) {
|
|
102
|
+
const ci = parsed.ci;
|
|
103
|
+
const runs = typeof ci.runs === 'number' && ci.runs >= 0 ? Math.floor(ci.runs) : DEFAULTS.ci.runs;
|
|
104
|
+
const days = typeof ci.days === 'number' && ci.days >= 0 ? Math.floor(ci.days) : DEFAULTS.ci.days;
|
|
105
|
+
const artifactMb = typeof ci.artifactMb === 'number' && ci.artifactMb > 0 ? Math.floor(ci.artifactMb) : DEFAULTS.ci.artifactMb;
|
|
106
|
+
config.ci = { runs, days, artifactMb };
|
|
107
|
+
}
|
|
108
|
+
// A site host lets each repository's site have an origin of its own. A
|
|
109
|
+
// value that is not a plausible hostname is ignored the way an unknown
|
|
110
|
+
// theme name is: a typo here would otherwise stop sites being served at
|
|
111
|
+
// all, or worse, serve them from a name that is not the one the
|
|
112
|
+
// certificate covers.
|
|
113
|
+
if (typeof parsed.sites === 'object' && parsed.sites !== null) {
|
|
114
|
+
const sites = parsed.sites;
|
|
115
|
+
const raw = typeof sites.host === 'string' ? (0, siteshost_1.normalizeHostname)(sites.host) : '';
|
|
116
|
+
config.sites = { host: isPlausibleHostname(raw) ? raw : DEFAULTS.sites.host };
|
|
117
|
+
}
|
|
118
|
+
// Whether the forwarded headers may be believed. False by default, because
|
|
119
|
+
// believing them on a directly exposed vault lets any client claim any
|
|
120
|
+
// address, which defeats every per-address limit and makes the limiter's own
|
|
121
|
+
// key space unbounded.
|
|
122
|
+
if (typeof parsed.network === 'object' && parsed.network !== null) {
|
|
123
|
+
const network = parsed.network;
|
|
124
|
+
config.network = {
|
|
125
|
+
trustProxy: typeof network.trustProxy === 'boolean' ? network.trustProxy : DEFAULTS.network.trustProxy,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Each field falls back to its own default, so one bad value does not
|
|
129
|
+
// discard the block.
|
|
130
|
+
if (typeof parsed.limits === 'object' && parsed.limits !== null) {
|
|
131
|
+
const limits = parsed.limits;
|
|
132
|
+
// A limit may be 0, which disables it. A concurrency may not: a gate of
|
|
133
|
+
// zero slots would refuse everything forever, which is an outage rather
|
|
134
|
+
// than a setting, so it is treated as a bad value.
|
|
135
|
+
const limit = (v, fallback) => typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : fallback;
|
|
136
|
+
const slots = (v, fallback) => typeof v === 'number' && Number.isFinite(v) && v >= 1 ? Math.floor(v) : fallback;
|
|
137
|
+
// Not floored, unlike the two above: half a gigabyte is a reasonable
|
|
138
|
+
// budget to give a vault, and 0 disables the cap.
|
|
139
|
+
const gigabytes = (v, fallback) => typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : fallback;
|
|
140
|
+
config.limits = {
|
|
141
|
+
requestsPerMinute: limit(limits.requestsPerMinute, DEFAULTS.limits.requestsPerMinute),
|
|
142
|
+
authFailures: limit(limits.authFailures, DEFAULTS.limits.authFailures),
|
|
143
|
+
clone: slots(limits.clone, DEFAULTS.limits.clone),
|
|
144
|
+
push: slots(limits.push, DEFAULTS.limits.push),
|
|
145
|
+
search: slots(limits.search, DEFAULTS.limits.search),
|
|
146
|
+
tree: slots(limits.tree, DEFAULTS.limits.tree),
|
|
147
|
+
egressGbPerDay: gigabytes(limits.egressGbPerDay, DEFAULTS.limits.egressGbPerDay),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
config = defaults();
|
|
153
|
+
}
|
|
154
|
+
return config;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Hold config.json's lock across a read, an edit, and the write back.
|
|
158
|
+
*
|
|
159
|
+
* `mochi config set` and `PATCH /api/config` merge changes into whatever is
|
|
160
|
+
* on disk, so two of them overlapping keeps one change and drops the other. The
|
|
161
|
+
* cache is dropped on the way in for the same reason it is in the runner
|
|
162
|
+
* registry: an unchanged mtime and size is a good enough guess for a read, and
|
|
163
|
+
* not good enough for the read half of a read-modify-write.
|
|
164
|
+
*
|
|
165
|
+
* The lock is not reentrant, so anything that needs to look at the file before
|
|
166
|
+
* deciding whether to save calls `saveLocked` from inside its own `editConfig`
|
|
167
|
+
* rather than calling `saveConfig`.
|
|
168
|
+
*/
|
|
169
|
+
function editConfig(root, fn) {
|
|
170
|
+
return (0, atomic_1.withFileLock)(`${configFilePath(root)}.lock`, () => {
|
|
171
|
+
cache.invalidate(configFilePath(root));
|
|
172
|
+
return fn();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function saveLocked(root, changes) {
|
|
176
|
+
const next = { ...loadConfig(root), ...changes };
|
|
177
|
+
(0, atomic_1.writeFileAtomic)(configFilePath(root), JSON.stringify(next, null, 2) + '\n');
|
|
178
|
+
cache.invalidate(configFilePath(root));
|
|
179
|
+
return next;
|
|
180
|
+
}
|
|
181
|
+
function saveConfig(root, changes) {
|
|
182
|
+
return editConfig(root, () => saveLocked(root, changes));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Record network.trustProxy: true unless the vault has already said something
|
|
186
|
+
* about it. Called by `serve` when MOCHI_TRUST_PROXY is set, which is how
|
|
187
|
+
* `mochi deploy fly` tells a vault it is behind a proxy: the deploy cannot
|
|
188
|
+
* write to the volume itself, since the vault does not exist until the server
|
|
189
|
+
* starts.
|
|
190
|
+
*
|
|
191
|
+
* It runs on every start and not only on a fresh vault, because a vault created
|
|
192
|
+
* before this setting existed would otherwise come back from an upgrade with
|
|
193
|
+
* forwarded headers no longer believed, which would break its clone URLs and its
|
|
194
|
+
* Secure cookies. An explicit value in config.json is always left alone, so
|
|
195
|
+
* turning it off by hand stays turned off.
|
|
196
|
+
*/
|
|
197
|
+
function seedTrustProxy(root) {
|
|
198
|
+
return editConfig(root, () => {
|
|
199
|
+
let parsed = {};
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(fs.readFileSync(configFilePath(root), 'utf8'));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
parsed = {};
|
|
205
|
+
}
|
|
206
|
+
const network = typeof parsed.network === 'object' && parsed.network !== null ? parsed.network : {};
|
|
207
|
+
if (typeof network.trustProxy === 'boolean')
|
|
208
|
+
return false;
|
|
209
|
+
saveLocked(root, { network: { trustProxy: true } });
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CredentialError = void 0;
|
|
37
|
+
exports.credentialTarget = credentialTarget;
|
|
38
|
+
exports.configuredHelper = configuredHelper;
|
|
39
|
+
exports.setHelper = setHelper;
|
|
40
|
+
exports.approveCredential = approveCredential;
|
|
41
|
+
exports.readCredential = readCredential;
|
|
42
|
+
exports.rejectCredential = rejectCredential;
|
|
43
|
+
exports.loginPath = loginPath;
|
|
44
|
+
exports.loadLogin = loadLogin;
|
|
45
|
+
exports.saveLogin = saveLogin;
|
|
46
|
+
exports.clearLogin = clearLogin;
|
|
47
|
+
exports.vaultTarget = vaultTarget;
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
// The client side of authentication. A token is the password git sends over
|
|
53
|
+
// Basic auth, so the place to keep it is git's own credential store: clone,
|
|
54
|
+
// fetch, push, and git-lfs all read credentials through the same `git
|
|
55
|
+
// credential` plumbing, and storing it once there stops every one of them from
|
|
56
|
+
// asking again. Nothing here is vault state. The server never learns that a
|
|
57
|
+
// credential was stored, and `git credential reject` undoes it.
|
|
58
|
+
class CredentialError extends Error {
|
|
59
|
+
}
|
|
60
|
+
exports.CredentialError = CredentialError;
|
|
61
|
+
// Unlike execGit in git.ts, these commands are not about a repository, and a
|
|
62
|
+
// non-zero exit is frequently the answer rather than a failure: `git config
|
|
63
|
+
// --get-urlmatch` exits 1 when nothing matches, and `git credential fill`
|
|
64
|
+
// exits non-zero when it has nothing to give. So this reports the exit code
|
|
65
|
+
// instead of throwing on it.
|
|
66
|
+
function git(args, opts = {}) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const child = (0, child_process_1.execFile)('git', args, { env: opts.env ?? process.env, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
69
|
+
const code = err?.code;
|
|
70
|
+
if (code === 'ENOENT') {
|
|
71
|
+
reject(new CredentialError('git is not on PATH, so there is nowhere to store a credential'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve({ code: typeof code === 'number' ? code : err ? 1 : 0, stdout: String(stdout), stderr: String(stderr) });
|
|
75
|
+
});
|
|
76
|
+
if (child.stdin) {
|
|
77
|
+
if (opts.input !== undefined)
|
|
78
|
+
child.stdin.write(opts.input);
|
|
79
|
+
child.stdin.end();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// git keys credentials by protocol and host, the port included. The path plays
|
|
84
|
+
// no part unless credential.useHttpPath is set, so a vault served under a
|
|
85
|
+
// subpath is still one credential for the whole vault.
|
|
86
|
+
function credentialTarget(hostUrl) {
|
|
87
|
+
let u;
|
|
88
|
+
try {
|
|
89
|
+
u = new URL(hostUrl);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new CredentialError(`Not a URL: ${hostUrl}`);
|
|
93
|
+
}
|
|
94
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
95
|
+
throw new CredentialError(`${hostUrl} is not an http or https URL`);
|
|
96
|
+
}
|
|
97
|
+
const protocol = u.protocol.slice(0, -1);
|
|
98
|
+
return { protocol, host: u.host, url: `${protocol}://${u.host}` };
|
|
99
|
+
}
|
|
100
|
+
// The credential description git reads on stdin: key=value lines, then a blank
|
|
101
|
+
// line. A newline inside a value would let it forge further fields, so values
|
|
102
|
+
// carrying one are refused rather than written.
|
|
103
|
+
function describe(target, fields) {
|
|
104
|
+
const lines = [`protocol=${target.protocol}`, `host=${target.host}`];
|
|
105
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
106
|
+
if (/[\n\r\0]/.test(value))
|
|
107
|
+
throw new CredentialError(`${key} contains a line break`);
|
|
108
|
+
lines.push(`${key}=${value}`);
|
|
109
|
+
}
|
|
110
|
+
return lines.join('\n') + '\n\n';
|
|
111
|
+
}
|
|
112
|
+
function parseCredential(stdout) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const line of stdout.split('\n')) {
|
|
115
|
+
const i = line.indexOf('=');
|
|
116
|
+
if (i > 0)
|
|
117
|
+
out[line.slice(0, i)] = line.slice(i + 1);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
// What git would actually use for this host, taking both credential.helper and
|
|
122
|
+
// any URL-scoped credential.<url>.helper into account. Null means none, which
|
|
123
|
+
// matters more than it looks: `git credential approve` with no helper
|
|
124
|
+
// configured succeeds and stores nothing at all.
|
|
125
|
+
async function configuredHelper(url) {
|
|
126
|
+
const r = await git(['config', '--get-urlmatch', 'credential.helper', url]);
|
|
127
|
+
const value = r.stdout.trim();
|
|
128
|
+
return r.code === 0 && value !== '' ? value : null;
|
|
129
|
+
}
|
|
130
|
+
// Recorded for this URL alone, so other remotes keep whatever they use now.
|
|
131
|
+
async function setHelper(url, helper) {
|
|
132
|
+
const r = await git(['config', '--global', `credential.${url}.helper`, helper]);
|
|
133
|
+
if (r.code !== 0) {
|
|
134
|
+
throw new CredentialError(r.stderr.trim() || `git config exited ${r.code}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function approveCredential(target, username, password) {
|
|
138
|
+
const r = await git(['credential', 'approve'], { input: describe(target, { username, password }) });
|
|
139
|
+
if (r.code !== 0) {
|
|
140
|
+
throw new CredentialError(r.stderr.trim() || `git credential approve exited ${r.code}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Reading back what is stored must never turn into a prompt. Without both of
|
|
144
|
+
// these, a lookup that finds nothing would ask: through an editor's askpass
|
|
145
|
+
// dialog when GIT_ASKPASS is set, which is invisible from the terminal the
|
|
146
|
+
// command was run in, or on the terminal otherwise. An empty GIT_ASKPASS also
|
|
147
|
+
// shadows core.askPass and SSH_ASKPASS, which git consults only when
|
|
148
|
+
// GIT_ASKPASS is unset.
|
|
149
|
+
async function readCredential(target) {
|
|
150
|
+
const r = await git(['credential', 'fill'], {
|
|
151
|
+
input: describe(target, {}),
|
|
152
|
+
env: { ...process.env, GIT_ASKPASS: '', GIT_TERMINAL_PROMPT: '0' },
|
|
153
|
+
});
|
|
154
|
+
if (r.code !== 0)
|
|
155
|
+
return null;
|
|
156
|
+
const c = parseCredential(r.stdout);
|
|
157
|
+
return c.username && c.password ? { username: c.username, password: c.password } : null;
|
|
158
|
+
}
|
|
159
|
+
async function rejectCredential(target, username) {
|
|
160
|
+
const r = await git(['credential', 'reject'], {
|
|
161
|
+
input: describe(target, username ? { username } : {}),
|
|
162
|
+
});
|
|
163
|
+
if (r.code !== 0) {
|
|
164
|
+
throw new CredentialError(r.stderr.trim() || `git credential reject exited ${r.code}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Where the CLI remembers which vault it is talking to. The token itself is
|
|
168
|
+
// never written here: it lives in git's credential store, put there by
|
|
169
|
+
// `mochi login`, so there is one place a token is kept rather than two.
|
|
170
|
+
// This file records only the vault URL of the most recent login, which is what
|
|
171
|
+
// makes `mochi user list` work with no arguments and no environment.
|
|
172
|
+
function loginPath() {
|
|
173
|
+
const base = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config');
|
|
174
|
+
return path.join(base, 'mochi', 'login.json');
|
|
175
|
+
}
|
|
176
|
+
function loadLogin(file = loginPath()) {
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
179
|
+
return typeof parsed.host === 'string' && parsed.host ? { host: parsed.host } : null;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function saveLogin(host, file = loginPath()) {
|
|
186
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
187
|
+
const tmp = `${file}.tmp`;
|
|
188
|
+
fs.writeFileSync(tmp, JSON.stringify({ host }, null, 2) + '\n', { mode: 0o600 });
|
|
189
|
+
fs.renameSync(tmp, file);
|
|
190
|
+
}
|
|
191
|
+
// Only forgets the vault named, so logging out of one vault does not silently
|
|
192
|
+
// redirect commands that were aimed at another.
|
|
193
|
+
function clearLogin(host, file = loginPath()) {
|
|
194
|
+
const saved = loadLogin(file);
|
|
195
|
+
if (!saved || saved.host !== host)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
fs.rmSync(file);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* nothing to remove */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// The vault a command should talk to, and the token to talk with. Precedence is
|
|
205
|
+
// the same for both: the option, then the environment, then what `mochi
|
|
206
|
+
// login` left behind (the URL in login.json, the token in git's credential
|
|
207
|
+
// store).
|
|
208
|
+
//
|
|
209
|
+
// The environment is here for a caller that has no keyring and possibly no
|
|
210
|
+
// writable home directory, which is an agent in a container rather than a
|
|
211
|
+
// person at a laptop. `mochi login` remains the one thing a person
|
|
212
|
+
// configures, and the runner already took this shape with
|
|
213
|
+
// MOCHI_RUNNER_TOKEN.
|
|
214
|
+
async function vaultTarget(args) {
|
|
215
|
+
const envHost = process.env.MOCHI_HOST?.trim() || null;
|
|
216
|
+
const envToken = process.env.MOCHI_TOKEN?.trim() || null;
|
|
217
|
+
const host = (args.host ?? envHost ?? loadLogin()?.host ?? '').replace(/\/+$/, '');
|
|
218
|
+
if (!host) {
|
|
219
|
+
throw new CredentialError('No vault. Log in to one first:\n\n' +
|
|
220
|
+
' mochi login https://vault.example.com\n\n' +
|
|
221
|
+
'or pass --host <url>, or set MOCHI_HOST.');
|
|
222
|
+
}
|
|
223
|
+
if (args.token)
|
|
224
|
+
return { host, token: args.token };
|
|
225
|
+
if (envToken)
|
|
226
|
+
return { host, token: envToken };
|
|
227
|
+
const target = credentialTarget(host);
|
|
228
|
+
const stored = await readCredential(target);
|
|
229
|
+
if (!stored) {
|
|
230
|
+
throw new CredentialError(`No stored token for ${target.url}. Log in again:\n\n` +
|
|
231
|
+
` mochi login ${target.url}\n\n` +
|
|
232
|
+
'or pass --token <token>, or set MOCHI_TOKEN.');
|
|
233
|
+
}
|
|
234
|
+
return { host, token: stored.password };
|
|
235
|
+
}
|