@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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
package/dist/lfs.js ADDED
@@ -0,0 +1,438 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerLfs = registerLfs;
40
+ const express_1 = __importDefault(require("express"));
41
+ const crypto = __importStar(require("crypto"));
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const stream_1 = require("stream");
45
+ const promises_1 = require("stream/promises");
46
+ const githttp_1 = require("./githttp");
47
+ const lfsstore_1 = require("./lfsstore");
48
+ const scan_1 = require("./scan");
49
+ const web_1 = require("./web");
50
+ // The server side of the Git LFS Batch API, plus the two transfer routes used
51
+ // only by the local backend. The Batch API lets the server return an
52
+ // arbitrary URL per object transfer, so with the s3 backend we return
53
+ // presigned bucket URLs and large-file bytes never pass through this process.
54
+ //
55
+ // git-lfs derives its endpoint by appending `.git/info/lfs` to the remote
56
+ // URL; findRepo strips the optional `.git`, so both spellings resolve and no
57
+ // client-side configuration is needed.
58
+ const MAX_BATCH_OBJECTS = 1000;
59
+ const HEAD_CONCURRENCY = 8;
60
+ // Object-id validation is the security boundary of this module: every oid
61
+ // must match this before it is used to build a key or path, playing the same
62
+ // role for object ids that isValidName plays for collection and repo names.
63
+ const OID_RE = /^[0-9a-f]{64}$/;
64
+ function sendLfs(res, status, body) {
65
+ res
66
+ .status(status)
67
+ .set('Content-Type', 'application/vnd.git-lfs+json; charset=utf-8')
68
+ // Batch responses carry short-lived signed URLs; nothing in front of the
69
+ // server should hold on to them.
70
+ .set('Cache-Control', 'no-store')
71
+ .send(JSON.stringify(body));
72
+ }
73
+ function lfsError(res, status, message) {
74
+ sendLfs(res, status, { message });
75
+ }
76
+ // A reader cancelling a large download, or a client dropping mid-upload, is
77
+ // ordinary traffic for this module rather than a fault, and must not fill the
78
+ // log with stack traces.
79
+ function isClientAbort(e) {
80
+ const code = e?.code;
81
+ return code === 'ERR_STREAM_PREMATURE_CLOSE' || code === 'ECONNRESET' || code === 'ECANCELED';
82
+ }
83
+ // Async-handler wrapper that reports failures in the LFS error shape rather
84
+ // than the HTML error page.
85
+ function lah(fn) {
86
+ return (req, res) => {
87
+ fn(req, res).catch((e) => {
88
+ if (isClientAbort(e)) {
89
+ res.end();
90
+ return;
91
+ }
92
+ console.error(e);
93
+ if (!res.headersSent)
94
+ lfsError(res, 500, 'internal error');
95
+ else
96
+ res.end();
97
+ });
98
+ };
99
+ }
100
+ async function mapLimit(items, limit, fn) {
101
+ const out = new Array(items.length);
102
+ let next = 0;
103
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
104
+ for (;;) {
105
+ const i = next++;
106
+ if (i >= items.length)
107
+ return;
108
+ out[i] = await fn(items[i]);
109
+ }
110
+ });
111
+ await Promise.all(workers);
112
+ return out;
113
+ }
114
+ // The local backend signs relative hrefs (it does not know the request host);
115
+ // prefix the request's own base URL. Bucket hrefs are already absolute.
116
+ function action(a, base) {
117
+ return { href: a.href.startsWith('/') ? base + a.href : a.href, header: a.header, expires_in: a.expiresIn };
118
+ }
119
+ class SizeExceeded extends Error {
120
+ }
121
+ function registerLfs(app, root, lfs, authLimiter) {
122
+ const store = lfs?.store ?? null;
123
+ const maxSize = lfs?.maxSize ?? 0;
124
+ // Lenient about the request content type: git-lfs sends
125
+ // application/vnd.git-lfs+json, curl in tests may not.
126
+ const json = express_1.default.json({ type: () => true, limit: '5mb' });
127
+ // A body that will not parse is a malformed request, and must be reported
128
+ // in the LFS error shape rather than falling through to the HTML error
129
+ // page the app-level handler would render.
130
+ const jsonErr = (err, _req, res, next) => {
131
+ if (err) {
132
+ lfsError(res, 422, `request body is not valid JSON: ${err.message}`);
133
+ return;
134
+ }
135
+ next();
136
+ };
137
+ function unavailable(res) {
138
+ if (store)
139
+ return false;
140
+ lfsError(res, 501, 'Git LFS is not enabled on this vault');
141
+ return true;
142
+ }
143
+ function sendDenied(res, check) {
144
+ if (check.status === 401) {
145
+ // LFS-Authenticate is the header git-lfs looks for.
146
+ res.setHeader('LFS-Authenticate', 'Basic realm="mochi"');
147
+ res.setHeader('WWW-Authenticate', 'Basic realm="mochi"');
148
+ }
149
+ if (check.retryAfter !== undefined)
150
+ res.setHeader('Retry-After', String(check.retryAfter));
151
+ lfsError(res, check.status, check.message);
152
+ }
153
+ // Upload (and verify) reuse the push authorization.
154
+ function requireUploadAuth(req, res, repo) {
155
+ const check = (0, githttp_1.checkPushAuth)(root, authLimiter, req, repo.collection, repo.name, repo);
156
+ if (!check.ok) {
157
+ sendDenied(res, check);
158
+ return false;
159
+ }
160
+ return true;
161
+ }
162
+ /**
163
+ * The repository these LFS objects belong to, readable by this request, or
164
+ * null having sent the refusal. Reading follows the git wire's rule
165
+ * (checkReadAuth): public repositories serve anonymously, and a private one
166
+ * answers exactly as an absent one does.
167
+ */
168
+ function requireRepoRead(req, res) {
169
+ const repo = (0, scan_1.findRepo)(root, req.params.collection, req.params.repo);
170
+ const check = (0, githttp_1.checkReadAuth)(root, authLimiter, req, repo);
171
+ if (!check.ok) {
172
+ sendDenied(res, check);
173
+ return null;
174
+ }
175
+ if (!repo) {
176
+ // Unreachable: checkReadAuth answers ok only for a repository it saw.
177
+ lfsError(res, 404, 'repository not found');
178
+ return null;
179
+ }
180
+ return repo;
181
+ }
182
+ // ---- Batch API ----
183
+ app.post('/:collection/:repo/info/lfs/objects/batch', json, jsonErr, lah(async (req, res) => {
184
+ if (unavailable(res) || !store)
185
+ return;
186
+ const repo = requireRepoRead(req, res);
187
+ if (!repo)
188
+ return;
189
+ const body = (req.body ?? {});
190
+ const operation = body.operation;
191
+ if (operation !== 'download' && operation !== 'upload') {
192
+ lfsError(res, 422, 'operation must be "download" or "upload"');
193
+ return;
194
+ }
195
+ const transfers = body.transfers;
196
+ if (transfers !== undefined && !(Array.isArray(transfers) && transfers.includes('basic'))) {
197
+ lfsError(res, 422, 'only the "basic" transfer adapter is supported');
198
+ return;
199
+ }
200
+ if (body.hash_algo !== undefined && body.hash_algo !== 'sha256') {
201
+ lfsError(res, 422, 'only the sha256 hash algorithm is supported');
202
+ return;
203
+ }
204
+ const objects = body.objects;
205
+ if (!Array.isArray(objects)) {
206
+ lfsError(res, 422, '"objects" must be an array');
207
+ return;
208
+ }
209
+ if (objects.length > MAX_BATCH_OBJECTS) {
210
+ lfsError(res, 422, `batches are limited to ${MAX_BATCH_OBJECTS} objects`);
211
+ return;
212
+ }
213
+ const parsed = [];
214
+ for (const o of objects) {
215
+ const rec = (typeof o === 'object' && o !== null ? o : {});
216
+ if (typeof rec.oid !== 'string' || !OID_RE.test(rec.oid)) {
217
+ lfsError(res, 422, 'invalid object id (expected 64 lowercase hex characters)');
218
+ return;
219
+ }
220
+ if (typeof rec.size !== 'number' || !Number.isSafeInteger(rec.size) || rec.size < 0) {
221
+ lfsError(res, 422, 'invalid object size');
222
+ return;
223
+ }
224
+ parsed.push({ oid: rec.oid, size: rec.size });
225
+ }
226
+ // Download needs no more than the read access requireRepoRead already
227
+ // established, matching anonymous clone: a public repository must
228
+ // support `git clone` plus `git lfs pull` with no credentials.
229
+ if (operation === 'upload' && !requireUploadAuth(req, res, repo))
230
+ return;
231
+ const base = (0, web_1.baseUrlOf)(req);
232
+ const authz = req.get('authorization');
233
+ // One storage lookup per distinct object id. Download is anonymous, so
234
+ // without this a single request repeating one oid a thousand times
235
+ // would fan out to a thousand bucket operations.
236
+ const distinct = [...new Map(parsed.map((o) => [o.oid, o])).values()];
237
+ const results = await mapLimit(distinct, HEAD_CONCURRENCY, async (o) => {
238
+ const info = await store.head(repo.collection, repo.name, o.oid);
239
+ if (operation === 'download') {
240
+ if (!info) {
241
+ // A presigned URL for a missing object would surface as a
242
+ // confusing mid-transfer failure; fail the object up front.
243
+ return { oid: o.oid, size: o.size, error: { code: 404, message: 'object not found' } };
244
+ }
245
+ const dl = await store.signDownload(repo.collection, repo.name, o.oid);
246
+ return {
247
+ oid: o.oid,
248
+ size: info.size,
249
+ authenticated: true,
250
+ actions: { download: action(dl, base) },
251
+ };
252
+ }
253
+ // Already present: omitting actions tells the client to skip the
254
+ // upload; this is the deduplication mechanism.
255
+ if (info)
256
+ return { oid: o.oid, size: o.size, authenticated: true };
257
+ if (o.size > maxSize) {
258
+ return {
259
+ oid: o.oid,
260
+ size: o.size,
261
+ error: { code: 422, message: `object exceeds the maximum size of ${maxSize} bytes` },
262
+ };
263
+ }
264
+ const up = await store.signUpload(repo.collection, repo.name, o.oid, o.size);
265
+ return {
266
+ oid: o.oid,
267
+ size: o.size,
268
+ authenticated: true,
269
+ actions: {
270
+ upload: action(up, base),
271
+ verify: {
272
+ href: `${base}/${encodeURIComponent(repo.collection)}/${encodeURIComponent(repo.name)}/info/lfs/objects/verify`,
273
+ header: authz ? { Authorization: authz } : {},
274
+ expires_in: up.expiresIn,
275
+ },
276
+ },
277
+ };
278
+ });
279
+ // Answer in the shape the client asked in: one entry per requested
280
+ // object, in request order, even though repeats shared a lookup.
281
+ const byOid = new Map(results.map((r) => [r.oid, r]));
282
+ const out = parsed.map((o) => byOid.get(o.oid));
283
+ sendLfs(res, 200, { transfer: 'basic', objects: out, hash_algo: 'sha256' });
284
+ }));
285
+ // ---- verify endpoint ----
286
+ // Upload bytes bypass the server entirely in the s3 configuration, so this
287
+ // is the only integrity check available there.
288
+ app.post('/:collection/:repo/info/lfs/objects/verify', json, jsonErr, lah(async (req, res) => {
289
+ if (unavailable(res) || !store)
290
+ return;
291
+ const repo = requireRepoRead(req, res);
292
+ if (!repo)
293
+ return;
294
+ const body = (req.body ?? {});
295
+ const oid = body.oid;
296
+ const size = body.size;
297
+ if (typeof oid !== 'string' || !OID_RE.test(oid)) {
298
+ lfsError(res, 422, 'invalid object id (expected 64 lowercase hex characters)');
299
+ return;
300
+ }
301
+ if (typeof size !== 'number' || !Number.isSafeInteger(size) || size < 0) {
302
+ lfsError(res, 422, 'invalid object size');
303
+ return;
304
+ }
305
+ if (!requireUploadAuth(req, res, repo))
306
+ return;
307
+ const info = await store.head(repo.collection, repo.name, oid);
308
+ if (!info) {
309
+ lfsError(res, 404, 'object not found');
310
+ return;
311
+ }
312
+ if (info.size !== size) {
313
+ lfsError(res, 422, `size mismatch: expected ${size} bytes, stored object has ${info.size}`);
314
+ return;
315
+ }
316
+ sendLfs(res, 200, { oid, size });
317
+ }));
318
+ function checkTransfer(req, res, op) {
319
+ if (unavailable(res) || !store)
320
+ return null;
321
+ const repo = (0, scan_1.findRepo)(root, req.params.collection, req.params.repo);
322
+ if (!repo) {
323
+ lfsError(res, 404, 'repository not found');
324
+ return null;
325
+ }
326
+ const oid = req.params.oid;
327
+ if (!OID_RE.test(oid)) {
328
+ lfsError(res, 422, 'invalid object id');
329
+ return null;
330
+ }
331
+ if (store.kind !== 'local') {
332
+ lfsError(res, 400, 'this vault stores LFS objects in a bucket; transfer URLs point at the bucket');
333
+ return null;
334
+ }
335
+ // exp must be exactly the digits that were signed, not merely something
336
+ // parseInt can salvage from them: the signature covers String(exp), so
337
+ // accepting "1700000000zzz" would put a byte in the URL that no signature
338
+ // covers, contradicting the invariant the scheme rests on.
339
+ const rawExp = typeof req.query.exp === 'string' ? req.query.exp : '';
340
+ const sig = typeof req.query.sig === 'string' ? req.query.sig : '';
341
+ const disp = typeof req.query.disp === 'string' ? req.query.disp : '';
342
+ if (!/^\d{1,15}$/.test(rawExp)) {
343
+ lfsError(res, 403, 'malformed transfer URL');
344
+ return null;
345
+ }
346
+ const exp = parseInt(rawExp, 10);
347
+ if (exp < Math.floor(Date.now() / 1000)) {
348
+ lfsError(res, 403, 'this transfer URL has expired');
349
+ return null;
350
+ }
351
+ if (!(0, lfsstore_1.verifyTransfer)(root, op, repo.collection, repo.name, oid, exp, disp, sig)) {
352
+ lfsError(res, 403, 'invalid transfer signature');
353
+ return null;
354
+ }
355
+ return { collection: repo.collection, repo: repo.name, oid, disp };
356
+ }
357
+ app.get('/:collection/:repo/info/lfs/objects/:oid', lah(async (req, res) => {
358
+ const t = checkTransfer(req, res, 'download');
359
+ if (!t)
360
+ return;
361
+ const file = (0, lfsstore_1.localObjectPath)(root, t.collection, t.repo, t.oid);
362
+ let st;
363
+ try {
364
+ st = fs.statSync(file);
365
+ }
366
+ catch {
367
+ lfsError(res, 404, 'object not found');
368
+ return;
369
+ }
370
+ // These bytes are repository content served from the vault's own
371
+ // origin, so they get the same treatment as the raw route in
372
+ // src/browse.ts: a sandbox CSP and an attachment disposition, so an
373
+ // uploaded HTML or SVG payload can never run as script here. The
374
+ // filename is only present on hrefs the raw route signed.
375
+ res.setHeader('Content-Type', 'application/octet-stream');
376
+ res.setHeader('Content-Length', String(st.size));
377
+ res.setHeader('Content-Security-Policy', 'sandbox');
378
+ res.setHeader('X-Content-Type-Options', 'nosniff');
379
+ res.setHeader('Content-Disposition', t.disp === '' ? 'attachment' : `attachment; filename="${t.disp}"`);
380
+ await (0, promises_1.pipeline)(fs.createReadStream(file), res);
381
+ }));
382
+ app.put('/:collection/:repo/info/lfs/objects/:oid', lah(async (req, res) => {
383
+ const t = checkTransfer(req, res, 'upload');
384
+ if (!t)
385
+ return;
386
+ const file = (0, lfsstore_1.localObjectPath)(root, t.collection, t.repo, t.oid);
387
+ const dir = path.dirname(file);
388
+ fs.mkdirSync(dir, { recursive: true });
389
+ // Stream to a temporary file and rename into place, so a failed or
390
+ // interrupted upload never leaves a corrupt object at the final path.
391
+ // The rename is atomic, so two clients uploading the same object
392
+ // concurrently are harmless: the content is identical by construction.
393
+ const tmp = path.join(dir, `tmp-${crypto.randomBytes(8).toString('hex')}`);
394
+ const hash = crypto.createHash('sha256');
395
+ let bytes = 0;
396
+ const hasher = new stream_1.Transform({
397
+ transform(chunk, _enc, cb) {
398
+ bytes += chunk.length;
399
+ // The signature binds the oid but not a size, so cap what a signed
400
+ // URL can write to disk.
401
+ if (bytes > maxSize) {
402
+ cb(new SizeExceeded());
403
+ return;
404
+ }
405
+ hash.update(chunk);
406
+ cb(null, chunk);
407
+ },
408
+ });
409
+ try {
410
+ await (0, promises_1.pipeline)(req, hasher, fs.createWriteStream(tmp, { flags: 'wx', mode: 0o600 }));
411
+ }
412
+ catch (e) {
413
+ fs.rmSync(tmp, { force: true });
414
+ if (e instanceof SizeExceeded) {
415
+ lfsError(res, 422, `object exceeds the maximum size of ${maxSize} bytes`);
416
+ return;
417
+ }
418
+ throw e;
419
+ }
420
+ const digest = hash.digest('hex');
421
+ if (digest !== t.oid) {
422
+ fs.rmSync(tmp, { force: true });
423
+ lfsError(res, 422, `content hashes to ${digest}, not the requested object id`);
424
+ return;
425
+ }
426
+ fs.renameSync(tmp, file);
427
+ res.status(200).end();
428
+ }));
429
+ // ---- locking API: deliberately unsupported ----
430
+ // git-lfs probes /locks/verify at push time and treats a 404 as "locking
431
+ // not supported", which is the answer we want.
432
+ app.all('/:collection/:repo/info/lfs/locks', (_req, res) => {
433
+ lfsError(res, 404, 'the LFS file locking API is not supported');
434
+ });
435
+ app.all('/:collection/:repo/info/lfs/locks/*', (_req, res) => {
436
+ lfsError(res, 404, 'the LFS file locking API is not supported');
437
+ });
438
+ }