@magland/mochi 0.3.1 → 0.4.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 (83) hide show
  1. package/README.md +7 -6
  2. package/dist/agefile.js +47 -0
  3. package/dist/agescript.js +343 -0
  4. package/dist/api/admin.js +66 -28
  5. package/dist/api/auth.js +3 -2
  6. package/dist/api/backup.js +227 -192
  7. package/dist/api/contents.js +46 -16
  8. package/dist/api/repos.js +60 -4
  9. package/dist/api/write.js +138 -8
  10. package/dist/api.js +34 -1
  11. package/dist/browse.js +171 -33
  12. package/dist/build-info.json +2 -2
  13. package/dist/ci/api.js +138 -4
  14. package/dist/ci/artifacts.js +82 -2
  15. package/dist/ci/engine.js +30 -17
  16. package/dist/ci/expr.js +14 -0
  17. package/dist/ci/runners.js +60 -1
  18. package/dist/ci/runs.js +21 -3
  19. package/dist/ci/trigger.js +6 -4
  20. package/dist/ci/views.js +24 -3
  21. package/dist/ci/web.js +48 -0
  22. package/dist/cli/admin-cmd.js +40 -3
  23. package/dist/cli/backup-cmd.js +166 -120
  24. package/dist/cli/gitrun.js +50 -0
  25. package/dist/cli/pr-cmd.js +155 -0
  26. package/dist/cli/repo-cmd.js +134 -6
  27. package/dist/cli/sync-cmd.js +128 -0
  28. package/dist/cli/target.js +7 -3
  29. package/dist/config.js +10 -0
  30. package/dist/credentials.js +9 -8
  31. package/dist/deploy-cli.js +214 -109
  32. package/dist/deploy-runner-cli.js +125 -49
  33. package/dist/domains.js +259 -0
  34. package/dist/egress.js +23 -6
  35. package/dist/forms.js +703 -32
  36. package/dist/git.js +124 -13
  37. package/dist/githttp.js +80 -23
  38. package/dist/githubauth.js +185 -0
  39. package/dist/icons.js +14 -0
  40. package/dist/import-cli.js +40 -25
  41. package/dist/index.js +110 -4
  42. package/dist/issueweb.js +10 -6
  43. package/dist/lfs.js +9 -2
  44. package/dist/limit.js +2 -2
  45. package/dist/logincodes.js +31 -0
  46. package/dist/maintenance.js +297 -0
  47. package/dist/markdown.js +193 -3
  48. package/dist/mdedit.js +29 -0
  49. package/dist/migrate.js +48 -0
  50. package/dist/naming.js +25 -0
  51. package/dist/onetime.js +112 -0
  52. package/dist/ops.js +293 -16
  53. package/dist/pagescript.js +355 -24
  54. package/dist/perms.js +1 -0
  55. package/dist/profile.js +2 -0
  56. package/dist/pulls.js +21 -1
  57. package/dist/pullweb.js +25 -6
  58. package/dist/releases.js +10 -1
  59. package/dist/runner/actions.js +12 -1
  60. package/dist/runner/client.js +40 -2
  61. package/dist/runner/docker.js +40 -28
  62. package/dist/runner/download.js +77 -0
  63. package/dist/runner/externals.js +12 -1
  64. package/dist/runner/job.js +26 -11
  65. package/dist/runner/steps.js +42 -7
  66. package/dist/runner-cli.js +89 -0
  67. package/dist/scan.js +58 -2
  68. package/dist/server.js +107 -26
  69. package/dist/session.js +82 -40
  70. package/dist/site.js +110 -27
  71. package/dist/sitepublish.js +177 -0
  72. package/dist/sitesettings.js +420 -0
  73. package/dist/siteshost.js +116 -28
  74. package/dist/source.js +20 -0
  75. package/dist/style.js +162 -3
  76. package/dist/topics.js +165 -0
  77. package/dist/vault.js +399 -6
  78. package/dist/vendor-age.js +15 -0
  79. package/dist/views.js +242 -57
  80. package/dist/web.js +11 -1
  81. package/dist/webauthn.js +347 -0
  82. package/dist/webops.js +1263 -33
  83. 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
- hasSite: (0, scan_1.siteDir)(root, collection, name) !== null,
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: true, entries, updated });
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.isValidRefName)(newBranch))
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
- if (description === null && defaultBranch === null && priv === undefined) {
390
- (0, auth_1.apiError)(res, 400, 'nothing to change; provide "description", "defaultBranch", and/or "private"');
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
- // Visibility is the one setting here that takes the admin role rather than
394
- // write: publishing a repository, or withdrawing one from everybody who
395
- // could see it, is the repository's own business the way deleting it is.
396
- if (priv !== undefined && !(0, perms_1.canAdminRepo)(root, ctx.auth, ctx.repo)) {
397
- (0, auth_1.apiError)(res, 403, `changing visibility needs the admin role on ${ctx.repo.collection}/${ctx.repo.name}`);
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
- res.json({ name, owners: (0, perms_1.collectionOwners)(root, name), repos: visibleRepos(auth, name).map(scan_1.displayName) });
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,