@magland/mochi 0.3.1 → 0.4.0
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/README.md +7 -6
- package/dist/agefile.js +47 -0
- package/dist/agescript.js +343 -0
- package/dist/api/admin.js +66 -28
- package/dist/api/auth.js +3 -2
- package/dist/api/backup.js +227 -192
- package/dist/api/contents.js +46 -16
- package/dist/api/repos.js +60 -4
- package/dist/api/write.js +138 -8
- package/dist/api.js +34 -1
- package/dist/browse.js +171 -33
- package/dist/build-info.json +2 -2
- package/dist/ci/api.js +138 -4
- package/dist/ci/artifacts.js +82 -2
- package/dist/ci/engine.js +30 -17
- package/dist/ci/expr.js +14 -0
- package/dist/ci/runners.js +60 -1
- package/dist/ci/runs.js +21 -3
- package/dist/ci/trigger.js +6 -4
- package/dist/ci/views.js +24 -3
- package/dist/ci/web.js +48 -0
- package/dist/cli/admin-cmd.js +40 -3
- package/dist/cli/backup-cmd.js +138 -120
- package/dist/cli/gitrun.js +50 -0
- package/dist/cli/pr-cmd.js +155 -0
- package/dist/cli/repo-cmd.js +134 -6
- package/dist/cli/sync-cmd.js +128 -0
- package/dist/cli/target.js +7 -3
- package/dist/config.js +10 -0
- package/dist/credentials.js +9 -8
- package/dist/deploy-cli.js +214 -109
- package/dist/deploy-runner-cli.js +125 -49
- package/dist/domains.js +259 -0
- package/dist/egress.js +23 -6
- package/dist/forms.js +703 -32
- package/dist/git.js +124 -13
- package/dist/githttp.js +80 -23
- package/dist/githubauth.js +185 -0
- package/dist/icons.js +14 -0
- package/dist/import-cli.js +40 -25
- package/dist/index.js +110 -4
- package/dist/issueweb.js +10 -6
- package/dist/lfs.js +9 -2
- package/dist/limit.js +2 -2
- package/dist/logincodes.js +31 -0
- package/dist/maintenance.js +297 -0
- package/dist/markdown.js +192 -3
- package/dist/mdedit.js +29 -0
- package/dist/migrate.js +48 -0
- package/dist/naming.js +25 -0
- package/dist/onetime.js +112 -0
- package/dist/ops.js +293 -16
- package/dist/pagescript.js +355 -24
- package/dist/perms.js +1 -0
- package/dist/profile.js +2 -0
- package/dist/pulls.js +21 -1
- package/dist/pullweb.js +25 -6
- package/dist/releases.js +10 -1
- package/dist/runner/actions.js +12 -1
- package/dist/runner/client.js +40 -2
- package/dist/runner/docker.js +40 -28
- package/dist/runner/download.js +77 -0
- package/dist/runner/externals.js +12 -1
- package/dist/runner/job.js +26 -11
- package/dist/runner/steps.js +42 -7
- package/dist/runner-cli.js +89 -0
- package/dist/scan.js +58 -2
- package/dist/server.js +107 -26
- package/dist/session.js +82 -40
- package/dist/site.js +110 -27
- package/dist/sitepublish.js +177 -0
- package/dist/sitesettings.js +420 -0
- package/dist/siteshost.js +116 -28
- package/dist/source.js +20 -0
- package/dist/style.js +162 -3
- package/dist/topics.js +165 -0
- package/dist/vault.js +399 -6
- package/dist/vendor-age.js +15 -0
- package/dist/views.js +242 -57
- package/dist/web.js +11 -1
- package/dist/webauthn.js +347 -0
- package/dist/webops.js +1263 -33
- package/package.json +4 -4
package/dist/git.js
CHANGED
|
@@ -4,7 +4,9 @@ exports.GitRepo = exports.GitError = void 0;
|
|
|
4
4
|
exports.execGit = execGit;
|
|
5
5
|
exports.execGitStatus = execGitStatus;
|
|
6
6
|
exports.isValidRefName = isValidRefName;
|
|
7
|
+
exports.isValidNewRefName = isValidNewRefName;
|
|
7
8
|
exports.isValidRepoPath = isValidRepoPath;
|
|
9
|
+
exports.isValidNewRepoPath = isValidNewRepoPath;
|
|
8
10
|
exports.isValidSha = isValidSha;
|
|
9
11
|
const child_process_1 = require("child_process");
|
|
10
12
|
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
@@ -55,6 +57,32 @@ function isValidRefName(ref) {
|
|
|
55
57
|
return false;
|
|
56
58
|
return true;
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether a name may become a new branch or tag. Stricter than isValidRefName,
|
|
62
|
+
* which stays permissive because it also vets names being looked up:
|
|
63
|
+
*
|
|
64
|
+
* - `HEAD` and git's other pseudo-refs (FETCH_HEAD, MERGE_HEAD, ...) resolve
|
|
65
|
+
* ahead of any branch or tag of the same name, so a ref that could be
|
|
66
|
+
* created under one would be ambiguous everywhere it is typed and warned
|
|
67
|
+
* about on every clone.
|
|
68
|
+
* - A `refs/` prefix means the caller pasted a full ref name; creating it
|
|
69
|
+
* verbatim would silently nest (refs/heads/refs/heads/x).
|
|
70
|
+
* - 200 characters keeps the loose-ref file under every common filesystem's
|
|
71
|
+
* 255-byte name limit, so a too-long name is refused here with a clear
|
|
72
|
+
* message rather than failing inside git.
|
|
73
|
+
* - A leading dash could read as an option to a git command.
|
|
74
|
+
*/
|
|
75
|
+
function isValidNewRefName(name) {
|
|
76
|
+
if (!isValidRefName(name) || name.startsWith('-'))
|
|
77
|
+
return false;
|
|
78
|
+
if (name.length > 200)
|
|
79
|
+
return false;
|
|
80
|
+
if (name === 'HEAD' || /^[A-Z_]+_HEAD$/.test(name))
|
|
81
|
+
return false;
|
|
82
|
+
if (name === 'refs' || name.startsWith('refs/'))
|
|
83
|
+
return false;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
58
86
|
function isValidRepoPath(p) {
|
|
59
87
|
if (p === '')
|
|
60
88
|
return true;
|
|
@@ -64,6 +92,24 @@ function isValidRepoPath(p) {
|
|
|
64
92
|
return false;
|
|
65
93
|
return p.split('/').every((seg) => seg !== '' && seg !== '.' && seg !== '..');
|
|
66
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether a path may be written into a repository. Stricter than
|
|
97
|
+
* isValidRepoPath, which also vets paths being read and so must keep serving
|
|
98
|
+
* whatever a repository already holds:
|
|
99
|
+
*
|
|
100
|
+
* - a `.git` component (in any case) is refused, because git itself will
|
|
101
|
+
* not take it in a tree and the failure would otherwise surface deep in
|
|
102
|
+
* the plumbing rather than as a validation error;
|
|
103
|
+
* - a backslash is refused, because a path carrying one cannot be checked
|
|
104
|
+
* out on Windows and is almost always a Windows separator pasted whole.
|
|
105
|
+
*/
|
|
106
|
+
function isValidNewRepoPath(p) {
|
|
107
|
+
if (!isValidRepoPath(p))
|
|
108
|
+
return false;
|
|
109
|
+
if (p.includes('\\'))
|
|
110
|
+
return false;
|
|
111
|
+
return p.split('/').every((seg) => seg.toLowerCase() !== '.git');
|
|
112
|
+
}
|
|
67
113
|
function isValidSha(s) {
|
|
68
114
|
return /^[0-9a-f]{40}$/i.test(s);
|
|
69
115
|
}
|
|
@@ -146,8 +192,65 @@ class GitRepo {
|
|
|
146
192
|
async catBlob(ref, path) {
|
|
147
193
|
return execGit(this.dir, ['cat-file', 'blob', `${ref}:${path}`]);
|
|
148
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The blob at a path: its object id and size, or null if there is no blob
|
|
197
|
+
* there. Asked before catBlob wherever the answer decides whether to read at
|
|
198
|
+
* all, since catBlob holds the whole object in memory and a repository may
|
|
199
|
+
* hold objects the size of the machine.
|
|
200
|
+
*/
|
|
201
|
+
async blobInfo(ref, path) {
|
|
202
|
+
let out;
|
|
203
|
+
try {
|
|
204
|
+
out = (await execGit(this.dir, ['ls-tree', '-z', '-l', ref, '--', path])).toString('utf8');
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
// ls-tree lists what the path names; a directory lists its own entry,
|
|
210
|
+
// typed tree, and is not a blob.
|
|
211
|
+
const m = out.split('\0')[0]?.match(/^(\S+) (\S+) (\S+) +(\S+)\t/);
|
|
212
|
+
if (!m || m[2] !== 'blob')
|
|
213
|
+
return null;
|
|
214
|
+
const size = parseInt(m[4], 10);
|
|
215
|
+
return Number.isFinite(size) ? { sha: m[3], size } : null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Write a blob's bytes to a stream without holding them. Resolves when git
|
|
219
|
+
* has finished or the destination has gone; rejects only if git could not
|
|
220
|
+
* start or exited in error before writing anything. A destination that
|
|
221
|
+
* closes early has git killed rather than left blocked on a full pipe.
|
|
222
|
+
*/
|
|
223
|
+
streamBlob(ref, path, out) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const child = (0, child_process_1.spawn)('git', ['-C', this.dir, 'cat-file', 'blob', `${ref}:${path}`], {
|
|
226
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
227
|
+
});
|
|
228
|
+
let stderr = '';
|
|
229
|
+
let wrote = false;
|
|
230
|
+
child.stderr.on('data', (d) => {
|
|
231
|
+
if (stderr.length < 4096)
|
|
232
|
+
stderr += d.toString();
|
|
233
|
+
});
|
|
234
|
+
child.stdout.once('data', () => {
|
|
235
|
+
wrote = true;
|
|
236
|
+
});
|
|
237
|
+
child.stdout.pipe(out, { end: true });
|
|
238
|
+
out.on('close', () => {
|
|
239
|
+
if (child.exitCode === null)
|
|
240
|
+
child.kill();
|
|
241
|
+
resolve();
|
|
242
|
+
});
|
|
243
|
+
child.on('error', (e) => reject(new GitError(`git cat-file failed: ${e.message}`)));
|
|
244
|
+
child.on('close', (code) => {
|
|
245
|
+
if (code === 0 || wrote)
|
|
246
|
+
resolve();
|
|
247
|
+
else
|
|
248
|
+
reject(new GitError(`git cat-file failed: ${stderr.trim() || `exit ${code}`}`));
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
149
252
|
async log(ref, skip, limit, path, author) {
|
|
150
|
-
const fmt = '%H%x00%an%x00%aI%x00%s';
|
|
253
|
+
const fmt = '%H%x00%an%x00%ae%x00%aI%x00%s';
|
|
151
254
|
const args = ['log', `--format=${fmt}`, '-n', String(limit), `--skip=${skip}`];
|
|
152
255
|
// -F makes --author a literal string rather than a pattern, so a name
|
|
153
256
|
// with a dot or a plus in it means itself.
|
|
@@ -164,8 +267,8 @@ class GitRepo {
|
|
|
164
267
|
return [];
|
|
165
268
|
}
|
|
166
269
|
return ls.map((l) => {
|
|
167
|
-
const [sha, author, date, subject] = l.split('\0');
|
|
168
|
-
return { sha, author, date, subject: subject ?? '' };
|
|
270
|
+
const [sha, author, email, date, subject] = l.split('\0');
|
|
271
|
+
return { sha, author, email, date, subject: subject ?? '' };
|
|
169
272
|
});
|
|
170
273
|
}
|
|
171
274
|
/**
|
|
@@ -220,7 +323,7 @@ class GitRepo {
|
|
|
220
323
|
// Renames are wanted as renames rather than as an add and a delete, so
|
|
221
324
|
// that both names date from the same commit.
|
|
222
325
|
'--find-renames',
|
|
223
|
-
'--format=%x01%H%x00%an%x00%aI%x00%s',
|
|
326
|
+
'--format=%x01%H%x00%an%x00%ae%x00%aI%x00%s',
|
|
224
327
|
ref,
|
|
225
328
|
'--',
|
|
226
329
|
];
|
|
@@ -274,13 +377,13 @@ class GitRepo {
|
|
|
274
377
|
if (!record)
|
|
275
378
|
continue;
|
|
276
379
|
const parts = record.split('\0');
|
|
277
|
-
const [sha, author, date, subject] = parts;
|
|
380
|
+
const [sha, author, email, date, subject] = parts;
|
|
278
381
|
if (!sha)
|
|
279
382
|
continue;
|
|
280
|
-
const commit = { sha, author, date, subject: subject ?? '' };
|
|
281
|
-
// parts[
|
|
383
|
+
const commit = { sha, author, email, date, subject: subject ?? '' };
|
|
384
|
+
// parts[5] onwards is the diff: a status, then the one path it renames
|
|
282
385
|
// from and to, or the one path it touches.
|
|
283
|
-
for (let i =
|
|
386
|
+
for (let i = 5; i < parts.length; i++) {
|
|
284
387
|
const status = parts[i].replace(/^\n/, '');
|
|
285
388
|
if (!status)
|
|
286
389
|
continue;
|
|
@@ -316,7 +419,10 @@ class GitRepo {
|
|
|
316
419
|
const fmt = '%H%x00%an%x00%ae%x00%aI%x00%P%x00%B';
|
|
317
420
|
let out;
|
|
318
421
|
try {
|
|
319
|
-
|
|
422
|
+
// --end-of-options, so that a revision beginning with a dash is a
|
|
423
|
+
// revision git fails to find rather than an option it obeys: `--output`
|
|
424
|
+
// would otherwise write this listing wherever the caller said.
|
|
425
|
+
out = (await execGit(this.dir, ['log', '-1', `--format=${fmt}`, '--end-of-options', sha, '--'])).toString('utf8');
|
|
320
426
|
}
|
|
321
427
|
catch {
|
|
322
428
|
return null;
|
|
@@ -394,6 +500,7 @@ class GitRepo {
|
|
|
394
500
|
lines.push({
|
|
395
501
|
sha: cur.sha ?? '',
|
|
396
502
|
author: cur.author ?? '',
|
|
503
|
+
email: cur.email ?? '',
|
|
397
504
|
date: cur.time ? new Date(cur.time * 1000).toISOString() : '',
|
|
398
505
|
summary: cur.summary ?? '',
|
|
399
506
|
previous: cur.previous ?? null,
|
|
@@ -412,6 +519,10 @@ class GitRepo {
|
|
|
412
519
|
const value = sp === -1 ? '' : line.slice(sp + 1);
|
|
413
520
|
if (key === 'author')
|
|
414
521
|
cur.author = value;
|
|
522
|
+
// The porcelain wraps the address in angle brackets; the brackets are
|
|
523
|
+
// its framing, not part of the address.
|
|
524
|
+
else if (key === 'author-mail')
|
|
525
|
+
cur.email = value.replace(/^<|>$/g, '');
|
|
415
526
|
else if (key === 'author-time')
|
|
416
527
|
cur.time = parseInt(value, 10);
|
|
417
528
|
else if (key === 'summary')
|
|
@@ -433,7 +544,7 @@ class GitRepo {
|
|
|
433
544
|
* a reader almost always means.
|
|
434
545
|
*/
|
|
435
546
|
async compare(base, head, limit = 250) {
|
|
436
|
-
const fmt = '%H%x00%an%x00%aI%x00%s';
|
|
547
|
+
const fmt = '%H%x00%an%x00%ae%x00%aI%x00%s';
|
|
437
548
|
const [counts, log, patch, mergeBase] = await Promise.all([
|
|
438
549
|
execGit(this.dir, ['rev-list', '--left-right', '--count', `${base}...${head}`])
|
|
439
550
|
.then((b) => b.toString('utf8').trim().split(/\s+/))
|
|
@@ -452,8 +563,8 @@ class GitRepo {
|
|
|
452
563
|
.split('\n')
|
|
453
564
|
.filter((l) => l !== '')
|
|
454
565
|
.map((l) => {
|
|
455
|
-
const [sha, author, date, subject] = l.split('\0');
|
|
456
|
-
return { sha, author, date, subject: subject ?? '' };
|
|
566
|
+
const [sha, author, email, date, subject] = l.split('\0');
|
|
567
|
+
return { sha, author, email, date, subject: subject ?? '' };
|
|
457
568
|
});
|
|
458
569
|
return {
|
|
459
570
|
behind: parseInt(counts[0], 10) || 0,
|
|
@@ -485,7 +596,7 @@ class GitRepo {
|
|
|
485
596
|
return people;
|
|
486
597
|
}
|
|
487
598
|
async commitPatch(sha) {
|
|
488
|
-
return (await execGit(this.dir, ['show', '--format=', '--patch', '--no-color', sha, '--'])).toString('utf8');
|
|
599
|
+
return (await execGit(this.dir, ['show', '--format=', '--patch', '--no-color', '--end-of-options', sha, '--'])).toString('utf8');
|
|
489
600
|
}
|
|
490
601
|
/**
|
|
491
602
|
* Stream `git archive` for `ref` into `out`, as GitHub's source downloads
|
package/dist/githttp.js
CHANGED
|
@@ -38,12 +38,14 @@ exports.checkReadAuth = checkReadAuth;
|
|
|
38
38
|
exports.checkPushAuth = checkPushAuth;
|
|
39
39
|
exports.registerGitHttp = registerGitHttp;
|
|
40
40
|
const child_process_1 = require("child_process");
|
|
41
|
+
const stream = __importStar(require("stream"));
|
|
41
42
|
const zlib = __importStar(require("zlib"));
|
|
42
43
|
const git_1 = require("./git");
|
|
43
44
|
const jobtoken_1 = require("./jobtoken");
|
|
44
45
|
const ops_1 = require("./ops");
|
|
45
46
|
const perms_1 = require("./perms");
|
|
46
47
|
const scan_1 = require("./scan");
|
|
48
|
+
const sitepublish_1 = require("./sitepublish");
|
|
47
49
|
const limit_1 = require("./limit");
|
|
48
50
|
const vault_1 = require("./vault");
|
|
49
51
|
const web_1 = require("./web");
|
|
@@ -168,10 +170,23 @@ function checkPushAuth(root, limiter, req, collection, repoName, repo) {
|
|
|
168
170
|
if ('ok' in checked)
|
|
169
171
|
return checked;
|
|
170
172
|
const auth = checked.auth;
|
|
173
|
+
// One refusal, in one wording, for a private repository the credential has
|
|
174
|
+
// no role in and for a name that does not exist in a collection it may not
|
|
175
|
+
// create in. The two used to differ -- 404 for the first, 403 naming the
|
|
176
|
+
// user for the second -- which let any signed-in user list a collection's
|
|
177
|
+
// private repositories by pushing at guessed names and reading which code
|
|
178
|
+
// came back. The sentence is true of both: whoever may create in a
|
|
179
|
+
// collection owns it, and an owner has the admin role on every repository
|
|
180
|
+
// in it, so nobody who could create here is ever refused a private one.
|
|
181
|
+
const notFound = {
|
|
182
|
+
ok: false,
|
|
183
|
+
status: 404,
|
|
184
|
+
message: 'repository not found (a push to a name that does not exist creates it only in a collection you own)',
|
|
185
|
+
};
|
|
171
186
|
if (repo) {
|
|
172
187
|
const role = (0, perms_1.repoRole)(root, auth, repo);
|
|
173
188
|
if (role === null)
|
|
174
|
-
return
|
|
189
|
+
return notFound;
|
|
175
190
|
if (!(0, perms_1.atLeast)(role, 'write')) {
|
|
176
191
|
return {
|
|
177
192
|
ok: false,
|
|
@@ -181,13 +196,8 @@ function checkPushAuth(root, limiter, req, collection, repoName, repo) {
|
|
|
181
196
|
}
|
|
182
197
|
return { ok: true, auth };
|
|
183
198
|
}
|
|
184
|
-
if (!(0, perms_1.canCreateRepo)(root, auth, collection, repoName))
|
|
185
|
-
return
|
|
186
|
-
ok: false,
|
|
187
|
-
status: 403,
|
|
188
|
-
message: `user ${auth.username} is not allowed to create ${collection}/${repoName}`,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
199
|
+
if (!(0, perms_1.canCreateRepo)(root, auth, collection, repoName))
|
|
200
|
+
return notFound;
|
|
191
201
|
return { ok: true, auth };
|
|
192
202
|
}
|
|
193
203
|
// The refs a push may have changed, read before and after receive-pack. A
|
|
@@ -305,15 +315,35 @@ function registerGitHttp(app, root, gates, authLimiter, engine) {
|
|
|
305
315
|
res.setHeader('Content-Type', `application/x-${service}-result`);
|
|
306
316
|
res.setHeader('Cache-Control', 'no-cache');
|
|
307
317
|
const child = (0, child_process_1.spawn)('git', [service.slice(4), '--stateless-rpc', dir], { env: gitEnv(req) });
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
318
|
+
// The request body reaches git through a pipeline rather than a chain of
|
|
319
|
+
// pipe() calls, because pipe() leaves an error on the destination with no
|
|
320
|
+
// listener, and an 'error' nobody listens for ends the process. A body
|
|
321
|
+
// declared gzip that is not gzip is the case that reaches here from the
|
|
322
|
+
// network: it is a bad request, and what should die is the request.
|
|
323
|
+
// Likewise git exiting early, which turns its stdin into an EPIPE for the
|
|
324
|
+
// writer: the exit code is the answer, and the pipe error is noise.
|
|
325
|
+
const stages = [req];
|
|
326
|
+
if (req.headers['content-encoding'] === 'gzip')
|
|
327
|
+
stages.push(zlib.createGunzip());
|
|
328
|
+
stream.pipeline([...stages, child.stdin], (err) => {
|
|
329
|
+
if (err)
|
|
330
|
+
child.kill();
|
|
331
|
+
});
|
|
313
332
|
child.stdout.pipe(res);
|
|
333
|
+
// A client that goes away mid-transfer leaves git writing into a pipe
|
|
334
|
+
// nobody drains. Node unpipes and pauses the source, which is a git
|
|
335
|
+
// process blocked forever, holding its packfile in memory, one per
|
|
336
|
+
// abandoned clone. A response closed without finishing is that case;
|
|
337
|
+
// one that finished has a git that is already exiting.
|
|
338
|
+
res.on('close', () => {
|
|
339
|
+
if (!res.writableFinished)
|
|
340
|
+
child.kill();
|
|
341
|
+
});
|
|
314
342
|
child.on('error', () => {
|
|
315
343
|
release();
|
|
316
|
-
res.
|
|
344
|
+
if (!res.headersSent)
|
|
345
|
+
res.status(500);
|
|
346
|
+
res.end();
|
|
317
347
|
});
|
|
318
348
|
child.on('close', (code) => {
|
|
319
349
|
release();
|
|
@@ -359,7 +389,19 @@ function registerGitHttp(app, root, gates, authLimiter, engine) {
|
|
|
359
389
|
res.status(400).type('text/plain').send('collection names may not begin with a dot\n');
|
|
360
390
|
return;
|
|
361
391
|
}
|
|
362
|
-
|
|
392
|
+
// The ops layer applies the rest of what a new name must satisfy
|
|
393
|
+
// (length, case collisions with existing names); its refusal is an
|
|
394
|
+
// answer for the pusher, not a failure of the server.
|
|
395
|
+
try {
|
|
396
|
+
repo = await (0, ops_1.createRepo)(root, collectionName, repoName);
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
if (e instanceof ops_1.OpError) {
|
|
400
|
+
res.status((0, ops_1.opErrorStatus)(e.kind)).type('text/plain').send(`${e.message}\n`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
throw e;
|
|
404
|
+
}
|
|
363
405
|
}
|
|
364
406
|
await advertise(req, res, 'git-receive-pack', repo.dir);
|
|
365
407
|
return;
|
|
@@ -397,7 +439,18 @@ function registerGitHttp(app, root, gates, authLimiter, engine) {
|
|
|
397
439
|
res.status(400).type('text/plain').send('collection names may not begin with a dot\n');
|
|
398
440
|
return;
|
|
399
441
|
}
|
|
400
|
-
|
|
442
|
+
// As in the advertisement above: the ops layer's refusal of a new
|
|
443
|
+
// name is an answer for the pusher.
|
|
444
|
+
try {
|
|
445
|
+
repo = await (0, ops_1.createRepo)(root, collectionName, repoName);
|
|
446
|
+
}
|
|
447
|
+
catch (e) {
|
|
448
|
+
if (e instanceof ops_1.OpError) {
|
|
449
|
+
res.status((0, ops_1.opErrorStatus)(e.kind)).type('text/plain').send(`${e.message}\n`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
throw e;
|
|
453
|
+
}
|
|
401
454
|
}
|
|
402
455
|
const target = repo;
|
|
403
456
|
const actor = auth.username;
|
|
@@ -409,20 +462,24 @@ function registerGitHttp(app, root, gates, authLimiter, engine) {
|
|
|
409
462
|
if (code !== 0)
|
|
410
463
|
return;
|
|
411
464
|
ensureHead(target)
|
|
412
|
-
.then(() => (
|
|
465
|
+
.then(() => firePushEvents(root, engine, target, before, actor))
|
|
413
466
|
.catch((e) => console.error(`post-receive handling failed: ${e instanceof Error ? e.message : e}`));
|
|
414
467
|
});
|
|
415
468
|
}));
|
|
416
469
|
}
|
|
417
|
-
// Turn a before/after ref snapshot into push events
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
470
|
+
// Turn a before/after ref snapshot into push events: a site published from the
|
|
471
|
+
// repository is rewritten when its branch moved, and the CI engine, when there
|
|
472
|
+
// is one, is told about every moved ref. Ref deletions are reported (with an
|
|
473
|
+
// all-zero "after") and ignored downstream; a workflow file that fails to
|
|
474
|
+
// parse still produces a visible failed run, which is why nothing here
|
|
475
|
+
// filters on content.
|
|
476
|
+
async function firePushEvents(root, engine, repo, before, actor) {
|
|
422
477
|
const after = await refSnapshot(repo);
|
|
423
478
|
for (const [ref, sha] of after) {
|
|
424
479
|
if (before.get(ref) === sha)
|
|
425
480
|
continue;
|
|
426
|
-
await
|
|
481
|
+
await (0, sitepublish_1.publishSiteAfterPush)(root, repo, ref);
|
|
482
|
+
if (engine)
|
|
483
|
+
await engine.handlePush(repo, { ref, before: before.get(ref) ?? ZERO, after: sha, actor });
|
|
427
484
|
}
|
|
428
485
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
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.GITHUB_SECRET_FILE = void 0;
|
|
37
|
+
exports.overrideGithubEndpointsForTests = overrideGithubEndpointsForTests;
|
|
38
|
+
exports.readGithubSecret = readGithubSecret;
|
|
39
|
+
exports.writeGithubSecret = writeGithubSecret;
|
|
40
|
+
exports.clearGithubSecret = clearGithubSecret;
|
|
41
|
+
exports.githubConfigured = githubConfigured;
|
|
42
|
+
exports.githubAuthorizeUrl = githubAuthorizeUrl;
|
|
43
|
+
exports.exchangeGithubCode = exchangeGithubCode;
|
|
44
|
+
exports.fetchGithubUser = fetchGithubUser;
|
|
45
|
+
exports.isPlausibleGithubLogin = isPlausibleGithubLogin;
|
|
46
|
+
exports.lookupGithubLogin = lookupGithubLogin;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const config_1 = require("./config");
|
|
50
|
+
// Sign-in with GitHub: the OAuth code flow against github.com, and the small
|
|
51
|
+
// outbound calls it needs. These are the only requests this server makes to
|
|
52
|
+
// another host, they happen only when an administrator has configured the
|
|
53
|
+
// feature, and nothing GitHub returns is stored: the access token is used for
|
|
54
|
+
// one request -- who signed in -- and dropped, so the vault holds no GitHub
|
|
55
|
+
// token. Who may sign in, and the gh:<id> session binding the callback mints,
|
|
56
|
+
// live in src/vault.ts beside the other credentials; the routes are in
|
|
57
|
+
// src/webops.ts with the other sign-in flows.
|
|
58
|
+
/**
|
|
59
|
+
* Where the OAuth App's client secret is kept: a mode-600 file beside
|
|
60
|
+
* `.secret`, because config.json is ordinary vault state and the secret is
|
|
61
|
+
* not. The client id, which is public, does live in config.json.
|
|
62
|
+
*/
|
|
63
|
+
exports.GITHUB_SECRET_FILE = '.github-secret';
|
|
64
|
+
// The three places the flow talks to, overridable so the tests can stand a
|
|
65
|
+
// local stub in for github.com. Nothing else writes this.
|
|
66
|
+
let endpoints = {
|
|
67
|
+
authorize: 'https://github.com/login/oauth/authorize',
|
|
68
|
+
token: 'https://github.com/login/oauth/access_token',
|
|
69
|
+
api: 'https://api.github.com',
|
|
70
|
+
};
|
|
71
|
+
function overrideGithubEndpointsForTests(next) {
|
|
72
|
+
endpoints = next;
|
|
73
|
+
}
|
|
74
|
+
function readGithubSecret(root) {
|
|
75
|
+
try {
|
|
76
|
+
const secret = fs.readFileSync(path.join(root, exports.GITHUB_SECRET_FILE), 'utf8').trim();
|
|
77
|
+
return secret === '' ? null : secret;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function writeGithubSecret(root, secret) {
|
|
84
|
+
fs.writeFileSync(path.join(root, exports.GITHUB_SECRET_FILE), secret.trim() + '\n', { mode: 0o600 });
|
|
85
|
+
}
|
|
86
|
+
function clearGithubSecret(root) {
|
|
87
|
+
fs.rmSync(path.join(root, exports.GITHUB_SECRET_FILE), { force: true });
|
|
88
|
+
}
|
|
89
|
+
/** Whether sign-in with GitHub can work here: a client id and a stored secret. */
|
|
90
|
+
function githubConfigured(root) {
|
|
91
|
+
return (0, config_1.loadConfig)(root).auth.githubClientId !== '' && readGithubSecret(root) !== null;
|
|
92
|
+
}
|
|
93
|
+
/** Where the sign-in redirect sends the browser. No scopes are requested: the
|
|
94
|
+
* callback asks /user for the id and login, which a scopeless token can read. */
|
|
95
|
+
function githubAuthorizeUrl(clientId, redirectUri, state) {
|
|
96
|
+
const u = new URL(endpoints.authorize);
|
|
97
|
+
u.searchParams.set('client_id', clientId);
|
|
98
|
+
u.searchParams.set('redirect_uri', redirectUri);
|
|
99
|
+
u.searchParams.set('state', state);
|
|
100
|
+
return u.toString();
|
|
101
|
+
}
|
|
102
|
+
const FETCH_TIMEOUT_MS = 10000;
|
|
103
|
+
// GitHub's API refuses requests that carry no User-Agent.
|
|
104
|
+
const USER_AGENT = 'mochi-forge';
|
|
105
|
+
/** Trade the callback's code for an access token. Null on any refusal or
|
|
106
|
+
* network failure; the caller has one message for all of them, since the
|
|
107
|
+
* person at the browser can do the same thing about each: try again. */
|
|
108
|
+
async function exchangeGithubCode(opts) {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(endpoints.token, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
accept: 'application/json',
|
|
114
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
115
|
+
'user-agent': USER_AGENT,
|
|
116
|
+
},
|
|
117
|
+
body: new URLSearchParams({
|
|
118
|
+
client_id: opts.clientId,
|
|
119
|
+
client_secret: opts.clientSecret,
|
|
120
|
+
code: opts.code,
|
|
121
|
+
redirect_uri: opts.redirectUri,
|
|
122
|
+
}),
|
|
123
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
return null;
|
|
127
|
+
const body = (await res.json());
|
|
128
|
+
return typeof body.access_token === 'string' && body.access_token !== '' ? body.access_token : null;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Who the access token belongs to. The token's only use; it is not kept. */
|
|
135
|
+
async function fetchGithubUser(accessToken) {
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(`${endpoints.api}/user`, {
|
|
138
|
+
headers: {
|
|
139
|
+
accept: 'application/vnd.github+json',
|
|
140
|
+
authorization: `Bearer ${accessToken}`,
|
|
141
|
+
'user-agent': USER_AGENT,
|
|
142
|
+
},
|
|
143
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok)
|
|
146
|
+
return null;
|
|
147
|
+
return asGithubUser(await res.json());
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** A GitHub login: alphanumeric with interior hyphens, at most 39 characters.
|
|
154
|
+
* Checked before a lookup so a stray path or an @handle is refused with a
|
|
155
|
+
* message rather than sent to the API inside a URL. */
|
|
156
|
+
function isPlausibleGithubLogin(login) {
|
|
157
|
+
return /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(login);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a login to its stable numeric id, over GitHub's public,
|
|
161
|
+
* unauthenticated API. Called once per approval an administrator adds, which
|
|
162
|
+
* sits far under that API's rate limit.
|
|
163
|
+
*/
|
|
164
|
+
async function lookupGithubLogin(login) {
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(`${endpoints.api}/users/${encodeURIComponent(login)}`, {
|
|
167
|
+
headers: { accept: 'application/vnd.github+json', 'user-agent': USER_AGENT },
|
|
168
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok)
|
|
171
|
+
return null;
|
|
172
|
+
return asGithubUser(await res.json());
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function asGithubUser(body) {
|
|
179
|
+
if (typeof body !== 'object' || body === null)
|
|
180
|
+
return null;
|
|
181
|
+
const rec = body;
|
|
182
|
+
if (typeof rec.id !== 'number' || typeof rec.login !== 'string')
|
|
183
|
+
return null;
|
|
184
|
+
return { id: Math.floor(rec.id), login: rec.login };
|
|
185
|
+
}
|
package/dist/icons.js
CHANGED
|
@@ -43,6 +43,7 @@ const PATHS = {
|
|
|
43
43
|
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 6.8V12L15.6 14.1"/>',
|
|
44
44
|
sync: '<path d="M12 3.5a8.5 8.5 0 1 1-8.5 8.5"/><path d="M1.9 14.6 3.5 12 5.1 14.6"/>',
|
|
45
45
|
kebab: `${DOT(5, 12)}${DOT(12, 12)}${DOT(19, 12)}`,
|
|
46
|
+
info: `<circle cx="12" cy="12" r="9"/>${DOT(12, 7.8, 1.2)}<path d="M12 11.2V16.6"/>`,
|
|
46
47
|
// Repositories and their contents.
|
|
47
48
|
repo: '<rect x="4" y="3" width="16" height="18" rx="2.5"/><path d="M8.5 3V21"/>',
|
|
48
49
|
'repo-forked': '<circle cx="6" cy="5" r="2.5"/><circle cx="18" cy="5" r="2.5"/><circle cx="12" cy="19" r="2.5"/><path d="M6 7.5v1.5a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V7.5M12 12v4.5"/>',
|
|
@@ -55,6 +56,9 @@ const PATHS = {
|
|
|
55
56
|
package: '<path d="M3.2 7.2 12 2.4l8.8 4.8v9.6L12 21.6l-8.8-4.8Z"/><path d="M3.2 7.2 12 12l8.8-4.8M12 12v9.6"/>',
|
|
56
57
|
tag: '<path d="M13.6 3.6A2 2 0 0 0 12.2 3H5a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l7.2 7.2a2 2 0 0 0 2.8 0l7.2-7.2a2 2 0 0 0 0-2.8Z"/><circle cx="7.6" cy="7.6" r="1.4"/>',
|
|
57
58
|
law: '<path d="M12 4V21M8 21h8M4.5 7.5 12 6l7.5 1.5"/><path d="M4.5 7.5 2 13a2.5 2.5 0 0 0 5 0Z"/><path d="M19.5 7.5 17 13a2.5 2.5 0 0 0 5 0Z"/>',
|
|
59
|
+
lock: `<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8.5 11V7.5a3.5 3.5 0 0 1 7 0V11"/>${DOT(12, 15.5)}`,
|
|
60
|
+
eye: '<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/>',
|
|
61
|
+
'eye-off': '<path d="M2.5 12S6 5.5 12 5.5c1.6 0 3 .4 4.3 1.1M20 8.5c.9 1.9 1.5 3.5 1.5 3.5S18 18.5 12 18.5c-1.6 0-3-.4-4.3-1.1"/><path d="M9.9 14.1a2.8 2.8 0 0 1 4-4"/><path d="M4 20 20 4"/>',
|
|
58
62
|
// The git graph.
|
|
59
63
|
'git-commit': '<circle cx="12" cy="12" r="3.6"/><path d="M2.5 12h5.9M15.6 12h5.9"/>',
|
|
60
64
|
'git-branch': '<circle cx="6" cy="18.5" r="2.5"/><circle cx="18" cy="6" r="2.5"/><path d="M6 16V3M18 8.5a9.5 9.5 0 0 1-9.5 9.5H8"/>',
|
|
@@ -70,6 +74,16 @@ const PATHS = {
|
|
|
70
74
|
filter: '<path d="M3.5 6.5h17M6.5 12h11M10 17.5h4"/>',
|
|
71
75
|
search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.2 15.2 20.5 20.5"/>',
|
|
72
76
|
link: '<path d="M9.5 14.5 14.5 9.5"/><path d="M10.5 6.6 12.3 4.8a4.5 4.5 0 0 1 6.4 6.4l-1.8 1.8"/><path d="M13.5 17.4 11.7 19.2a4.5 4.5 0 0 1-6.4-6.4l1.8-1.8"/>',
|
|
77
|
+
// The markdown toolbar. Letterforms and list marks, drawn in the same
|
|
78
|
+
// monoline stroke as everything else rather than borrowed from a text face.
|
|
79
|
+
heading: '<path d="M5.5 5v14M18.5 5v14M5.5 12h13"/>',
|
|
80
|
+
bold: '<path d="M7.5 5.5V18.5"/><path d="M7.5 5.5h5.2a3.25 3.25 0 0 1 0 6.5H7.5"/><path d="M7.5 12h6.2a3.25 3.25 0 0 1 0 6.5H7.5"/>',
|
|
81
|
+
italic: '<path d="M10.5 5.5h7M6.5 18.5h7M14 5.5 10 18.5"/>',
|
|
82
|
+
strikethrough: '<path d="M4 12h16"/><path d="M16.6 7.4C16 6 14.2 5 12 5 9.5 5 7.6 6.3 7.6 8.2c0 1.4 1 2.3 2.9 2.9"/><path d="M7.4 16.6C8 18 9.8 19 12 19c2.5 0 4.4-1.3 4.4-3.2 0-.6-.2-1.1-.5-1.5"/>',
|
|
83
|
+
quote: '<path d="M4.5 5.5v13"/><path d="M9.5 7.5h11M9.5 12h8M9.5 16.5h11"/>',
|
|
84
|
+
'list-unordered': `${DOT(4.5, 6)}${DOT(4.5, 12)}${DOT(4.5, 18)}<path d="M9.5 6h11M9.5 12h11M9.5 18h11"/>`,
|
|
85
|
+
'list-ordered': '<path d="M4.2 5.6 6.1 4.2V9.5"/><path d="M4.2 15.2a1.9 1.9 0 0 1 3.7.6c0 1.4-3.7 2.2-3.7 4h3.9"/><path d="M11 6.5h9.5M11 17.5h9.5"/>',
|
|
86
|
+
tasklist: '<rect x="3.5" y="3.5" width="7" height="7" rx="1.8"/><path d="M5.7 7.2 6.9 8.4 8.8 6"/><path d="M14 7h6.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.8"/><path d="M14 17h6.5"/>',
|
|
73
87
|
// People.
|
|
74
88
|
person: '<circle cx="12" cy="8" r="3.6"/><path d="M4.5 20.5a7.5 7.5 0 0 1 15 0"/>',
|
|
75
89
|
people: '<circle cx="8.6" cy="8" r="3.2"/><circle cx="17" cy="8.8" r="2.4"/><path d="M2.6 19.5a6 6 0 0 1 12 0"/><path d="M16.2 14.2a5.6 5.6 0 0 1 5.2 5.3"/>',
|