@magland/mochi 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- package/package.json +71 -0
package/dist/vault.js
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
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.VAULT_VERSION = exports.VAULT_FILE = void 0;
|
|
37
|
+
exports.tokenId = tokenId;
|
|
38
|
+
exports.vaultFilePath = vaultFilePath;
|
|
39
|
+
exports.loadVault = loadVault;
|
|
40
|
+
exports.hashToken = hashToken;
|
|
41
|
+
exports.mintToken = mintToken;
|
|
42
|
+
exports.globMatch = globMatch;
|
|
43
|
+
exports.authenticate = authenticate;
|
|
44
|
+
exports.authenticateToken = authenticateToken;
|
|
45
|
+
exports.addUserToken = addUserToken;
|
|
46
|
+
exports.finishPermsMigration = finishPermsMigration;
|
|
47
|
+
exports.setSiteAdmin = setSiteAdmin;
|
|
48
|
+
exports.bootstrapVault = bootstrapVault;
|
|
49
|
+
exports.revokeToken = revokeToken;
|
|
50
|
+
exports.setUserEmails = setUserEmails;
|
|
51
|
+
exports.accountForEmail = accountForEmail;
|
|
52
|
+
exports.mergeContributors = mergeContributors;
|
|
53
|
+
exports.removeUser = removeUser;
|
|
54
|
+
const crypto = __importStar(require("crypto"));
|
|
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 scan_1 = require("./scan");
|
|
60
|
+
exports.VAULT_FILE = 'vault.json';
|
|
61
|
+
/** How a token is named in a listing or a revocation: its id, or a stand-in for one. */
|
|
62
|
+
function tokenId(t) {
|
|
63
|
+
return t.id ?? t.hash.slice(0, 8);
|
|
64
|
+
}
|
|
65
|
+
/** The version written to vault.json since roles replaced glob scopes. */
|
|
66
|
+
exports.VAULT_VERSION = 2;
|
|
67
|
+
function vaultFilePath(root) {
|
|
68
|
+
return path.join(root, exports.VAULT_FILE);
|
|
69
|
+
}
|
|
70
|
+
function asStringArray(v) {
|
|
71
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string') ? v : null;
|
|
72
|
+
}
|
|
73
|
+
function normalizeVault(parsed) {
|
|
74
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
75
|
+
throw new Error('vault.json must be a JSON object');
|
|
76
|
+
}
|
|
77
|
+
const usersRaw = parsed.users;
|
|
78
|
+
if (typeof usersRaw !== 'object' || usersRaw === null) {
|
|
79
|
+
throw new Error('vault.json must have a "users" object');
|
|
80
|
+
}
|
|
81
|
+
// A file without the version marker was written before roles existed and
|
|
82
|
+
// still carries glob scopes. Its users and tokens parse as usual, and the
|
|
83
|
+
// globs ride along in `legacy` for the migration to translate; the file is
|
|
84
|
+
// legacy as a whole, so half-migrated states cannot exist.
|
|
85
|
+
const legacy = parsed.version !== exports.VAULT_VERSION;
|
|
86
|
+
const users = {};
|
|
87
|
+
for (const [name, u] of Object.entries(usersRaw)) {
|
|
88
|
+
if (typeof u !== 'object' || u === null) {
|
|
89
|
+
throw new Error(`user ${name} must be an object`);
|
|
90
|
+
}
|
|
91
|
+
const rec = u;
|
|
92
|
+
const scope = asStringArray(rec.scope ?? []);
|
|
93
|
+
if (!scope) {
|
|
94
|
+
throw new Error(`user ${name}: "scope" must be a list of strings`);
|
|
95
|
+
}
|
|
96
|
+
const admin = asStringArray(rec.admin ?? []);
|
|
97
|
+
if (!admin) {
|
|
98
|
+
throw new Error(`user ${name}: "admin" must be a list of strings`);
|
|
99
|
+
}
|
|
100
|
+
const tokensRaw = rec.tokens ?? [];
|
|
101
|
+
if (!Array.isArray(tokensRaw)) {
|
|
102
|
+
throw new Error(`user ${name}: "tokens" must be a list`);
|
|
103
|
+
}
|
|
104
|
+
const tokens = tokensRaw.map((t, i) => {
|
|
105
|
+
if (typeof t === 'string')
|
|
106
|
+
return { hash: t };
|
|
107
|
+
if (typeof t === 'object' && t !== null && typeof t.hash === 'string') {
|
|
108
|
+
const tRec = t;
|
|
109
|
+
const rec = { hash: tRec.hash };
|
|
110
|
+
if (typeof tRec.id === 'string' && tRec.id !== '')
|
|
111
|
+
rec.id = tRec.id;
|
|
112
|
+
if (typeof tRec.created === 'string' && tRec.created !== '')
|
|
113
|
+
rec.created = tRec.created;
|
|
114
|
+
if (tRec.scope !== undefined) {
|
|
115
|
+
const ts = asStringArray(tRec.scope);
|
|
116
|
+
if (!ts)
|
|
117
|
+
throw new Error(`user ${name}: token ${i} "scope" must be a list of strings`);
|
|
118
|
+
rec.scope = ts;
|
|
119
|
+
}
|
|
120
|
+
return rec;
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`user ${name}: token ${i} must be a hash string or an object with a "hash"`);
|
|
123
|
+
});
|
|
124
|
+
const emails = asStringArray(rec.emails ?? []);
|
|
125
|
+
if (!emails) {
|
|
126
|
+
throw new Error(`user ${name}: "emails" must be a list of strings`);
|
|
127
|
+
}
|
|
128
|
+
users[name] = {
|
|
129
|
+
tokens,
|
|
130
|
+
...(rec.siteAdmin === true ? { siteAdmin: true } : {}),
|
|
131
|
+
...(emails.length ? { emails } : {}),
|
|
132
|
+
...(legacy ? { legacy: { scope, admin } } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { users, ...(legacy ? { legacy: true } : {}) };
|
|
136
|
+
}
|
|
137
|
+
const cache = (0, filecache_1.fileCache)({
|
|
138
|
+
read: (file) => {
|
|
139
|
+
try {
|
|
140
|
+
return { status: 'ok', vault: normalizeVault(JSON.parse(fs.readFileSync(file, 'utf8'))) };
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
return { status: 'error', message: e instanceof Error ? e.message : String(e) };
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
missing: () => ({ status: 'missing' }),
|
|
147
|
+
});
|
|
148
|
+
function loadVault(root) {
|
|
149
|
+
return cache.get(vaultFilePath(root));
|
|
150
|
+
}
|
|
151
|
+
function hashToken(token) {
|
|
152
|
+
return crypto.createHash('sha256').update(token).digest('hex');
|
|
153
|
+
}
|
|
154
|
+
function mintToken() {
|
|
155
|
+
const token = 'mochi_' + crypto.randomBytes(32).toString('hex');
|
|
156
|
+
return { token, hash: hashToken(token) };
|
|
157
|
+
}
|
|
158
|
+
function globMatch(pattern, target) {
|
|
159
|
+
const rx = pattern
|
|
160
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
161
|
+
.replace(/\*/g, '.*')
|
|
162
|
+
.replace(/\?/g, '.');
|
|
163
|
+
return new RegExp(`^${rx}$`).test(target);
|
|
164
|
+
}
|
|
165
|
+
function authenticate(vault, username, tokenPlain) {
|
|
166
|
+
const user = vault.users[username];
|
|
167
|
+
if (!user)
|
|
168
|
+
return null;
|
|
169
|
+
const presented = Buffer.from(hashToken(tokenPlain), 'hex');
|
|
170
|
+
for (const t of user.tokens) {
|
|
171
|
+
let stored;
|
|
172
|
+
try {
|
|
173
|
+
stored = Buffer.from(t.hash, 'hex');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (stored.length === presented.length && crypto.timingSafeEqual(stored, presented)) {
|
|
179
|
+
return { username, user, token: t };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function authenticateToken(vault, tokenPlain) {
|
|
185
|
+
const presented = Buffer.from(hashToken(tokenPlain), 'hex');
|
|
186
|
+
for (const [username, user] of Object.entries(vault.users)) {
|
|
187
|
+
for (const t of user.tokens) {
|
|
188
|
+
let stored;
|
|
189
|
+
try {
|
|
190
|
+
stored = Buffer.from(t.hash, 'hex');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (stored.length === presented.length && crypto.timingSafeEqual(stored, presented)) {
|
|
196
|
+
return { username, user, token: t };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
// Who may do what lives in src/perms.ts, which reads roles and ownership from
|
|
203
|
+
// where each is stored; this file holds only who somebody is.
|
|
204
|
+
function writeVault(file, vault) {
|
|
205
|
+
const users = {};
|
|
206
|
+
for (const [name, u] of Object.entries(vault.users)) {
|
|
207
|
+
const { legacy: _legacy, ...rest } = u;
|
|
208
|
+
users[name] = rest;
|
|
209
|
+
}
|
|
210
|
+
(0, atomic_1.writeFileAtomic)(file, JSON.stringify({ version: exports.VAULT_VERSION, users }, null, 2) + '\n', { mode: 0o600 });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Every edit to vault.json is a read, a change in memory, and a write back, and
|
|
214
|
+
* they are all short. Holding one lock across the whole of one makes them
|
|
215
|
+
* serial against a second server and against a CLI run on the same directory,
|
|
216
|
+
* which is the arrangement the documentation invites by saying a vault is just
|
|
217
|
+
* a directory. Readers do not take the lock: they see the old file or the new
|
|
218
|
+
* one, which the atomic rename already guarantees.
|
|
219
|
+
*/
|
|
220
|
+
function editVault(root, fn) {
|
|
221
|
+
return (0, atomic_1.withFileLock)(path.join(root, `${exports.VAULT_FILE}.lock`), () => fn(vaultFilePath(root)));
|
|
222
|
+
}
|
|
223
|
+
// Reread under the lock, refusing a pre-roles file: writeVault writes the
|
|
224
|
+
// current shape, so editing a legacy file in place would silently drop the
|
|
225
|
+
// glob scopes the migration still has to translate.
|
|
226
|
+
function readVaultForEdit(file) {
|
|
227
|
+
const vault = normalizeVault(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
228
|
+
if (vault.legacy) {
|
|
229
|
+
throw new Error('vault.json predates roles; start the server once to migrate it');
|
|
230
|
+
}
|
|
231
|
+
return vault;
|
|
232
|
+
}
|
|
233
|
+
// The body of addUserToken, without the lock, so that bootstrapVault can put
|
|
234
|
+
// its own check-then-act inside the same one rather than nesting a second.
|
|
235
|
+
function addUserTokenLocked(file, username, opts) {
|
|
236
|
+
let vault = { users: {} };
|
|
237
|
+
if (fs.existsSync(file)) {
|
|
238
|
+
vault = normalizeVault(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
239
|
+
if (vault.legacy) {
|
|
240
|
+
throw new Error('vault.json predates roles; start the server once to migrate it');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let user = vault.users[username];
|
|
244
|
+
const created = !user;
|
|
245
|
+
if (!user) {
|
|
246
|
+
// A leading dot is allowed in a repository's name and in nothing else; a
|
|
247
|
+
// user carrying one would be a name the interface hides for no reason.
|
|
248
|
+
if ((0, scan_1.isDotName)(username))
|
|
249
|
+
throw new Error(`invalid username ${username}`);
|
|
250
|
+
user = { tokens: [], ...(opts.siteAdmin ? { siteAdmin: true } : {}) };
|
|
251
|
+
vault.users[username] = user;
|
|
252
|
+
}
|
|
253
|
+
else if (opts.siteAdmin) {
|
|
254
|
+
throw new Error(`user ${username} already exists; site admin is granted from the admin page or the users API`);
|
|
255
|
+
}
|
|
256
|
+
// A caller may supply the token instead of taking a minted one, which is how
|
|
257
|
+
// a vault can be bootstrapped with a token its operator already holds. Only
|
|
258
|
+
// the hash is stored either way, so the two cases differ in nothing else.
|
|
259
|
+
const { token, hash } = opts.token ? { token: opts.token, hash: hashToken(opts.token) } : mintToken();
|
|
260
|
+
const rec = { hash, id: crypto.randomBytes(4).toString('hex'), created: new Date().toISOString() };
|
|
261
|
+
if (opts.tokenScope && opts.tokenScope.length)
|
|
262
|
+
rec.scope = opts.tokenScope;
|
|
263
|
+
user.tokens.push(rec);
|
|
264
|
+
writeVault(file, vault);
|
|
265
|
+
return { token, created, user };
|
|
266
|
+
}
|
|
267
|
+
function addUserToken(root, username, opts = {}) {
|
|
268
|
+
return editVault(root, (file) => addUserTokenLocked(file, username, opts));
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The last step of the permissions migration (src/migrate.ts): rewrite a
|
|
272
|
+
* pre-roles vault.json in the current shape, granting the site-admin bit to
|
|
273
|
+
* the users named. The glob scopes are dropped here and only here, after the
|
|
274
|
+
* migration has translated them into ownership and collaborator files; the
|
|
275
|
+
* original file survives as the backup the migration wrote first.
|
|
276
|
+
*/
|
|
277
|
+
function finishPermsMigration(root, siteAdmins) {
|
|
278
|
+
editVault(root, (file) => {
|
|
279
|
+
const vault = normalizeVault(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
280
|
+
if (!vault.legacy)
|
|
281
|
+
return;
|
|
282
|
+
delete vault.legacy;
|
|
283
|
+
for (const [name, user] of Object.entries(vault.users)) {
|
|
284
|
+
delete user.legacy;
|
|
285
|
+
if (siteAdmins.includes(name))
|
|
286
|
+
user.siteAdmin = true;
|
|
287
|
+
}
|
|
288
|
+
writeVault(file, vault);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Grant or withdraw the site-admin bit. */
|
|
292
|
+
function setSiteAdmin(root, username, value) {
|
|
293
|
+
return editVault(root, (file) => {
|
|
294
|
+
if (!fs.existsSync(file)) {
|
|
295
|
+
throw new Error(`no vault.json at ${file}; create the user first with: mochi user add ${username}`);
|
|
296
|
+
}
|
|
297
|
+
const vault = readVaultForEdit(file);
|
|
298
|
+
const user = vault.users[username];
|
|
299
|
+
if (!user) {
|
|
300
|
+
throw new Error(`user ${username} does not exist; create it with: mochi user add ${username}`);
|
|
301
|
+
}
|
|
302
|
+
if (value)
|
|
303
|
+
user.siteAdmin = true;
|
|
304
|
+
else
|
|
305
|
+
delete user.siteAdmin;
|
|
306
|
+
writeVault(file, vault);
|
|
307
|
+
return user;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Initialize a vault that has none, creating the owner and its first token.
|
|
312
|
+
* Returns null when there is already a vault.json, since a vault is
|
|
313
|
+
* initialized once and everything after that is the operator's own doing.
|
|
314
|
+
*
|
|
315
|
+
* `presetToken` lets the token be handed in rather than minted, which is what
|
|
316
|
+
* `mochi deploy` does: it mints the token on the operator's machine and
|
|
317
|
+
* passes it to the server as an environment secret, so a fresh remote vault can
|
|
318
|
+
* be logged in to without reading a token back out of the logs. A preset token
|
|
319
|
+
* is never echoed by the caller, so it does not reach the log at all.
|
|
320
|
+
*/
|
|
321
|
+
function bootstrapVault(root, presetToken) {
|
|
322
|
+
const preset = (presetToken ?? '').trim();
|
|
323
|
+
if (presetToken !== undefined && presetToken !== null && presetToken !== '' && !preset) {
|
|
324
|
+
throw new Error('the owner token given for a new vault is blank');
|
|
325
|
+
}
|
|
326
|
+
// A token travels as a Basic-auth password and lands in URLs' credential
|
|
327
|
+
// slots, so a supplied one is held to the shape of a minted one rather than
|
|
328
|
+
// taken as given: printable, no spaces, and long enough not to be guessed.
|
|
329
|
+
if (preset && !/^[\x21-\x7e]{24,256}$/.test(preset)) {
|
|
330
|
+
throw new Error('the owner token given for a new vault is not usable: it must be 24 to 256 characters, ' +
|
|
331
|
+
'printable, and contain no spaces');
|
|
332
|
+
}
|
|
333
|
+
// The existence check and the creation are one operation. Two servers started
|
|
334
|
+
// against one fresh directory would otherwise both find no vault.json and both
|
|
335
|
+
// bootstrap, and the second owner token to be written would be the only one
|
|
336
|
+
// that worked, while both had been printed as if they were the credential.
|
|
337
|
+
return editVault(root, (file) => {
|
|
338
|
+
if (fs.existsSync(file))
|
|
339
|
+
return null;
|
|
340
|
+
const { token } = addUserTokenLocked(file, 'owner', {
|
|
341
|
+
siteAdmin: true,
|
|
342
|
+
...(preset ? { token: preset } : {}),
|
|
343
|
+
});
|
|
344
|
+
return { username: 'owner', token, preset: preset !== '' };
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Revoke one token by its id. Revoking the token currently in use is allowed and
|
|
349
|
+
* reported plainly rather than refused: locking yourself out is your business,
|
|
350
|
+
* and vault.json remains hand-editable either way.
|
|
351
|
+
*/
|
|
352
|
+
function revokeToken(root, username, id) {
|
|
353
|
+
return editVault(root, (file) => {
|
|
354
|
+
const vault = readVaultForEdit(file);
|
|
355
|
+
const user = vault.users[username];
|
|
356
|
+
if (!user)
|
|
357
|
+
throw new Error(`no user ${username}`);
|
|
358
|
+
const before = user.tokens.length;
|
|
359
|
+
user.tokens = user.tokens.filter((t) => tokenId(t) !== id);
|
|
360
|
+
if (user.tokens.length === before)
|
|
361
|
+
return { revoked: false, remaining: before };
|
|
362
|
+
writeVault(file, vault);
|
|
363
|
+
return { revoked: true, remaining: user.tokens.length };
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** Replace the git author emails aliased to a user. An empty list clears them. */
|
|
367
|
+
function setUserEmails(root, username, emails) {
|
|
368
|
+
return editVault(root, (file) => {
|
|
369
|
+
const vault = readVaultForEdit(file);
|
|
370
|
+
const user = vault.users[username];
|
|
371
|
+
if (!user)
|
|
372
|
+
throw new Error(`no user ${username}`);
|
|
373
|
+
if (emails.length)
|
|
374
|
+
user.emails = emails;
|
|
375
|
+
else
|
|
376
|
+
delete user.emails;
|
|
377
|
+
writeVault(file, vault);
|
|
378
|
+
return user;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The user a commit author email belongs to, or null when it belongs to no
|
|
383
|
+
* one this vault knows. Two ways in: the synthetic `<user>@noreply.<anything>`
|
|
384
|
+
* the web editor writes, recognised by shape so the answer does not depend on
|
|
385
|
+
* which hostname the vault was being served under at the time, and the emails
|
|
386
|
+
* a user has listed as theirs.
|
|
387
|
+
*/
|
|
388
|
+
function accountForEmail(vault, email) {
|
|
389
|
+
const lower = email.toLowerCase();
|
|
390
|
+
const m = lower.match(/^([^@]+)@noreply\./);
|
|
391
|
+
if (m && vault.users[m[1]])
|
|
392
|
+
return m[1];
|
|
393
|
+
for (const [name, user] of Object.entries(vault.users)) {
|
|
394
|
+
if (user.emails?.some((e) => e.toLowerCase() === lower))
|
|
395
|
+
return name;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Fold one person's identities into one contributor. Grouped by account where
|
|
401
|
+
* an email resolves to one, and by email otherwise; within a group the
|
|
402
|
+
* human-written identity is preferred over the synthetic one for the name and
|
|
403
|
+
* the email shown, since "Jeremy" is who committed and `owner@noreply...` is
|
|
404
|
+
* how. Without a vault (one that failed to load) everyone stands as git
|
|
405
|
+
* reported them.
|
|
406
|
+
*/
|
|
407
|
+
function mergeContributors(vault, people) {
|
|
408
|
+
const groups = new Map();
|
|
409
|
+
for (const p of people) {
|
|
410
|
+
const account = vault ? accountForEmail(vault, p.email) : null;
|
|
411
|
+
const key = account !== null ? `u:${account}` : `e:${(p.email || p.name).toLowerCase()}`;
|
|
412
|
+
const synthetic = /@noreply\./i.test(p.email);
|
|
413
|
+
const g = groups.get(key);
|
|
414
|
+
if (!g) {
|
|
415
|
+
groups.set(key, { ...p, account, bestCommits: p.commits, bestSynthetic: synthetic });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
g.commits += p.commits;
|
|
419
|
+
// The identity shown is the best-represented human one; a synthetic
|
|
420
|
+
// identity only fronts a group that holds nothing else.
|
|
421
|
+
if ((g.bestSynthetic && !synthetic) || (g.bestSynthetic === synthetic && p.commits > g.bestCommits)) {
|
|
422
|
+
g.name = p.name;
|
|
423
|
+
g.email = p.email;
|
|
424
|
+
g.bestCommits = p.commits;
|
|
425
|
+
g.bestSynthetic = synthetic;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return [...groups.values()]
|
|
429
|
+
.map(({ bestCommits: _b, bestSynthetic: _s, ...c }) => c)
|
|
430
|
+
.sort((a, b) => b.commits - a.commits);
|
|
431
|
+
}
|
|
432
|
+
/** Remove a user, and with them every token they hold. */
|
|
433
|
+
function removeUser(root, username) {
|
|
434
|
+
return editVault(root, (file) => {
|
|
435
|
+
const vault = readVaultForEdit(file);
|
|
436
|
+
if (!vault.users[username])
|
|
437
|
+
return false;
|
|
438
|
+
delete vault.users[username];
|
|
439
|
+
writeVault(file, vault);
|
|
440
|
+
return true;
|
|
441
|
+
});
|
|
442
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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.packageVersion = packageVersion;
|
|
37
|
+
exports.buildInfo = buildInfo;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
// What this vault is running, in the two forms something asks for it: the
|
|
41
|
+
// version of the package, and the build that version was compiled from.
|
|
42
|
+
//
|
|
43
|
+
// The version is read from the running package's own package.json. It is used
|
|
44
|
+
// where the server writes a command for somebody else's machine to run: the
|
|
45
|
+
// pasted `npx @magland/mochi@<version>` pins what that machine executes to
|
|
46
|
+
// what this vault was tested against, rather than to whatever npm serves the
|
|
47
|
+
// day the command is pasted.
|
|
48
|
+
//
|
|
49
|
+
// The build is a question package.json cannot answer. Two vaults reporting
|
|
50
|
+
// 0.3.0 can be different commits, since main carries the version of the last
|
|
51
|
+
// release until the next bump, and "is the fix I pushed the code that is
|
|
52
|
+
// running" is then unanswerable from the version alone. So the compile writes
|
|
53
|
+
// dist/build-info.json beside the code it emits (scripts/build-info.js, run by
|
|
54
|
+
// `npm run build`) and it is read back here. Running from source has no such
|
|
55
|
+
// file and says so rather than guessing.
|
|
56
|
+
let cached = null;
|
|
57
|
+
function packageVersion() {
|
|
58
|
+
if (cached !== null)
|
|
59
|
+
return cached;
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
62
|
+
cached = typeof pkg.version === 'string' && pkg.version !== '' ? pkg.version : 'latest';
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
cached = 'latest';
|
|
66
|
+
}
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
69
|
+
let build = null;
|
|
70
|
+
function buildInfo() {
|
|
71
|
+
if (build !== null)
|
|
72
|
+
return build;
|
|
73
|
+
let commit = null;
|
|
74
|
+
let builtAt = null;
|
|
75
|
+
try {
|
|
76
|
+
const stamp = JSON.parse(fs.readFileSync(path.join(__dirname, 'build-info.json'), 'utf8'));
|
|
77
|
+
if (typeof stamp.commit === 'string' && stamp.commit !== '')
|
|
78
|
+
commit = stamp.commit;
|
|
79
|
+
if (typeof stamp.builtAt === 'string' && stamp.builtAt !== '')
|
|
80
|
+
builtAt = stamp.builtAt;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// No stamp: source under tsx, or a dist compiled by tsc on its own. Both
|
|
84
|
+
// are honest as "unbuilt" and neither is worth inventing a date for.
|
|
85
|
+
}
|
|
86
|
+
build = { version: packageVersion(), commit, builtAt };
|
|
87
|
+
return build;
|
|
88
|
+
}
|