@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
@@ -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">&hellip;</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)}&amp;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
+ }