@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.
- 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 +166 -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 +193 -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
|
@@ -0,0 +1,420 @@
|
|
|
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.COLLECTION_SITE_FILE = exports.SITE_SETTINGS_FILE = exports.SITE_SOURCES = void 0;
|
|
37
|
+
exports.isSiteSource = isSiteSource;
|
|
38
|
+
exports.isUsableSitePath = isUsableSitePath;
|
|
39
|
+
exports.normalizeSitePath = normalizeSitePath;
|
|
40
|
+
exports.isUsableSiteLabel = isUsableSiteLabel;
|
|
41
|
+
exports.siteSettings = siteSettings;
|
|
42
|
+
exports.editSiteSettings = editSiteSettings;
|
|
43
|
+
exports.findRepoBySiteLabel = findRepoBySiteLabel;
|
|
44
|
+
exports.siteLabelConflict = siteLabelConflict;
|
|
45
|
+
exports.listSiteLabels = listSiteLabels;
|
|
46
|
+
exports.isUsableCollectionAlias = isUsableCollectionAlias;
|
|
47
|
+
exports.storedCollectionAlias = storedCollectionAlias;
|
|
48
|
+
exports.setCollectionAlias = setCollectionAlias;
|
|
49
|
+
exports.collectionSiteAliases = collectionSiteAliases;
|
|
50
|
+
exports.collectionSiteAlias = collectionSiteAlias;
|
|
51
|
+
exports.collectionForSiteAlias = collectionForSiteAlias;
|
|
52
|
+
exports.collectionAliasConflict = collectionAliasConflict;
|
|
53
|
+
exports.siteHostFor = siteHostFor;
|
|
54
|
+
exports.findRepoBySiteHostname = findRepoBySiteHostname;
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const atomic_1 = require("./atomic");
|
|
58
|
+
const filecache_1 = require("./filecache");
|
|
59
|
+
const layout_1 = require("./layout");
|
|
60
|
+
const scan_1 = require("./scan");
|
|
61
|
+
const siteshost_1 = require("./siteshost");
|
|
62
|
+
exports.SITE_SOURCES = ['copy', 'actions', 'repository'];
|
|
63
|
+
function isSiteSource(value) {
|
|
64
|
+
return typeof value === 'string' && exports.SITE_SOURCES.includes(value);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether a string may be stored as the directory a 'repository' site is
|
|
68
|
+
* published from: empty for the root, or relative path segments of ordinary
|
|
69
|
+
* filename characters, so `docs` and `build/site` but never `..`, an absolute
|
|
70
|
+
* path, or a segment that starts with a dot or a dash. The path is later handed to
|
|
71
|
+
* `git archive` as `<branch>:<path>`, and this grammar is what keeps it a
|
|
72
|
+
* tree path and nothing git would read as an option or a revision.
|
|
73
|
+
*/
|
|
74
|
+
function isUsableSitePath(value) {
|
|
75
|
+
if (value === '')
|
|
76
|
+
return true;
|
|
77
|
+
if (value.length > 200)
|
|
78
|
+
return false;
|
|
79
|
+
return /^[A-Za-z0-9_][A-Za-z0-9._-]*(\/[A-Za-z0-9_][A-Za-z0-9._-]*)*$/.test(value);
|
|
80
|
+
}
|
|
81
|
+
/** A directory field as typed by a person, normalized to what isUsableSitePath accepts. */
|
|
82
|
+
function normalizeSitePath(value) {
|
|
83
|
+
return value.trim().replace(/^\/+|\/+$/g, '').replace(/\/{2,}/g, '/');
|
|
84
|
+
}
|
|
85
|
+
exports.SITE_SETTINGS_FILE = 'site.json';
|
|
86
|
+
/**
|
|
87
|
+
* Whether a string may be stored as a custom site label: a single DNS label,
|
|
88
|
+
* never with a double hyphen, and not one of the names the operator keeps. The
|
|
89
|
+
* reserved check is here rather than in the grammar because it is a question
|
|
90
|
+
* about who may claim a name, not about what a hostname may look like: a
|
|
91
|
+
* request arriving for `www.<sites host>` is parsed as usual and then answers
|
|
92
|
+
* to nothing, which is what makes the name free for the operator to point
|
|
93
|
+
* elsewhere.
|
|
94
|
+
*/
|
|
95
|
+
function isUsableSiteLabel(label) {
|
|
96
|
+
return ((0, siteshost_1.isSiteLabelSafe)(label) &&
|
|
97
|
+
!label.includes('--') &&
|
|
98
|
+
label.length <= siteshost_1.MAX_SITE_LABEL &&
|
|
99
|
+
!(0, siteshost_1.isReservedSiteLabel)(label));
|
|
100
|
+
}
|
|
101
|
+
function defaults() {
|
|
102
|
+
return { enabled: false, source: 'copy', label: '', path: '' };
|
|
103
|
+
}
|
|
104
|
+
function normalizeSiteSettings(parsed) {
|
|
105
|
+
const out = defaults();
|
|
106
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
107
|
+
return out;
|
|
108
|
+
const rec = parsed;
|
|
109
|
+
if (rec.enabled === true)
|
|
110
|
+
out.enabled = true;
|
|
111
|
+
if (rec.source === 'actions' || rec.source === 'repository')
|
|
112
|
+
out.source = rec.source;
|
|
113
|
+
if (typeof rec.label === 'string' && isUsableSiteLabel(rec.label))
|
|
114
|
+
out.label = rec.label;
|
|
115
|
+
if (typeof rec.path === 'string' && isUsableSitePath(rec.path))
|
|
116
|
+
out.path = rec.path;
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const settingsCache = (0, filecache_1.fileCache)({
|
|
120
|
+
read: (file) => {
|
|
121
|
+
try {
|
|
122
|
+
return normalizeSiteSettings(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return defaults();
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
missing: () => defaults(),
|
|
129
|
+
});
|
|
130
|
+
/** The site settings of the repository whose bare directory this is. */
|
|
131
|
+
function siteSettings(repoDir) {
|
|
132
|
+
return settingsCache.get(path.join(repoDir, exports.SITE_SETTINGS_FILE));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read-modify-write on a repository's site settings, under a lock, the same
|
|
136
|
+
* arrangement mochi.json edits use in src/perms.ts. The cache is invalidated
|
|
137
|
+
* explicitly because a write landing within the mtime granularity would
|
|
138
|
+
* otherwise be invisible to it.
|
|
139
|
+
*/
|
|
140
|
+
function editSiteSettings(repoDir, fn) {
|
|
141
|
+
const file = path.join(repoDir, exports.SITE_SETTINGS_FILE);
|
|
142
|
+
return (0, atomic_1.withFileLock)(`${file}.lock`, () => {
|
|
143
|
+
let settings = defaults();
|
|
144
|
+
if (fs.existsSync(file)) {
|
|
145
|
+
try {
|
|
146
|
+
settings = normalizeSiteSettings(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// An unreadable file is edited as the defaults it already reads as.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
fn(settings);
|
|
153
|
+
(0, atomic_1.writeFileAtomic)(file, JSON.stringify(settings, null, 2) + '\n');
|
|
154
|
+
settingsCache.invalidate(file);
|
|
155
|
+
return settings;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The repository whose custom label this is, or null. A scan rather than an
|
|
160
|
+
* index, like every other lookup over a vault's repositories: the answer is a
|
|
161
|
+
* stat-cached read per repository, and it is only asked for a hostname that is
|
|
162
|
+
* under the sites host but is not a derived <repo>--<alias> name.
|
|
163
|
+
*/
|
|
164
|
+
function findRepoBySiteLabel(root, label) {
|
|
165
|
+
if (!isUsableSiteLabel(label))
|
|
166
|
+
return null;
|
|
167
|
+
for (const { name: collection } of (0, scan_1.listCollections)(root)) {
|
|
168
|
+
for (const dirName of (0, scan_1.listRepoDirs)(root, collection)) {
|
|
169
|
+
if (siteSettings((0, layout_1.repoPath)(root, collection, dirName)).label === label) {
|
|
170
|
+
return { collection, repo: (0, scan_1.displayName)(dirName) };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The `<collection>/<repo>` already using a label, or null when the label is
|
|
178
|
+
* free. Asked where a label is being set; the repository setting it does not
|
|
179
|
+
* conflict with itself. A label cannot collide with any derived label, since a
|
|
180
|
+
* derived label always contains `--` and a custom one never may.
|
|
181
|
+
*/
|
|
182
|
+
function siteLabelConflict(root, label, collection, repo) {
|
|
183
|
+
const holder = findRepoBySiteLabel(root, label);
|
|
184
|
+
if (!holder)
|
|
185
|
+
return null;
|
|
186
|
+
if (holder.collection === collection && holder.repo === repo)
|
|
187
|
+
return null;
|
|
188
|
+
return `${holder.collection}/${holder.repo}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Every custom label in the vault, with the repository holding it, sorted by
|
|
192
|
+
* label. The same scan findRepoBySiteLabel does, kept apart from it because
|
|
193
|
+
* this one answers a different question: what is already taken. Nothing on a
|
|
194
|
+
* request path asks it; the vault settings page does, so that the flat
|
|
195
|
+
* namespace of claimed labels can be read rather than discovered one 409 at a
|
|
196
|
+
* time.
|
|
197
|
+
*/
|
|
198
|
+
function listSiteLabels(root) {
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const { name: collection } of (0, scan_1.listCollections)(root)) {
|
|
201
|
+
for (const dirName of (0, scan_1.listRepoDirs)(root, collection)) {
|
|
202
|
+
const label = siteSettings((0, layout_1.repoPath)(root, collection, dirName)).label;
|
|
203
|
+
if (label)
|
|
204
|
+
out.push({ label, collection, repo: (0, scan_1.displayName)(dirName) });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return out.sort((a, b) => a.label.localeCompare(b.label));
|
|
208
|
+
}
|
|
209
|
+
// ---- the collection half: collections/<name>/site.json ----
|
|
210
|
+
//
|
|
211
|
+
// A derived site hostname is `<repo>--<alias>`, and the alias is not always the
|
|
212
|
+
// collection's name. Collection names are as permissive as repository names, so
|
|
213
|
+
// a collection called `simulated_instruments` has no name a hostname label may
|
|
214
|
+
// carry, and every repository in it would have been left with no derived
|
|
215
|
+
// hostname at all: the tier that exists precisely to be the guaranteed one.
|
|
216
|
+
// Claiming a custom label was the only way out, which put the guaranteed name
|
|
217
|
+
// behind a first-come namespace.
|
|
218
|
+
//
|
|
219
|
+
// So each collection has a site alias, decided in three tiers:
|
|
220
|
+
//
|
|
221
|
+
// 1. the alias stored in collections/<name>/site.json, when one is set. It is
|
|
222
|
+
// checked against every other collection where it is written, so it is
|
|
223
|
+
// unique by construction.
|
|
224
|
+
// 2. otherwise the collection's own name, when that name is already a usable
|
|
225
|
+
// label. This tier can never be taken away by anything: a vault serving
|
|
226
|
+
// `webapp--alice` keeps serving it whatever else is created.
|
|
227
|
+
// 3. otherwise the name rewritten as a label (`simulated-instruments`), and
|
|
228
|
+
// only when no other collection holds that label at any tier. The rewrite
|
|
229
|
+
// is not injective, so `a_b` and `a.b` would both want `a-b`; neither gets
|
|
230
|
+
// it, symmetrically, rather than one silently winning. An owner breaks the
|
|
231
|
+
// tie by storing an alias at tier 1.
|
|
232
|
+
//
|
|
233
|
+
// A separate file rather than a key in collection.json: that file is written
|
|
234
|
+
// through a normalizer that keeps only the owners it knows, so an alias stored
|
|
235
|
+
// there would be dropped by the next owner change. It is the same reason the
|
|
236
|
+
// per-repository settings above are not in mochi.json.
|
|
237
|
+
exports.COLLECTION_SITE_FILE = 'site.json';
|
|
238
|
+
/** Whether a string may be stored as a collection's site alias. */
|
|
239
|
+
function isUsableCollectionAlias(alias) {
|
|
240
|
+
return (0, siteshost_1.isSiteLabelSafe)(alias) && !alias.includes('--') && alias.length <= siteshost_1.MAX_SITE_LABEL;
|
|
241
|
+
}
|
|
242
|
+
function collectionSiteFile(root, collection) {
|
|
243
|
+
return path.join((0, layout_1.collectionDir)(root, collection), exports.COLLECTION_SITE_FILE);
|
|
244
|
+
}
|
|
245
|
+
function normalizeCollectionSite(parsed) {
|
|
246
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
247
|
+
return '';
|
|
248
|
+
const alias = parsed.alias;
|
|
249
|
+
return typeof alias === 'string' && isUsableCollectionAlias(alias) ? alias : '';
|
|
250
|
+
}
|
|
251
|
+
const aliasCache = (0, filecache_1.fileCache)({
|
|
252
|
+
read: (file) => {
|
|
253
|
+
try {
|
|
254
|
+
return normalizeCollectionSite(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// An unreadable file stores no alias, which leaves the collection on the
|
|
258
|
+
// tiers below rather than taking its derived hostnames away.
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
missing: () => '',
|
|
263
|
+
});
|
|
264
|
+
/** The alias stored for a collection, or '' when it is on a lower tier. */
|
|
265
|
+
function storedCollectionAlias(root, collection) {
|
|
266
|
+
return aliasCache.get(collectionSiteFile(root, collection));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Store a collection's site alias, or clear it with ''. Under a lock and with
|
|
270
|
+
* the cache invalidated, on the same terms as editSiteSettings above; the file
|
|
271
|
+
* is removed rather than left holding an empty alias, so a collection that has
|
|
272
|
+
* never had one and one that had it cleared look the same on disk. The caller
|
|
273
|
+
* checks collectionAliasConflict first: this writes what it is given.
|
|
274
|
+
*/
|
|
275
|
+
function setCollectionAlias(root, collection, alias) {
|
|
276
|
+
const file = collectionSiteFile(root, collection);
|
|
277
|
+
(0, atomic_1.withFileLock)(`${file}.lock`, () => {
|
|
278
|
+
if (alias === '')
|
|
279
|
+
fs.rmSync(file, { force: true });
|
|
280
|
+
else
|
|
281
|
+
(0, atomic_1.writeFileAtomic)(file, JSON.stringify({ alias }, null, 2) + '\n');
|
|
282
|
+
aliasCache.invalidate(file);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Every collection's site alias, resolved together because the tiers are
|
|
287
|
+
* decided against each other. Sorted by collection name, as listCollections
|
|
288
|
+
* returns them.
|
|
289
|
+
*/
|
|
290
|
+
function collectionSiteAliases(root) {
|
|
291
|
+
const rows = [];
|
|
292
|
+
// Tiers 1 and 2 first: both are unambiguous on their own, and they are what
|
|
293
|
+
// tier 3 has to give way to.
|
|
294
|
+
const claimed = new Map();
|
|
295
|
+
const pending = new Map();
|
|
296
|
+
for (const { name } of (0, scan_1.listCollections)(root)) {
|
|
297
|
+
const stored = storedCollectionAlias(root, name);
|
|
298
|
+
if (stored) {
|
|
299
|
+
rows.push({ collection: name, alias: stored, stored: true, taken: '' });
|
|
300
|
+
if (!claimed.has(stored))
|
|
301
|
+
claimed.set(stored, name);
|
|
302
|
+
}
|
|
303
|
+
else if ((0, siteshost_1.isSiteLabelSafe)(name)) {
|
|
304
|
+
rows.push({ collection: name, alias: name, stored: false, taken: '' });
|
|
305
|
+
if (!claimed.has(name))
|
|
306
|
+
claimed.set(name, name);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
rows.push({ collection: name, alias: null, stored: false, taken: '' });
|
|
310
|
+
const wanted = (0, siteshost_1.sanitizedSiteLabel)(name);
|
|
311
|
+
if (wanted)
|
|
312
|
+
pending.set(name, wanted);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Then tier 3, refused where the label is claimed above or wanted twice.
|
|
316
|
+
const wantedTwice = new Set();
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
for (const wanted of pending.values()) {
|
|
319
|
+
if (seen.has(wanted))
|
|
320
|
+
wantedTwice.add(wanted);
|
|
321
|
+
seen.add(wanted);
|
|
322
|
+
}
|
|
323
|
+
for (const row of rows) {
|
|
324
|
+
const wanted = pending.get(row.collection);
|
|
325
|
+
if (!wanted)
|
|
326
|
+
continue;
|
|
327
|
+
if (claimed.has(wanted) || wantedTwice.has(wanted))
|
|
328
|
+
row.taken = wanted;
|
|
329
|
+
else
|
|
330
|
+
row.alias = wanted;
|
|
331
|
+
}
|
|
332
|
+
return rows;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* One collection's site alias, or null when it has none and its repositories
|
|
336
|
+
* therefore have no derived hostname.
|
|
337
|
+
*
|
|
338
|
+
* The two common cases answer without listing the vault's collections at all,
|
|
339
|
+
* since tiers 1 and 2 are decided by the collection alone; only a name that
|
|
340
|
+
* needs rewriting pays for the full resolution.
|
|
341
|
+
*/
|
|
342
|
+
function collectionSiteAlias(root, collection) {
|
|
343
|
+
const stored = storedCollectionAlias(root, collection);
|
|
344
|
+
if (stored)
|
|
345
|
+
return stored;
|
|
346
|
+
if ((0, siteshost_1.isSiteLabelSafe)(collection))
|
|
347
|
+
return collection;
|
|
348
|
+
return collectionSiteAliases(root).find((r) => r.collection === collection)?.alias ?? null;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The collection a derived hostname's alias names, or null when no collection
|
|
352
|
+
* answers to it. The fast path is the same shortcut collectionSiteAlias takes:
|
|
353
|
+
* a collection named exactly the alias, with nothing stored, holds it at tier 2
|
|
354
|
+
* and nothing can have taken it.
|
|
355
|
+
*/
|
|
356
|
+
function collectionForSiteAlias(root, alias) {
|
|
357
|
+
if (!isUsableCollectionAlias(alias))
|
|
358
|
+
return null;
|
|
359
|
+
if ((0, scan_1.isValidName)(alias) && isCollectionDir(root, alias) && storedCollectionAlias(root, alias) === '')
|
|
360
|
+
return alias;
|
|
361
|
+
return collectionSiteAliases(root).find((r) => r.alias === alias)?.collection ?? null;
|
|
362
|
+
}
|
|
363
|
+
function isCollectionDir(root, collection) {
|
|
364
|
+
try {
|
|
365
|
+
return fs.statSync((0, layout_1.collectionDir)(root, collection)).isDirectory();
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The collection already holding an alias, or null when it is free. Asked where
|
|
373
|
+
* an alias is being stored; the collection storing it does not conflict with
|
|
374
|
+
* itself. Every tier is checked, so storing an alias can neither shadow a
|
|
375
|
+
* collection that is reached by its own name nor take a rewritten label from
|
|
376
|
+
* the collection using it.
|
|
377
|
+
*/
|
|
378
|
+
function collectionAliasConflict(root, alias, collection) {
|
|
379
|
+
const holder = collectionSiteAliases(root).find((r) => r.alias === alias && r.collection !== collection);
|
|
380
|
+
return holder ? holder.collection : null;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The full hostname a repository's site would be derived to, or null when the
|
|
384
|
+
* pair has no derived hostname: the naming rule and the vault's collections in
|
|
385
|
+
* one call, which is what every caller outside this file wants.
|
|
386
|
+
*/
|
|
387
|
+
function siteHostFor(root, sitesHost, collection, repo) {
|
|
388
|
+
if (!sitesHost)
|
|
389
|
+
return null;
|
|
390
|
+
const alias = collectionSiteAlias(root, collection);
|
|
391
|
+
return alias === null ? null : (0, siteshost_1.derivedSiteHost)(sitesHost, alias, repo);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The collection and repository a hostname under the sites host names, or
|
|
395
|
+
* null: the same question in the other direction, and the one both request
|
|
396
|
+
* paths that see such a hostname ask (serving the site, and counting its
|
|
397
|
+
* bytes).
|
|
398
|
+
*
|
|
399
|
+
* A label with a doubled hyphen is a derived name, and one without is a label
|
|
400
|
+
* some repository claimed; the two cannot collide, since a claimed label may
|
|
401
|
+
* never contain the separator. The names are returned as the hostname carries
|
|
402
|
+
* them, resolved to a repository that exists by the caller, as usual: an alias
|
|
403
|
+
* naming no collection is read as a collection name instead, which is what
|
|
404
|
+
* keeps a renamed collection's published hostnames redirecting rather than
|
|
405
|
+
* answering nothing.
|
|
406
|
+
*/
|
|
407
|
+
function findRepoBySiteHostname(root, sitesHost, hostname) {
|
|
408
|
+
const label = (0, siteshost_1.siteHostLabel)(sitesHost, hostname);
|
|
409
|
+
if (label === null)
|
|
410
|
+
return null;
|
|
411
|
+
if (!label.includes('--'))
|
|
412
|
+
return findRepoBySiteLabel(root, label);
|
|
413
|
+
const parsed = (0, siteshost_1.parseSiteHost)(sitesHost, hostname);
|
|
414
|
+
if (!parsed)
|
|
415
|
+
return null;
|
|
416
|
+
return {
|
|
417
|
+
collection: collectionForSiteAlias(root, parsed.collectionAlias) ?? parsed.collectionAlias,
|
|
418
|
+
repo: parsed.repo,
|
|
419
|
+
};
|
|
420
|
+
}
|
package/dist/siteshost.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_SITE_LABEL = void 0;
|
|
3
4
|
exports.isSiteLabelSafe = isSiteLabelSafe;
|
|
5
|
+
exports.isReservedSiteLabel = isReservedSiteLabel;
|
|
6
|
+
exports.reservedSiteLabels = reservedSiteLabels;
|
|
7
|
+
exports.sanitizedSiteLabel = sanitizedSiteLabel;
|
|
4
8
|
exports.normalizeHostname = normalizeHostname;
|
|
5
|
-
exports.
|
|
9
|
+
exports.derivedSiteHost = derivedSiteHost;
|
|
10
|
+
exports.siteHostLabel = siteHostLabel;
|
|
6
11
|
exports.parseSiteHost = parseSiteHost;
|
|
7
12
|
exports.isUnderSitesHost = isUnderSitesHost;
|
|
8
13
|
const scan_1 = require("./scan");
|
|
9
14
|
// The grammar of a per-site hostname, in both directions, so that the parser
|
|
10
15
|
// and the link builder cannot drift apart. A site served from
|
|
11
|
-
// <repo>--<
|
|
16
|
+
// <repo>--<alias>.<sitesHost> has an origin of its own, which is what gives
|
|
12
17
|
// it back the cookies, storage, and service workers that the sandbox on the
|
|
13
18
|
// forge host takes away.
|
|
19
|
+
//
|
|
20
|
+
// The `alias` half is the collection's site alias rather than its name: a
|
|
21
|
+
// collection may be called something no hostname label can be, and resolving
|
|
22
|
+
// the two is a question about the vault's collections, which this file cannot
|
|
23
|
+
// see. Everything here is a pure function of the strings it is given, so the
|
|
24
|
+
// grammar stays testable and cheap; src/sitesettings.ts holds the half that
|
|
25
|
+
// reads the vault.
|
|
14
26
|
/**
|
|
15
27
|
* Whether a collection or repository name may appear in a site hostname. The
|
|
16
28
|
* regex is the whole definition: lowercase letters, digits, and single interior
|
|
@@ -20,8 +32,67 @@ const scan_1 = require("./scan");
|
|
|
20
32
|
function isSiteLabelSafe(name) {
|
|
21
33
|
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
|
|
22
34
|
}
|
|
23
|
-
/** The DNS limit on one label, which <repo>--<
|
|
24
|
-
|
|
35
|
+
/** The DNS limit on one label, which <repo>--<alias> has to fit inside. */
|
|
36
|
+
exports.MAX_SITE_LABEL = 63;
|
|
37
|
+
/**
|
|
38
|
+
* Labels under the sites host that no repository may claim.
|
|
39
|
+
*
|
|
40
|
+
* The sites host is a name the operator will want to use for other things:
|
|
41
|
+
* `www` for a landing page, `api` for something in front of the vault, the
|
|
42
|
+
* mail and nameserver names a registrar's defaults create. A repository that
|
|
43
|
+
* had claimed one of those would quietly own it, and taking it back would mean
|
|
44
|
+
* finding the repository holding it. Reserving them is the cheap direction.
|
|
45
|
+
*
|
|
46
|
+
* The set applies only to the labels a repository claims for itself, not to a
|
|
47
|
+
* collection's site alias: an alias appears in a hostname only after
|
|
48
|
+
* `<repo>--`, where it can shadow nothing. Note that `xn--anything` needs no
|
|
49
|
+
* reserving, since a claimed label may not contain a double hyphen at all.
|
|
50
|
+
*/
|
|
51
|
+
const RESERVED_SITE_LABELS = new Set([
|
|
52
|
+
'admin',
|
|
53
|
+
'api',
|
|
54
|
+
'assets',
|
|
55
|
+
'cdn',
|
|
56
|
+
'forge',
|
|
57
|
+
'git',
|
|
58
|
+
'localhost',
|
|
59
|
+
'mail',
|
|
60
|
+
'ns1',
|
|
61
|
+
'ns2',
|
|
62
|
+
'sites',
|
|
63
|
+
'smtp',
|
|
64
|
+
'static',
|
|
65
|
+
'vault',
|
|
66
|
+
'www',
|
|
67
|
+
]);
|
|
68
|
+
/** Whether a label is one the operator keeps, so no repository may claim it. */
|
|
69
|
+
function isReservedSiteLabel(label) {
|
|
70
|
+
return RESERVED_SITE_LABELS.has(label);
|
|
71
|
+
}
|
|
72
|
+
/** The reserved labels, sorted, for the settings page to name them. */
|
|
73
|
+
function reservedSiteLabels() {
|
|
74
|
+
return [...RESERVED_SITE_LABELS].sort();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A name rewritten as a hostname label, or '' when nothing usable is left.
|
|
78
|
+
*
|
|
79
|
+
* Every run of characters a label may not contain becomes a single hyphen and
|
|
80
|
+
* the ends are trimmed, so `Simulated_Instruments` becomes
|
|
81
|
+
* `simulated-instruments`. Collapsing the runs is what keeps a double hyphen
|
|
82
|
+
* out of the result, which the `<repo>--<alias>` grammar depends on.
|
|
83
|
+
*
|
|
84
|
+
* The rewrite is not injective: `a_b` and `a.b` both come out as `a-b`. It is
|
|
85
|
+
* therefore only ever used as a fallback that is dropped when two collections
|
|
86
|
+
* would land on the same label; see collectionSiteAliases in
|
|
87
|
+
* src/sitesettings.ts.
|
|
88
|
+
*/
|
|
89
|
+
function sanitizedSiteLabel(name) {
|
|
90
|
+
const label = name
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
93
|
+
.replace(/^-+|-+$/g, '');
|
|
94
|
+
return isSiteLabelSafe(label) && label.length <= exports.MAX_SITE_LABEL ? label : '';
|
|
95
|
+
}
|
|
25
96
|
/**
|
|
26
97
|
* The one normalization a hostname gets, wherever one is read. Exported because
|
|
27
98
|
* the configured sites host and the request's own hostname are compared against
|
|
@@ -32,36 +103,39 @@ function normalizeHostname(hostname) {
|
|
|
32
103
|
return hostname.trim().toLowerCase().replace(/\.$/, '');
|
|
33
104
|
}
|
|
34
105
|
/**
|
|
35
|
-
* The full hostname for a repository's site
|
|
106
|
+
* The full hostname for a repository's site under a collection's site alias, or
|
|
107
|
+
* null if the pair is not eligible.
|
|
36
108
|
*
|
|
37
|
-
* Repository
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* rather than mangling is deliberate:
|
|
41
|
-
* `webapp1` beside it, since
|
|
42
|
-
* legal on disk.
|
|
109
|
+
* Repository names are more permissive than a hostname label (isValidName
|
|
110
|
+
* allows '.', '_', and uppercase), so not every repository is eligible, and an
|
|
111
|
+
* ineligible one keeps being served on the forge host unless its admin claims a
|
|
112
|
+
* label that is usable. Refusing rather than mangling is deliberate here:
|
|
113
|
+
* lowercasing `Webapp1` would collide with a `webapp1` beside it, since
|
|
114
|
+
* hostnames are case-insensitive and both names are legal on disk. A
|
|
115
|
+
* collection's alias is where the mangling is done instead, because it can be
|
|
116
|
+
* checked against every other collection first, which a repository name inside
|
|
117
|
+
* one collection cannot be.
|
|
43
118
|
*/
|
|
44
|
-
function
|
|
119
|
+
function derivedSiteHost(sitesHost, collectionAlias, repo) {
|
|
45
120
|
if (!sitesHost)
|
|
46
121
|
return null;
|
|
47
122
|
const name = (0, scan_1.displayName)(repo);
|
|
48
|
-
if (!isSiteLabelSafe(name) || !isSiteLabelSafe(
|
|
123
|
+
if (!isSiteLabelSafe(name) || !isSiteLabelSafe(collectionAlias))
|
|
49
124
|
return null;
|
|
50
|
-
const label = `${name}--${
|
|
51
|
-
if (label.length >
|
|
125
|
+
const label = `${name}--${collectionAlias}`;
|
|
126
|
+
if (label.length > exports.MAX_SITE_LABEL)
|
|
52
127
|
return null;
|
|
53
128
|
return `${label}.${sitesHost}`;
|
|
54
129
|
}
|
|
55
130
|
/**
|
|
56
|
-
* The
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* the names with findRepo as usual.
|
|
131
|
+
* The single label a hostname adds under the sites host, or null: null for a
|
|
132
|
+
* hostname elsewhere, for the bare sites host, and for a deeper name such as
|
|
133
|
+
* a.b.<sitesHost>, which is refused and is not covered by a single wildcard
|
|
134
|
+
* certificate anyway. What the label means is the caller's question: one
|
|
135
|
+
* containing `--` is a derived <repo>--<alias> name for parseSiteHost below,
|
|
136
|
+
* and one without is a custom label a repository may have claimed.
|
|
63
137
|
*/
|
|
64
|
-
function
|
|
138
|
+
function siteHostLabel(sitesHost, hostname) {
|
|
65
139
|
if (!sitesHost)
|
|
66
140
|
return null;
|
|
67
141
|
const host = normalizeHostname(hostname);
|
|
@@ -69,17 +143,31 @@ function parseSiteHost(sitesHost, hostname) {
|
|
|
69
143
|
if (!host.endsWith(suffix))
|
|
70
144
|
return null;
|
|
71
145
|
const label = host.slice(0, -suffix.length);
|
|
72
|
-
// Exactly one remaining label. A deeper name such as a.b.<sitesHost> is
|
|
73
|
-
// refused, and is not covered by a single wildcard certificate anyway.
|
|
74
146
|
if (label === '' || label.includes('.'))
|
|
75
147
|
return null;
|
|
148
|
+
return label;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The collection alias and repository a request's hostname names, or null.
|
|
152
|
+
*
|
|
153
|
+
* The double hyphen is an unambiguous separator precisely because neither half
|
|
154
|
+
* may contain one, so `a--b` under alias `c` cannot be confused with `a` under
|
|
155
|
+
* alias `b--c`. It also means no label can begin `xn--`, which a browser would
|
|
156
|
+
* read as punycode. No filesystem access happens here: the alias is turned into
|
|
157
|
+
* a collection by collectionForSiteAlias, and the repository resolved with
|
|
158
|
+
* findRepo, as usual.
|
|
159
|
+
*/
|
|
160
|
+
function parseSiteHost(sitesHost, hostname) {
|
|
161
|
+
const label = siteHostLabel(sitesHost, hostname);
|
|
162
|
+
if (label === null)
|
|
163
|
+
return null;
|
|
76
164
|
const parts = label.split('--');
|
|
77
165
|
if (parts.length !== 2)
|
|
78
166
|
return null;
|
|
79
|
-
const [repo,
|
|
80
|
-
if (!isSiteLabelSafe(repo) || !isSiteLabelSafe(
|
|
167
|
+
const [repo, collectionAlias] = parts;
|
|
168
|
+
if (!isSiteLabelSafe(repo) || !isSiteLabelSafe(collectionAlias))
|
|
81
169
|
return null;
|
|
82
|
-
return {
|
|
170
|
+
return { collectionAlias, repo };
|
|
83
171
|
}
|
|
84
172
|
/**
|
|
85
173
|
* Whether a hostname belongs to the sites host at all, including the bare name
|
package/dist/source.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.parseSource = parseSource;
|
|
37
|
+
exports.parseUpstream = parseUpstream;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
// Where an import reads from. `mochi import` accepts an https git URL, an
|
|
@@ -88,3 +89,22 @@ function parseSource(input) {
|
|
|
88
89
|
const name = nameOf(url);
|
|
89
90
|
return name === '' ? null : { url, name, github: githubOf(url) };
|
|
90
91
|
}
|
|
92
|
+
function parseUpstream(input) {
|
|
93
|
+
if (typeof input !== 'string' || input === '')
|
|
94
|
+
return null;
|
|
95
|
+
const github = githubOf(input);
|
|
96
|
+
if (HTTPS_SOURCE.test(input)) {
|
|
97
|
+
const label = input.replace(/^https:\/\//, '').replace(/\.git$/i, '').replace(/\/+$/, '');
|
|
98
|
+
return { url: input, label, web: `https://${label}`, github };
|
|
99
|
+
}
|
|
100
|
+
if (SSH_SOURCE.test(input)) {
|
|
101
|
+
const label = input.replace(/^git@/, '').replace(':', '/').replace(/\.git$/i, '');
|
|
102
|
+
return {
|
|
103
|
+
url: input,
|
|
104
|
+
label,
|
|
105
|
+
web: github ? `https://github.com/${github.owner}/${github.repo}` : null,
|
|
106
|
+
github,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|