@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/api/repos.js
CHANGED
|
@@ -38,7 +38,11 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const layout_1 = require("../layout");
|
|
40
40
|
const perms_1 = require("../perms");
|
|
41
|
+
const domains_1 = require("../domains");
|
|
41
42
|
const scan_1 = require("../scan");
|
|
43
|
+
const sitesettings_1 = require("../sitesettings");
|
|
44
|
+
const site_1 = require("../site");
|
|
45
|
+
const topics_1 = require("../topics");
|
|
42
46
|
const issues_1 = require("../issues");
|
|
43
47
|
const pulls_1 = require("../pulls");
|
|
44
48
|
const releases_1 = require("../releases");
|
|
@@ -50,20 +54,48 @@ function summarize(root, collection, dirName) {
|
|
|
50
54
|
collection,
|
|
51
55
|
name,
|
|
52
56
|
description: (0, scan_1.repoDescription)(dir) ?? '',
|
|
57
|
+
topics: (0, topics_1.repoTopics)(dir),
|
|
53
58
|
forkedFrom: (0, scan_1.forkParent)(dir),
|
|
54
|
-
|
|
59
|
+
upstream: (0, scan_1.upstreamOf)(dir)?.url ?? null,
|
|
60
|
+
// Published, not merely present: files on disk with the site disabled are
|
|
61
|
+
// not a site, here as everywhere the interface asks.
|
|
62
|
+
hasSite: (0, sitesettings_1.siteSettings)(dir).enabled && (0, scan_1.siteDir)(root, collection, name) !== null,
|
|
55
63
|
private: (0, perms_1.repoIsPrivate)(dir),
|
|
56
64
|
};
|
|
57
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The site block a repository's API responses carry: the settings, and the URL
|
|
68
|
+
* the site is (or would be) served at, which only the vault knows.
|
|
69
|
+
*/
|
|
70
|
+
function siteInfo(root, req, repo) {
|
|
71
|
+
const settings = (0, sitesettings_1.siteSettings)(repo.dir);
|
|
72
|
+
const origin = (0, site_1.siteHostUrl)(root, req, repo.collection, repo.name);
|
|
73
|
+
return {
|
|
74
|
+
enabled: settings.enabled,
|
|
75
|
+
source: settings.source,
|
|
76
|
+
path: settings.path,
|
|
77
|
+
label: settings.label === '' ? null : settings.label,
|
|
78
|
+
domain: (0, domains_1.repoDomain)(root, repo.collection, repo.name),
|
|
79
|
+
url: origin
|
|
80
|
+
? `${origin}/`
|
|
81
|
+
: `/${encodeURIComponent(repo.collection)}/${encodeURIComponent(repo.name)}/site/`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
58
84
|
function registerRepoApi(app, root, limiter) {
|
|
59
85
|
// Every repository in the vault, flat. A vault's collections are small and
|
|
60
86
|
// this is read off disk per request, so there is no pagination: --limit and
|
|
61
87
|
// the server-side caps are enough, and a cursor protocol would be machinery
|
|
62
|
-
// with nothing to carry.
|
|
88
|
+
// with nothing to carry. ?topic= narrows the list to repositories carrying
|
|
89
|
+
// that topic, which is the one cross-repository query topics exist for.
|
|
63
90
|
app.get('/api/repos', (req, res) => {
|
|
64
91
|
const auth = (0, auth_1.requireApiAuth)(root, limiter, req, res);
|
|
65
92
|
if (!auth)
|
|
66
93
|
return;
|
|
94
|
+
const topic = typeof req.query.topic === 'string' ? req.query.topic : '';
|
|
95
|
+
if (topic !== '' && !(0, topics_1.isValidTopic)(topic)) {
|
|
96
|
+
(0, auth_1.apiError)(res, 400, 'a topic is lowercase letters, digits, and hyphens, starting with a letter or digit');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
67
99
|
const repos = [];
|
|
68
100
|
for (const c of (0, scan_1.listCollections)(root)) {
|
|
69
101
|
for (const dirName of (0, scan_1.listRepoDirs)(root, c.name)) {
|
|
@@ -72,11 +104,30 @@ function registerRepoApi(app, root, limiter) {
|
|
|
72
104
|
// same way its page 404s.
|
|
73
105
|
if ((0, perms_1.repoRole)(root, auth, { collection: c.name, name: (0, scan_1.displayName)(dirName), dir }) === null)
|
|
74
106
|
continue;
|
|
107
|
+
if (topic !== '' && !(0, topics_1.repoTopics)(dir).includes(topic))
|
|
108
|
+
continue;
|
|
75
109
|
repos.push(summarize(root, c.name, dirName));
|
|
76
110
|
}
|
|
77
111
|
}
|
|
78
112
|
res.json({ repos });
|
|
79
113
|
});
|
|
114
|
+
// Every topic in use, with how many repositories carry each: only what the
|
|
115
|
+
// caller could see listed anyway, counted rather than repeated.
|
|
116
|
+
app.get('/api/topics', (req, res) => {
|
|
117
|
+
const auth = (0, auth_1.requireApiAuth)(root, limiter, req, res);
|
|
118
|
+
if (!auth)
|
|
119
|
+
return;
|
|
120
|
+
const lists = [];
|
|
121
|
+
for (const c of (0, scan_1.listCollections)(root)) {
|
|
122
|
+
for (const dirName of (0, scan_1.listRepoDirs)(root, c.name)) {
|
|
123
|
+
const dir = (0, layout_1.repoPath)(root, c.name, dirName);
|
|
124
|
+
if ((0, perms_1.repoRole)(root, auth, { collection: c.name, name: (0, scan_1.displayName)(dirName), dir }) === null)
|
|
125
|
+
continue;
|
|
126
|
+
lists.push((0, topics_1.repoTopics)(dir));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
res.json({ topics: (0, topics_1.countTopics)(lists) });
|
|
130
|
+
});
|
|
80
131
|
app.get('/api/repos/:collection/:repo', async (req, res) => {
|
|
81
132
|
const found = (0, auth_1.requireRepo)(root, limiter, req, res);
|
|
82
133
|
if (!found)
|
|
@@ -98,6 +149,7 @@ function registerRepoApi(app, root, limiter) {
|
|
|
98
149
|
closedIssues: issues.closed,
|
|
99
150
|
openPulls: pulls.open,
|
|
100
151
|
closedPulls: pulls.closed,
|
|
152
|
+
site: siteInfo(root, req, repo),
|
|
101
153
|
// What this token may do here, so that a caller need not discover it by
|
|
102
154
|
// being refused.
|
|
103
155
|
role,
|
|
@@ -124,9 +176,13 @@ function registerRepoApi(app, root, limiter) {
|
|
|
124
176
|
const found = (0, auth_1.requireRepo)(root, limiter, req, res);
|
|
125
177
|
if (!found)
|
|
126
178
|
return;
|
|
179
|
+
const info = siteInfo(root, req, found.repo);
|
|
127
180
|
const dir = (0, scan_1.siteDir)(root, found.repo.collection, found.repo.name);
|
|
181
|
+
// `exists` says whether the site is being served, which takes both the
|
|
182
|
+
// switch and the files; the settings are reported either way, and so is
|
|
183
|
+
// what is on disk, since a disabled site keeps its directory.
|
|
128
184
|
if (!dir) {
|
|
129
|
-
res.json({ exists: false });
|
|
185
|
+
res.json({ ...info, exists: false });
|
|
130
186
|
return;
|
|
131
187
|
}
|
|
132
188
|
let entries = 0;
|
|
@@ -146,7 +202,7 @@ function registerRepoApi(app, root, limiter) {
|
|
|
146
202
|
catch {
|
|
147
203
|
// An unreadable site directory reports what it can rather than failing.
|
|
148
204
|
}
|
|
149
|
-
res.json({ exists:
|
|
205
|
+
res.json({ ...info, exists: info.enabled, entries, updated });
|
|
150
206
|
});
|
|
151
207
|
// A repository name that is not two path segments is a caller's mistake rather
|
|
152
208
|
// than a missing repository, and saying so is cheaper than letting it 404.
|
package/dist/api/write.js
CHANGED
|
@@ -5,7 +5,12 @@ const trigger_1 = require("../ci/trigger");
|
|
|
5
5
|
const git_1 = require("../git");
|
|
6
6
|
const ops_1 = require("../ops");
|
|
7
7
|
const perms_1 = require("../perms");
|
|
8
|
+
const domains_1 = require("../domains");
|
|
8
9
|
const scan_1 = require("../scan");
|
|
10
|
+
const sitesettings_1 = require("../sitesettings");
|
|
11
|
+
const sitepublish_1 = require("../sitepublish");
|
|
12
|
+
const siteshost_1 = require("../siteshost");
|
|
13
|
+
const topics_1 = require("../topics");
|
|
9
14
|
const vault_1 = require("../vault");
|
|
10
15
|
const auth_1 = require("./auth");
|
|
11
16
|
function writeBody(body, fallbackMessage) {
|
|
@@ -13,7 +18,7 @@ function writeBody(body, fallbackMessage) {
|
|
|
13
18
|
if (branch === null || !(0, git_1.isValidRefName)(branch))
|
|
14
19
|
return '"branch" is required and must be a usable branch name';
|
|
15
20
|
const newBranch = (0, auth_1.stringField)(body, 'newBranch');
|
|
16
|
-
if (newBranch !== null && !(0, git_1.
|
|
21
|
+
if (newBranch !== null && !(0, git_1.isValidNewRefName)(newBranch))
|
|
17
22
|
return '"newBranch" is not a usable branch name';
|
|
18
23
|
const expectedSha = (0, auth_1.stringField)(body, 'expectedSha');
|
|
19
24
|
return {
|
|
@@ -381,35 +386,131 @@ function registerWriteApi(app, root, limiter, lfs = null, engine) {
|
|
|
381
386
|
const body = (0, auth_1.bodyOf)(req);
|
|
382
387
|
const description = (0, auth_1.stringField)(body, 'description');
|
|
383
388
|
const defaultBranch = (0, auth_1.stringField)(body, 'defaultBranch');
|
|
389
|
+
const upstream = (0, auth_1.stringField)(body, 'upstream');
|
|
390
|
+
const topics = (0, auth_1.stringsField)(body, 'topics');
|
|
391
|
+
if (topics === null) {
|
|
392
|
+
(0, auth_1.apiError)(res, 400, '"topics" must be a list of strings');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
384
395
|
const priv = body.private;
|
|
385
396
|
if (priv !== undefined && typeof priv !== 'boolean') {
|
|
386
397
|
(0, auth_1.apiError)(res, 400, '"private" must be a boolean');
|
|
387
398
|
return;
|
|
388
399
|
}
|
|
389
|
-
|
|
390
|
-
|
|
400
|
+
const siteEnabled = body.siteEnabled;
|
|
401
|
+
if (siteEnabled !== undefined && typeof siteEnabled !== 'boolean') {
|
|
402
|
+
(0, auth_1.apiError)(res, 400, '"siteEnabled" must be a boolean');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const siteSource = body.siteSource;
|
|
406
|
+
if (siteSource !== undefined && !(0, sitesettings_1.isSiteSource)(siteSource)) {
|
|
407
|
+
(0, auth_1.apiError)(res, 400, '"siteSource" must be "copy", "actions", or "repository"');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const sitePathRaw = (0, auth_1.stringField)(body, 'sitePath');
|
|
411
|
+
const sitePath = sitePathRaw === null ? null : (0, sitesettings_1.normalizeSitePath)(sitePathRaw);
|
|
412
|
+
if (sitePath !== null && !(0, sitesettings_1.isUsableSitePath)(sitePath)) {
|
|
413
|
+
(0, auth_1.apiError)(res, 400, '"sitePath" must be a relative directory within the tree, such as "docs", or "" for the root');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const siteLabel = (0, auth_1.stringField)(body, 'siteLabel');
|
|
417
|
+
if (siteLabel !== null && siteLabel !== '' && (0, siteshost_1.isReservedSiteLabel)(siteLabel)) {
|
|
418
|
+
(0, auth_1.apiError)(res, 409, `the label ${siteLabel} is reserved for the operator of this vault`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (siteLabel !== null && siteLabel !== '' && !(0, sitesettings_1.isUsableSiteLabel)(siteLabel)) {
|
|
422
|
+
(0, auth_1.apiError)(res, 400, 'a site label is lowercase letters, digits, and single interior hyphens, at most 63 characters');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const siteDomain = (0, auth_1.stringField)(body, 'siteDomain');
|
|
426
|
+
const changingSite = siteEnabled !== undefined || siteSource !== undefined || siteLabel !== null || sitePath !== null;
|
|
427
|
+
if (description === null &&
|
|
428
|
+
defaultBranch === null &&
|
|
429
|
+
upstream === null &&
|
|
430
|
+
topics === undefined &&
|
|
431
|
+
priv === undefined &&
|
|
432
|
+
!changingSite &&
|
|
433
|
+
siteDomain === null) {
|
|
434
|
+
(0, auth_1.apiError)(res, 400, 'nothing to change; provide "description", "defaultBranch", "upstream", "topics", "private", "siteEnabled", "siteSource", "sitePath", "siteLabel", and/or "siteDomain"');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// Visibility takes the admin role rather than write: publishing a
|
|
438
|
+
// repository, or withdrawing one from everybody who could see it, is the
|
|
439
|
+
// repository's own business the way deleting it is. The site settings are
|
|
440
|
+
// on the same footing, since enabling a site publishes what the directory
|
|
441
|
+
// holds to everyone.
|
|
442
|
+
if ((priv !== undefined || changingSite) && !(0, perms_1.canAdminRepo)(root, ctx.auth, ctx.repo)) {
|
|
443
|
+
(0, auth_1.apiError)(res, 403, `changing visibility or site settings needs the admin role on ${ctx.repo.collection}/${ctx.repo.name}`);
|
|
391
444
|
return;
|
|
392
445
|
}
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
if (
|
|
397
|
-
(0, auth_1.apiError)(res, 403,
|
|
446
|
+
// A custom domain is the operator's act: the operator points DNS at the
|
|
447
|
+
// vault and covers the name with a certificate, so attaching one takes a
|
|
448
|
+
// site admin, not merely the repository's.
|
|
449
|
+
if (siteDomain !== null && !(0, perms_1.isSiteAdmin)(ctx.auth)) {
|
|
450
|
+
(0, auth_1.apiError)(res, 403, 'attaching a custom domain takes a site admin');
|
|
398
451
|
return;
|
|
399
452
|
}
|
|
453
|
+
if (siteLabel !== null && siteLabel !== '') {
|
|
454
|
+
const holder = (0, sitesettings_1.siteLabelConflict)(root, siteLabel, ctx.repo.collection, ctx.repo.name);
|
|
455
|
+
if (holder) {
|
|
456
|
+
(0, auth_1.apiError)(res, 409, `the label ${siteLabel} is already used by ${holder}`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (siteDomain !== null && siteDomain !== '') {
|
|
461
|
+
const problem = (0, domains_1.setRepoDomain)(root, ctx.repo.collection, ctx.repo.name, siteDomain);
|
|
462
|
+
if (problem) {
|
|
463
|
+
(0, auth_1.apiError)(res, problem.kind === 'conflict' ? 409 : 400, problem.message);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (siteDomain === '')
|
|
468
|
+
(0, domains_1.clearRepoDomain)(root, ctx.repo.collection, ctx.repo.name);
|
|
400
469
|
try {
|
|
401
470
|
if (description !== null)
|
|
402
471
|
(0, ops_1.setDescription)(ctx.repo.dir, description);
|
|
403
472
|
if (defaultBranch !== null)
|
|
404
473
|
await (0, ops_1.setDefaultBranch)(ctx.repo.dir, defaultBranch);
|
|
474
|
+
if (upstream !== null)
|
|
475
|
+
await (0, ops_1.setUpstream)(ctx.repo.dir, upstream);
|
|
476
|
+
if (topics !== undefined)
|
|
477
|
+
(0, topics_1.setTopics)(ctx.repo.dir, topics);
|
|
405
478
|
if (priv !== undefined)
|
|
406
479
|
(0, perms_1.setRepoPrivate)(ctx.repo.dir, priv);
|
|
480
|
+
if (changingSite) {
|
|
481
|
+
(0, sitesettings_1.editSiteSettings)(ctx.repo.dir, (s) => {
|
|
482
|
+
if (siteEnabled !== undefined)
|
|
483
|
+
s.enabled = siteEnabled;
|
|
484
|
+
if (siteSource !== undefined)
|
|
485
|
+
s.source = siteSource;
|
|
486
|
+
if (siteLabel !== null)
|
|
487
|
+
s.label = siteLabel;
|
|
488
|
+
if (sitePath !== null)
|
|
489
|
+
s.path = sitePath;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
407
492
|
const branches = await ctx.repo.listRefs('heads');
|
|
493
|
+
const settings = (0, sitesettings_1.siteSettings)(ctx.repo.dir);
|
|
494
|
+
// A site published from the repository is rewritten whenever what it
|
|
495
|
+
// should hold may have changed: the site settings, or the branch it
|
|
496
|
+
// follows. The publish's outcome rides in the response rather than
|
|
497
|
+
// failing it, since the settings themselves were saved.
|
|
498
|
+
const sitePublish = changingSite || defaultBranch !== null ? await (0, sitepublish_1.publishSiteForSettings)(root, ctx.repo) : null;
|
|
408
499
|
res.json({
|
|
409
500
|
collection: ctx.repo.collection,
|
|
410
501
|
name: ctx.repo.name,
|
|
411
502
|
defaultBranch: await ctx.repo.defaultBranch(branches),
|
|
503
|
+
upstream: (0, scan_1.upstreamOf)(ctx.repo.dir)?.url ?? null,
|
|
504
|
+
topics: (0, topics_1.repoTopics)(ctx.repo.dir),
|
|
412
505
|
private: (0, perms_1.repoIsPrivate)(ctx.repo.dir),
|
|
506
|
+
site: {
|
|
507
|
+
enabled: settings.enabled,
|
|
508
|
+
source: settings.source,
|
|
509
|
+
path: settings.path,
|
|
510
|
+
label: settings.label === '' ? null : settings.label,
|
|
511
|
+
domain: (0, domains_1.repoDomain)(root, ctx.repo.collection, ctx.repo.name),
|
|
512
|
+
},
|
|
513
|
+
sitePublish,
|
|
413
514
|
changed: true,
|
|
414
515
|
});
|
|
415
516
|
}
|
|
@@ -446,6 +547,35 @@ function registerWriteApi(app, root, limiter, lfs = null, engine) {
|
|
|
446
547
|
(0, auth_1.sendOpError)(res, e, 'could not rename the repository');
|
|
447
548
|
}
|
|
448
549
|
});
|
|
550
|
+
// Collecting a repository now, rather than waiting for the sweep in
|
|
551
|
+
// src/maintenance.ts to reach it. It takes the admin role and the same typed
|
|
552
|
+
// confirmation deletion takes, because what it removes is unrecoverable:
|
|
553
|
+
// everything no ref can reach, which is exactly what a person who has just
|
|
554
|
+
// rewritten a history away is asking for, and exactly what someone who
|
|
555
|
+
// mistook this for a repack is not.
|
|
556
|
+
app.post('/api/repos/:collection/:repo/gc', async (req, res) => {
|
|
557
|
+
const ctx = (0, auth_1.requireRepoAdmin)(root, limiter, req, res);
|
|
558
|
+
if (!ctx)
|
|
559
|
+
return;
|
|
560
|
+
const full = `${ctx.repo.collection}/${ctx.repo.name}`;
|
|
561
|
+
if (String(req.query.confirm ?? '') !== full) {
|
|
562
|
+
(0, auth_1.apiError)(res, 400, `to drop every unreachable object in this repository, send ?confirm=${full}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const { before, after } = await (0, ops_1.collectRepo)(ctx.repo.dir);
|
|
567
|
+
res.json({
|
|
568
|
+
repo: full,
|
|
569
|
+
before,
|
|
570
|
+
after,
|
|
571
|
+
removed: Math.max(0, before.objects - after.objects),
|
|
572
|
+
kilobytesFreed: Math.max(0, before.kilobytes - after.kilobytes),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
catch (e) {
|
|
576
|
+
(0, auth_1.sendOpError)(res, e, 'could not collect the repository');
|
|
577
|
+
}
|
|
578
|
+
});
|
|
449
579
|
// ?confirm= is the API's equivalent of the web's typed confirmation. It costs
|
|
450
580
|
// nothing and it makes an accidental DELETE from a loop over a listing
|
|
451
581
|
// impossible.
|
package/dist/api.js
CHANGED
|
@@ -46,12 +46,15 @@ const perms_1 = require("./perms");
|
|
|
46
46
|
const scan_1 = require("./scan");
|
|
47
47
|
const vault_1 = require("./vault");
|
|
48
48
|
const auth_1 = require("./api/auth");
|
|
49
|
+
const logincodes_1 = require("./logincodes");
|
|
49
50
|
const contents_1 = require("./api/contents");
|
|
50
51
|
const issues_1 = require("./api/issues");
|
|
51
52
|
const pulls_1 = require("./api/pulls");
|
|
52
53
|
const repos_1 = require("./api/repos");
|
|
53
54
|
const admin_1 = require("./api/admin");
|
|
54
55
|
const backup_1 = require("./api/backup");
|
|
56
|
+
const sitesettings_1 = require("./sitesettings");
|
|
57
|
+
const config_1 = require("./config");
|
|
55
58
|
const ci_1 = require("./api/ci");
|
|
56
59
|
const releases_1 = require("./api/releases");
|
|
57
60
|
const write_1 = require("./api/write");
|
|
@@ -99,6 +102,24 @@ function registerApi(app, root, authLimiter, gates, lfs = null, engine, egress)
|
|
|
99
102
|
name: (0, scan_1.displayName)(dirName),
|
|
100
103
|
dir: (0, layout_1.repoPath)(root, collection, dirName),
|
|
101
104
|
}) !== null);
|
|
105
|
+
// A one-time sign-in URL for the browser, which is how `mochi web` opens
|
|
106
|
+
// the vault already signed in: the CLI proves the token over the API, and
|
|
107
|
+
// the session the link starts is bound to that same token, so revoking it
|
|
108
|
+
// ends both. The link lands on a page that names the account and asks for a
|
|
109
|
+
// click; see /login/code/:code in src/webops.ts.
|
|
110
|
+
app.post('/api/login-url', (req, res) => {
|
|
111
|
+
const auth = requireApiAuth(req, res);
|
|
112
|
+
if (!auth)
|
|
113
|
+
return;
|
|
114
|
+
const body = (req.body ?? {});
|
|
115
|
+
const next = typeof body.next === 'string' ? body.next : '/';
|
|
116
|
+
const code = (0, logincodes_1.mintLoginLink)(root, auth.username, auth.token.hash, next);
|
|
117
|
+
res.json({
|
|
118
|
+
url: `${req.protocol}://${req.get('host')}/login/code/${code}`,
|
|
119
|
+
username: auth.username,
|
|
120
|
+
expiresInSeconds: Math.round(logincodes_1.LOGIN_LINK_TTL_MS / 1000),
|
|
121
|
+
});
|
|
122
|
+
});
|
|
102
123
|
app.get('/api/whoami', (req, res) => {
|
|
103
124
|
const auth = requireApiAuth(req, res);
|
|
104
125
|
if (!auth)
|
|
@@ -140,7 +161,18 @@ function registerApi(app, root, authLimiter, gates, lfs = null, engine, egress)
|
|
|
140
161
|
(0, auth_1.apiError)(res, 404, `no collection ${name} in this vault`);
|
|
141
162
|
return;
|
|
142
163
|
}
|
|
143
|
-
|
|
164
|
+
// siteAlias is what is stored and siteHostAlias what is in effect, the two
|
|
165
|
+
// differing wherever a tier below the stored one is answering; see the
|
|
166
|
+
// tiers in src/sitesettings.ts. Both are null on a vault with no sites
|
|
167
|
+
// host, where a derived hostname would mean nothing.
|
|
168
|
+
const sitesHost = (0, config_1.loadConfig)(root).sites.host;
|
|
169
|
+
res.json({
|
|
170
|
+
name,
|
|
171
|
+
owners: (0, perms_1.collectionOwners)(root, name),
|
|
172
|
+
repos: visibleRepos(auth, name).map(scan_1.displayName),
|
|
173
|
+
siteAlias: sitesHost ? (0, sitesettings_1.storedCollectionAlias)(root, name) : null,
|
|
174
|
+
siteHostAlias: sitesHost ? (0, sitesettings_1.collectionSiteAlias)(root, name) : null,
|
|
175
|
+
});
|
|
144
176
|
});
|
|
145
177
|
app.post('/api/collections', (req, res) => {
|
|
146
178
|
const auth = requireApiAuth(req, res);
|
|
@@ -287,6 +319,7 @@ function registerApi(app, root, authLimiter, gates, lfs = null, engine, egress)
|
|
|
287
319
|
const result = (0, vault_1.addUserToken)(root, username, {
|
|
288
320
|
siteAdmin: body.siteAdmin === true,
|
|
289
321
|
tokenScope: tokenScope ?? undefined,
|
|
322
|
+
by: auth.username,
|
|
290
323
|
});
|
|
291
324
|
res.json({
|
|
292
325
|
username,
|