@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
@@ -0,0 +1,347 @@
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.SUPPORTED_ALGS = exports.COSE_RS256 = exports.COSE_EDDSA = exports.COSE_ES256 = exports.WebAuthnError = void 0;
37
+ exports.b64url = b64url;
38
+ exports.fromB64url = fromB64url;
39
+ exports.mintChallenge = mintChallenge;
40
+ exports.claimedChallenge = claimedChallenge;
41
+ exports.verifyRegistration = verifyRegistration;
42
+ exports.verifyAssertion = verifyAssertion;
43
+ const crypto = __importStar(require("crypto"));
44
+ // WebAuthn (passkey) verification, on node's own crypto and nothing else.
45
+ //
46
+ // A passkey is a public-key credential the browser keeps: registration hands
47
+ // the vault a public key, and signing in proves possession of the private one
48
+ // by signing a server-minted challenge. What a server has to implement is
49
+ // small and precisely specified -- parse the authenticator's CBOR, check the
50
+ // challenge, the origin, and the RP ID hash, and verify one signature -- so it
51
+ // is implemented here rather than through a dependency, the way the multipart
52
+ // parser and the cookie parser are. The subset is deliberate:
53
+ //
54
+ // - Attestation is not verified. Attestation says which make of
55
+ // authenticator minted the key, which matters to a bank with a hardware
56
+ // policy and not to a vault whose user is registering a key on their own
57
+ // account. Browsers send fmt "none" unless asked; whatever arrives, only
58
+ // authData is read.
59
+ // - Three algorithms: ES256 (-7) and EdDSA (-8), which are what passkeys
60
+ // are minted with today, and RS256 (-257) for older platform
61
+ // authenticators. The COSE key is converted to a DER SPKI once, at
62
+ // registration, so signing in is a single crypto.verify.
63
+ //
64
+ // Nothing here touches the vault or the request: callers hand in the expected
65
+ // challenge, origin, and RP ID, and get back parsed facts or a thrown
66
+ // WebAuthnError whose message is safe to show.
67
+ class WebAuthnError extends Error {
68
+ }
69
+ exports.WebAuthnError = WebAuthnError;
70
+ // COSE algorithm identifiers, the only ones offered at registration.
71
+ exports.COSE_ES256 = -7;
72
+ exports.COSE_EDDSA = -8;
73
+ exports.COSE_RS256 = -257;
74
+ exports.SUPPORTED_ALGS = [exports.COSE_ES256, exports.COSE_EDDSA, exports.COSE_RS256];
75
+ function b64url(buf) {
76
+ return buf.toString('base64url');
77
+ }
78
+ function fromB64url(s) {
79
+ if (!/^[A-Za-z0-9_-]*$/.test(s))
80
+ throw new WebAuthnError('not base64url');
81
+ return Buffer.from(s, 'base64url');
82
+ }
83
+ /** A fresh challenge, base64url, as the client APIs expect to echo it back. */
84
+ function mintChallenge() {
85
+ return b64url(crypto.randomBytes(32));
86
+ }
87
+ function cborItem(buf, at) {
88
+ if (at >= buf.length)
89
+ throw new WebAuthnError('truncated CBOR');
90
+ const initial = buf[at];
91
+ const major = initial >> 5;
92
+ const info = initial & 0x1f;
93
+ let length = 0;
94
+ let next = at + 1;
95
+ if (info < 24) {
96
+ length = info;
97
+ }
98
+ else if (info === 24 || info === 25 || info === 26) {
99
+ const bytes = info === 24 ? 1 : info === 25 ? 2 : 4;
100
+ if (next + bytes > buf.length)
101
+ throw new WebAuthnError('truncated CBOR');
102
+ length = info === 24 ? buf[next] : info === 25 ? buf.readUInt16BE(next) : buf.readUInt32BE(next);
103
+ next += bytes;
104
+ }
105
+ else {
106
+ // 27 is a 64-bit length and 31 an indefinite one; neither appears in the
107
+ // structures this parses, and a credential id or key that claimed to need
108
+ // one would not be worth believing.
109
+ throw new WebAuthnError('unsupported CBOR length');
110
+ }
111
+ switch (major) {
112
+ case 0:
113
+ return { value: length, next };
114
+ case 1:
115
+ return { value: -1 - length, next };
116
+ case 2: {
117
+ if (next + length > buf.length)
118
+ throw new WebAuthnError('truncated CBOR');
119
+ return { value: buf.subarray(next, next + length), next: next + length };
120
+ }
121
+ case 3: {
122
+ if (next + length > buf.length)
123
+ throw new WebAuthnError('truncated CBOR');
124
+ return { value: buf.subarray(next, next + length).toString('utf8'), next: next + length };
125
+ }
126
+ case 4: {
127
+ const arr = [];
128
+ for (let i = 0; i < length; i++) {
129
+ const item = cborItem(buf, next);
130
+ arr.push(item.value);
131
+ next = item.next;
132
+ }
133
+ return { value: arr, next };
134
+ }
135
+ case 5: {
136
+ const map = new Map();
137
+ for (let i = 0; i < length; i++) {
138
+ const key = cborItem(buf, next);
139
+ const val = cborItem(buf, key.next);
140
+ map.set(key.value, val.value);
141
+ next = val.next;
142
+ }
143
+ return { value: map, next };
144
+ }
145
+ case 7: {
146
+ if (info === 20)
147
+ return { value: false, next };
148
+ if (info === 21)
149
+ return { value: true, next };
150
+ if (info === 22)
151
+ return { value: null, next };
152
+ throw new WebAuthnError('unsupported CBOR simple value');
153
+ }
154
+ default:
155
+ throw new WebAuthnError('unsupported CBOR type');
156
+ }
157
+ }
158
+ function cborDecode(buf) {
159
+ return cborItem(buf, 0).value;
160
+ }
161
+ // ---- COSE keys ----
162
+ function coseField(key, label) {
163
+ const v = key.get(label);
164
+ if (v === undefined)
165
+ throw new WebAuthnError('COSE key is missing a field');
166
+ return v;
167
+ }
168
+ function coseBytes(key, label) {
169
+ const v = coseField(key, label);
170
+ if (!Buffer.isBuffer(v))
171
+ throw new WebAuthnError('COSE key field is not bytes');
172
+ return v;
173
+ }
174
+ /**
175
+ * A COSE public key as a node KeyObject, via JWK import so no ASN.1 is built
176
+ * by hand. Returns the key with the algorithm it signs under, checked against
177
+ * the key's own type: a mismatched pair is refused here rather than trusted
178
+ * into vault.json.
179
+ */
180
+ function coseToKey(cose) {
181
+ const kty = coseField(cose, 1);
182
+ const alg = coseField(cose, 3);
183
+ if (typeof alg !== 'number' || !exports.SUPPORTED_ALGS.includes(alg)) {
184
+ throw new WebAuthnError('the authenticator chose an algorithm this vault does not accept');
185
+ }
186
+ if (kty === 2 && alg === exports.COSE_ES256) {
187
+ if (coseField(cose, -1) !== 1)
188
+ throw new WebAuthnError('ES256 key is not on P-256');
189
+ const x = coseBytes(cose, -2);
190
+ const y = coseBytes(cose, -3);
191
+ if (x.length !== 32 || y.length !== 32)
192
+ throw new WebAuthnError('P-256 coordinates have the wrong length');
193
+ const jwk = { kty: 'EC', crv: 'P-256', x: b64url(x), y: b64url(y) };
194
+ return { key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), alg };
195
+ }
196
+ if (kty === 1 && alg === exports.COSE_EDDSA) {
197
+ if (coseField(cose, -1) !== 6)
198
+ throw new WebAuthnError('EdDSA key is not Ed25519');
199
+ const jwk = { kty: 'OKP', crv: 'Ed25519', x: b64url(coseBytes(cose, -2)) };
200
+ return { key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), alg };
201
+ }
202
+ if (kty === 3 && alg === exports.COSE_RS256) {
203
+ const jwk = { kty: 'RSA', n: b64url(coseBytes(cose, -1)), e: b64url(coseBytes(cose, -2)) };
204
+ return { key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), alg };
205
+ }
206
+ throw new WebAuthnError('unsupported COSE key type');
207
+ }
208
+ // ---- authenticator data ----
209
+ const FLAG_UP = 0x01;
210
+ const FLAG_UV = 0x04;
211
+ const FLAG_AT = 0x40;
212
+ function parseAuthData(data) {
213
+ if (data.length < 37)
214
+ throw new WebAuthnError('authenticator data is too short');
215
+ const flags = data[32];
216
+ const counter = data.readUInt32BE(33);
217
+ let credential = null;
218
+ if (flags & FLAG_AT) {
219
+ if (data.length < 55)
220
+ throw new WebAuthnError('attested credential data is too short');
221
+ const idLen = data.readUInt16BE(53);
222
+ if (55 + idLen > data.length)
223
+ throw new WebAuthnError('credential id is truncated');
224
+ const id = data.subarray(55, 55 + idLen);
225
+ const cose = cborItem(data, 55 + idLen).value;
226
+ if (!(cose instanceof Map))
227
+ throw new WebAuthnError('credential public key is not a COSE map');
228
+ credential = { id, cose };
229
+ }
230
+ return {
231
+ rpIdHash: data.subarray(0, 32),
232
+ userPresent: (flags & FLAG_UP) !== 0,
233
+ userVerified: (flags & FLAG_UV) !== 0,
234
+ counter,
235
+ credential,
236
+ };
237
+ }
238
+ // ---- client data ----
239
+ function checkClientData(clientDataJSON, expect) {
240
+ let parsed;
241
+ try {
242
+ parsed = JSON.parse(clientDataJSON.toString('utf8'));
243
+ }
244
+ catch {
245
+ throw new WebAuthnError('clientDataJSON is not JSON');
246
+ }
247
+ const c = (typeof parsed === 'object' && parsed !== null ? parsed : {});
248
+ if (c.type !== expect.type)
249
+ throw new WebAuthnError('wrong WebAuthn ceremony type');
250
+ if (typeof c.challenge !== 'string' || c.challenge !== expect.challenge) {
251
+ throw new WebAuthnError('the challenge does not match');
252
+ }
253
+ if (typeof c.origin !== 'string' || c.origin !== expect.origin) {
254
+ throw new WebAuthnError(`the origin does not match this vault (${String(c.origin)})`);
255
+ }
256
+ }
257
+ /**
258
+ * The challenge a client data blob claims to answer, read before anything is
259
+ * verified. It is how the server finds the pending challenge to check the rest
260
+ * against; nothing is trusted from it until checkClientData has run.
261
+ */
262
+ function claimedChallenge(clientDataJSON) {
263
+ try {
264
+ const parsed = JSON.parse(clientDataJSON.toString('utf8'));
265
+ return typeof parsed.challenge === 'string' ? parsed.challenge : null;
266
+ }
267
+ catch {
268
+ return null;
269
+ }
270
+ }
271
+ function rpIdHashOk(authData, rpId) {
272
+ const expected = crypto.createHash('sha256').update(rpId).digest();
273
+ return authData.rpIdHash.length === expected.length && crypto.timingSafeEqual(authData.rpIdHash, expected);
274
+ }
275
+ /**
276
+ * Verify a navigator.credentials.create() response. The challenge must be one
277
+ * this server minted (the caller's business to have checked, via a one-time
278
+ * store); everything else is checked here.
279
+ */
280
+ function verifyRegistration(input) {
281
+ checkClientData(input.clientDataJSON, {
282
+ type: 'webauthn.create',
283
+ challenge: input.challenge,
284
+ origin: input.origin,
285
+ });
286
+ const att = cborDecode(input.attestationObject);
287
+ if (!(att instanceof Map))
288
+ throw new WebAuthnError('attestation object is not a CBOR map');
289
+ const rawAuthData = att.get('authData');
290
+ if (!Buffer.isBuffer(rawAuthData))
291
+ throw new WebAuthnError('attestation object has no authData');
292
+ const authData = parseAuthData(rawAuthData);
293
+ if (!rpIdHashOk(authData, input.rpId)) {
294
+ throw new WebAuthnError('the credential was made for a different hostname');
295
+ }
296
+ if (!authData.userPresent)
297
+ throw new WebAuthnError('the authenticator did not report user presence');
298
+ if (!authData.credential)
299
+ throw new WebAuthnError('no credential in the registration');
300
+ if (authData.credential.id.length < 8 || authData.credential.id.length > 1023) {
301
+ throw new WebAuthnError('credential id has an unreasonable length');
302
+ }
303
+ const { key, alg } = coseToKey(authData.credential.cose);
304
+ return {
305
+ id: b64url(authData.credential.id),
306
+ publicKey: b64url(key.export({ type: 'spki', format: 'der' })),
307
+ alg,
308
+ counter: authData.counter,
309
+ };
310
+ }
311
+ /**
312
+ * Verify a navigator.credentials.get() response against a stored public key.
313
+ * The signature covers authenticatorData plus the hash of clientDataJSON,
314
+ * which is what binds the challenge and the origin to the key.
315
+ */
316
+ function verifyAssertion(input) {
317
+ checkClientData(input.clientDataJSON, {
318
+ type: 'webauthn.get',
319
+ challenge: input.challenge,
320
+ origin: input.origin,
321
+ });
322
+ const authData = parseAuthData(input.authenticatorData);
323
+ if (!rpIdHashOk(authData, input.rpId)) {
324
+ throw new WebAuthnError('the assertion was made for a different hostname');
325
+ }
326
+ if (!authData.userPresent)
327
+ throw new WebAuthnError('the authenticator did not report user presence');
328
+ let key;
329
+ try {
330
+ key = crypto.createPublicKey({ key: fromB64url(input.publicKey), format: 'der', type: 'spki' });
331
+ }
332
+ catch {
333
+ throw new WebAuthnError('the stored public key could not be read');
334
+ }
335
+ const signed = Buffer.concat([
336
+ input.authenticatorData,
337
+ crypto.createHash('sha256').update(input.clientDataJSON).digest(),
338
+ ]);
339
+ // ES256 and RS256 hash before signing and their WebAuthn signatures are
340
+ // ASN.1 DER, which is node's default; Ed25519 signs the message itself.
341
+ const ok = input.alg === exports.COSE_EDDSA
342
+ ? crypto.verify(null, signed, key, input.signature)
343
+ : crypto.verify('sha256', signed, key, input.signature);
344
+ if (!ok)
345
+ throw new WebAuthnError('the signature does not verify');
346
+ return { counter: authData.counter, userVerified: authData.userVerified };
347
+ }