@formancy/server 0.2.0 → 0.4.0
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/dist/{file-collector-WWc-eo8h.mjs → file-collector-CGSJAtGO.mjs} +223 -142
- package/dist/file-collector-CGSJAtGO.mjs.map +1 -0
- package/dist/index.d.mts +14 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/main.mjs +109 -1
- package/dist/main.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/file-collector-WWc-eo8h.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { auditedBy, authenticateApiKey, authenticateLocal, can, collectAbandonedFiles, createApiKey, createLocalUser, createSubmission, decodeSolution, exportCsv, healthOf, listForms, listSubmissions, listVersions, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, saveDraft, screenUpload, setFormAccess, startDraft, verifySolution } from "@formancy/server-core";
|
|
2
3
|
import Fastify from "fastify";
|
|
3
4
|
import rateLimit from "@fastify/rate-limit";
|
|
4
|
-
import { auditedBy, authenticateApiKey, authenticateLocal, can, collectAbandonedFiles, createApiKey, createLocalUser, createSubmission, decodeSolution, exportCsv, healthOf, listForms, listSubmissions, listVersions, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, saveDraft, setFormAccess, startDraft, verifySolution } from "@formancy/server-core";
|
|
5
5
|
import { hash, verify } from "@node-rs/argon2";
|
|
6
6
|
import { SignJWT, jwtVerify } from "jose";
|
|
7
7
|
import { and, desc, eq, inArray, lt, lte, max, ne } from "drizzle-orm";
|
|
@@ -10,6 +10,216 @@ import { integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-c
|
|
|
10
10
|
import { createReadStream } from "node:fs";
|
|
11
11
|
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
12
12
|
import { dirname, join, resolve, sep } from "node:path";
|
|
13
|
+
//#region src/routes/files.ts
|
|
14
|
+
/**
|
|
15
|
+
* A form's files: offering one, receiving its bytes, and serving it back.
|
|
16
|
+
*
|
|
17
|
+
* One route family per plugin, the seam `app.ts`'s size budget names. It moved when
|
|
18
|
+
* scanning an upload's bytes needed lines that file had no room for.
|
|
19
|
+
*/
|
|
20
|
+
async function fileRoutes(app, { deps, fileStore, maxFileBytes, limit, actorOf }) {
|
|
21
|
+
const { storage } = deps;
|
|
22
|
+
const submissionLimit = limit;
|
|
23
|
+
/**
|
|
24
|
+
* Ask where to put a file.
|
|
25
|
+
*
|
|
26
|
+
* Refusing here, before a byte is sent, is the difference between rejecting
|
|
27
|
+
* a 2 GB upload and receiving one first. The reply carries the id the
|
|
28
|
+
* submission will reference and the URL to PUT the bytes to.
|
|
29
|
+
*/
|
|
30
|
+
app.post("/f/:path/files", { config: { rateLimit: {
|
|
31
|
+
max: submissionLimit.max,
|
|
32
|
+
timeWindow: submissionLimit.timeWindowMs
|
|
33
|
+
} } }, async (request, reply) => {
|
|
34
|
+
if (fileStore === void 0) return reply.code(501).send({
|
|
35
|
+
error: "uploads_unavailable",
|
|
36
|
+
message: "This deployment has no file store configured, so it cannot accept uploads."
|
|
37
|
+
});
|
|
38
|
+
const { path } = request.params;
|
|
39
|
+
const resolved = await resolveForm(deps, path);
|
|
40
|
+
if (resolved === void 0) return reply.code(404).send({ error: "unknown_form" });
|
|
41
|
+
const form = await storage.getFormByPath(path);
|
|
42
|
+
if (form === void 0) return reply.code(404).send({ error: "unknown_form" });
|
|
43
|
+
const actor = await actorOf(request);
|
|
44
|
+
const origin = request.headers.origin;
|
|
45
|
+
const body = request.body ?? {};
|
|
46
|
+
const field = typeof body.field === "string" ? body.field : void 0;
|
|
47
|
+
const name = typeof body.name === "string" ? body.name : void 0;
|
|
48
|
+
const size = typeof body.size === "number" ? body.size : void 0;
|
|
49
|
+
const contentType = typeof body.contentType === "string" ? body.contentType : "application/octet-stream";
|
|
50
|
+
if (field === void 0 || name === void 0 || size === void 0) return reply.code(400).send({
|
|
51
|
+
error: "invalid_request",
|
|
52
|
+
message: "Send the field, the name and the size of the file you want to upload."
|
|
53
|
+
});
|
|
54
|
+
if (size > maxFileBytes) return reply.code(413).send({
|
|
55
|
+
error: "too_large",
|
|
56
|
+
message: `This deployment accepts files up to ${String(maxFileBytes)} bytes.`
|
|
57
|
+
});
|
|
58
|
+
const outcome = await offerUpload({
|
|
59
|
+
storage,
|
|
60
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
61
|
+
newId: () => randomUUID()
|
|
62
|
+
}, {
|
|
63
|
+
schema: resolved.schema,
|
|
64
|
+
form,
|
|
65
|
+
actor: actor === void 0 ? "anonymous" : "authenticated",
|
|
66
|
+
origin: typeof origin === "string" ? origin : void 0,
|
|
67
|
+
field,
|
|
68
|
+
name,
|
|
69
|
+
size,
|
|
70
|
+
contentType
|
|
71
|
+
});
|
|
72
|
+
if (!outcome.ok) {
|
|
73
|
+
const status = outcome.reason === "not_allowed" ? 403 : outcome.reason === "too_large" ? 413 : 400;
|
|
74
|
+
return reply.code(status).send({ error: outcome.reason });
|
|
75
|
+
}
|
|
76
|
+
return reply.code(201).send({
|
|
77
|
+
id: outcome.file.id,
|
|
78
|
+
name: outcome.file.name,
|
|
79
|
+
size: outcome.file.size,
|
|
80
|
+
contentType: outcome.file.contentType,
|
|
81
|
+
storageKey: outcome.file.storageKey,
|
|
82
|
+
uploadUrl: `/f/${encodeURIComponent(path)}/files/${outcome.file.id}`
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
/** Receive the bytes for a file that was offered. */
|
|
86
|
+
app.put("/f/:path/files/:id", async (request, reply) => {
|
|
87
|
+
if (fileStore === void 0) return reply.code(501).send({ error: "uploads_unavailable" });
|
|
88
|
+
const { path, id } = request.params;
|
|
89
|
+
const form = await storage.getFormByPath(path);
|
|
90
|
+
if (form === void 0) return reply.code(404).send({ error: "unknown_form" });
|
|
91
|
+
const file = await storage.getFile(id);
|
|
92
|
+
if (file === void 0 || file.formId !== form.id) return reply.code(404).send({ error: "unknown_file" });
|
|
93
|
+
if (file.state !== "offered") return reply.code(409).send({ error: "already_uploaded" });
|
|
94
|
+
const bytes = request.body;
|
|
95
|
+
if (!Buffer.isBuffer(bytes)) return reply.code(400).send({ error: "no_body" });
|
|
96
|
+
if (bytes.byteLength > maxFileBytes) return reply.code(413).send({ error: "too_large" });
|
|
97
|
+
if (bytes.byteLength !== file.size) return reply.code(400).send({
|
|
98
|
+
error: "size_mismatch",
|
|
99
|
+
message: `This upload was offered as ${String(file.size)} bytes and ${String(bytes.byteLength)} arrived.`
|
|
100
|
+
});
|
|
101
|
+
const screened = await screenUpload(deps.scanner, file, bytes);
|
|
102
|
+
if (!screened.ok) {
|
|
103
|
+
if (screened.reason === "refused") {
|
|
104
|
+
request.log.warn({
|
|
105
|
+
file: file.id,
|
|
106
|
+
finding: screened.finding
|
|
107
|
+
}, "upload refused by the scanner");
|
|
108
|
+
return reply.code(422).send({
|
|
109
|
+
error: "refused_by_scanner",
|
|
110
|
+
message: `This file was refused by the deployment's virus scanner (${screened.finding}).`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
request.log.error({
|
|
114
|
+
file: file.id,
|
|
115
|
+
cause: screened.cause
|
|
116
|
+
}, "the scanner could not be asked");
|
|
117
|
+
return reply.code(503).send({
|
|
118
|
+
error: "scanner_unavailable",
|
|
119
|
+
message: "This file could not be scanned just now, so it was not kept. Try again."
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
await fileStore.put(file.storageKey, bytes);
|
|
123
|
+
await storage.updateFile({
|
|
124
|
+
...file,
|
|
125
|
+
state: "stored"
|
|
126
|
+
});
|
|
127
|
+
return reply.code(204).send();
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Serve a file back.
|
|
131
|
+
*
|
|
132
|
+
* Always as an attachment, never inline, and `nosniff` alongside it. Stored
|
|
133
|
+
* cross-site scripting through an uploaded HTML or SVG file is the most
|
|
134
|
+
* commonly exploited vulnerability in this product category, and the real
|
|
135
|
+
* answer is a separate hostname — which a single-container deployment does
|
|
136
|
+
* not have. Forcing a download is the accommodation, and it is the one place
|
|
137
|
+
* this deployment model genuinely costs something.
|
|
138
|
+
*/
|
|
139
|
+
app.get("/f/:path/files/:id", async (request, reply) => {
|
|
140
|
+
if (fileStore === void 0) return reply.code(501).send({ error: "uploads_unavailable" });
|
|
141
|
+
const { path, id } = request.params;
|
|
142
|
+
if (await actorOf(request) === void 0) return reply.code(401).send({ error: "unauthenticated" });
|
|
143
|
+
const form = await storage.getFormByPath(path);
|
|
144
|
+
const file = await storage.getFile(id);
|
|
145
|
+
if (form === void 0 || file === void 0 || file.formId !== form.id) return reply.code(404).send({ error: "unknown_file" });
|
|
146
|
+
const stream = await fileStore.open(file.storageKey);
|
|
147
|
+
if (stream === void 0) return reply.code(404).send({ error: "unknown_file" });
|
|
148
|
+
return reply.header("content-type", "application/octet-stream").header("x-content-type-options", "nosniff").header("content-security-policy", "default-src 'none'; sandbox").header("content-disposition", `attachment; filename="${file.name.replace(/["\r\n]/g, "")}"`).send(stream);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/headers.ts
|
|
153
|
+
/**
|
|
154
|
+
* The header a client declares what it was looking at with.
|
|
155
|
+
*
|
|
156
|
+
* One name, read by the submission route and by the publish plugin: a
|
|
157
|
+
* submission says which version it rendered, and a publish says which version it
|
|
158
|
+
* opened. Two spellings of one header is a bug nobody sees until a client sends
|
|
159
|
+
* the other one.
|
|
160
|
+
*/
|
|
161
|
+
const SCHEMA_HASH_HEADER = "x-formancy-schema-hash";
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/routes/publish.ts
|
|
164
|
+
/**
|
|
165
|
+
* Publishing a form, as a plugin of its own.
|
|
166
|
+
*
|
|
167
|
+
* One route family per plugin, which is Fastify's own unit and what `app.ts`'s
|
|
168
|
+
* size budget named as the seam. It moved when the budget refused the next thing
|
|
169
|
+
* added to it rather than letting the number drift.
|
|
170
|
+
*/
|
|
171
|
+
async function publishRoutes(app, { deps, requires }) {
|
|
172
|
+
app.post("/forms", { preHandler: requires("form.publish") }, async (request, reply) => {
|
|
173
|
+
const body = request.body;
|
|
174
|
+
if (body === null || typeof body.path !== "string" || body.path === "" || body.schema === void 0) return reply.code(400).send({
|
|
175
|
+
error: "bad_request",
|
|
176
|
+
message: "Body needs { path, schema }."
|
|
177
|
+
});
|
|
178
|
+
const basedOn = request.headers[SCHEMA_HASH_HEADER];
|
|
179
|
+
const outcome = await publishForm(deps, {
|
|
180
|
+
path: body.path,
|
|
181
|
+
schema: body.schema,
|
|
182
|
+
actor: request.actor,
|
|
183
|
+
...typeof basedOn === "string" && basedOn !== "" ? { basedOnSchemaHash: basedOn } : {}
|
|
184
|
+
});
|
|
185
|
+
if (!outcome.ok) switch (outcome.kind) {
|
|
186
|
+
case "invalid_schema": return reply.code(422).send({
|
|
187
|
+
error: outcome.kind,
|
|
188
|
+
errors: outcome.errors
|
|
189
|
+
});
|
|
190
|
+
case "unsafe_pattern": return reply.code(422).send({
|
|
191
|
+
error: outcome.kind,
|
|
192
|
+
patterns: outcome.patterns
|
|
193
|
+
});
|
|
194
|
+
case "invalid_logic": return reply.code(422).send({
|
|
195
|
+
error: outcome.kind,
|
|
196
|
+
message: outcome.message
|
|
197
|
+
});
|
|
198
|
+
case "unknown_options_source": return reply.code(422).send({
|
|
199
|
+
error: outcome.kind,
|
|
200
|
+
sources: outcome.sources
|
|
201
|
+
});
|
|
202
|
+
case "already_published": return reply.code(409).send({
|
|
203
|
+
error: outcome.kind,
|
|
204
|
+
version: outcome.version
|
|
205
|
+
});
|
|
206
|
+
case "version_changed": return reply.code(409).send({
|
|
207
|
+
error: "FORM_VERSION_CHANGED",
|
|
208
|
+
current: outcome.current === void 0 ? void 0 : {
|
|
209
|
+
version: outcome.current.version,
|
|
210
|
+
schemaHash: outcome.current.schemaHash,
|
|
211
|
+
schema: outcome.current.schema
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return reply.code(201).send({
|
|
216
|
+
version: outcome.version,
|
|
217
|
+
schemaHash: outcome.schemaHash,
|
|
218
|
+
...outcome.warnings.length > 0 ? { warnings: outcome.warnings } : {}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
13
223
|
//#region src/auth-runtime.ts
|
|
14
224
|
/**
|
|
15
225
|
* The real cryptography, confined to the composition root: argon2id for
|
|
@@ -67,7 +277,6 @@ function createSessionTokens(secret) {
|
|
|
67
277
|
}
|
|
68
278
|
//#endregion
|
|
69
279
|
//#region src/app.ts
|
|
70
|
-
const SCHEMA_HASH_HEADER = "x-formancy-schema-hash";
|
|
71
280
|
/** Base64 JSON, the shape an ALTCHA client already produces. */
|
|
72
281
|
const CHALLENGE_HEADER = "x-formancy-challenge";
|
|
73
282
|
/** Proves the bearer started this draft. Lower-case: Fastify normalises. */
|
|
@@ -121,6 +330,7 @@ async function createApp(storage, options) {
|
|
|
121
330
|
nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
122
331
|
draftSecret: options.authSecret,
|
|
123
332
|
...options.optionsSources === void 0 ? {} : { optionsSources: options.optionsSources },
|
|
333
|
+
...options.scanner === void 0 ? {} : { scanner: options.scanner },
|
|
124
334
|
capabilities: {
|
|
125
335
|
now: () => Date.now(),
|
|
126
336
|
today: () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
@@ -261,39 +471,9 @@ async function createApp(storage, options) {
|
|
|
261
471
|
secret: outcome.secret
|
|
262
472
|
});
|
|
263
473
|
});
|
|
264
|
-
app.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
error: "bad_request",
|
|
268
|
-
message: "Body needs { path, schema }."
|
|
269
|
-
});
|
|
270
|
-
const outcome = await publishForm(deps, {
|
|
271
|
-
path: body.path,
|
|
272
|
-
schema: body.schema,
|
|
273
|
-
actor: request.actor
|
|
274
|
-
});
|
|
275
|
-
if (!outcome.ok) switch (outcome.kind) {
|
|
276
|
-
case "invalid_schema": return reply.code(422).send({
|
|
277
|
-
error: outcome.kind,
|
|
278
|
-
errors: outcome.errors
|
|
279
|
-
});
|
|
280
|
-
case "unsafe_pattern": return reply.code(422).send({
|
|
281
|
-
error: outcome.kind,
|
|
282
|
-
patterns: outcome.patterns
|
|
283
|
-
});
|
|
284
|
-
case "invalid_logic": return reply.code(422).send({
|
|
285
|
-
error: outcome.kind,
|
|
286
|
-
message: outcome.message
|
|
287
|
-
});
|
|
288
|
-
case "unknown_options_source": return reply.code(422).send({
|
|
289
|
-
error: outcome.kind,
|
|
290
|
-
sources: outcome.sources
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
return reply.code(201).send({
|
|
294
|
-
version: outcome.version,
|
|
295
|
-
schemaHash: outcome.schemaHash
|
|
296
|
-
});
|
|
474
|
+
await app.register(publishRoutes, {
|
|
475
|
+
deps,
|
|
476
|
+
requires
|
|
297
477
|
});
|
|
298
478
|
app.put("/f/:path/access", { preHandler: requires("form.publish") }, async (request, reply) => {
|
|
299
479
|
const { path } = request.params;
|
|
@@ -548,111 +728,12 @@ async function createApp(storage, options) {
|
|
|
548
728
|
if (resumed === void 0) return reply.code(404).send({ error: "unknown_draft" });
|
|
549
729
|
return reply.send(resumed);
|
|
550
730
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*/
|
|
558
|
-
app.post("/f/:path/files", { config: { rateLimit: {
|
|
559
|
-
max: submissionLimit.max,
|
|
560
|
-
timeWindow: submissionLimit.timeWindowMs
|
|
561
|
-
} } }, async (request, reply) => {
|
|
562
|
-
if (fileStore === void 0) return reply.code(501).send({
|
|
563
|
-
error: "uploads_unavailable",
|
|
564
|
-
message: "This deployment has no file store configured, so it cannot accept uploads."
|
|
565
|
-
});
|
|
566
|
-
const { path } = request.params;
|
|
567
|
-
const resolved = await resolveForm(deps, path);
|
|
568
|
-
if (resolved === void 0) return reply.code(404).send({ error: "unknown_form" });
|
|
569
|
-
const form = await storage.getFormByPath(path);
|
|
570
|
-
if (form === void 0) return reply.code(404).send({ error: "unknown_form" });
|
|
571
|
-
const actor = await actorOf(request);
|
|
572
|
-
const origin = request.headers.origin;
|
|
573
|
-
const body = request.body ?? {};
|
|
574
|
-
const field = typeof body.field === "string" ? body.field : void 0;
|
|
575
|
-
const name = typeof body.name === "string" ? body.name : void 0;
|
|
576
|
-
const size = typeof body.size === "number" ? body.size : void 0;
|
|
577
|
-
const contentType = typeof body.contentType === "string" ? body.contentType : "application/octet-stream";
|
|
578
|
-
if (field === void 0 || name === void 0 || size === void 0) return reply.code(400).send({
|
|
579
|
-
error: "invalid_request",
|
|
580
|
-
message: "Send the field, the name and the size of the file you want to upload."
|
|
581
|
-
});
|
|
582
|
-
if (size > maxFileBytes) return reply.code(413).send({
|
|
583
|
-
error: "too_large",
|
|
584
|
-
message: `This deployment accepts files up to ${String(maxFileBytes)} bytes.`
|
|
585
|
-
});
|
|
586
|
-
const outcome = await offerUpload({
|
|
587
|
-
storage,
|
|
588
|
-
now: () => /* @__PURE__ */ new Date(),
|
|
589
|
-
newId: () => randomUUID()
|
|
590
|
-
}, {
|
|
591
|
-
schema: resolved.schema,
|
|
592
|
-
form,
|
|
593
|
-
actor: actor === void 0 ? "anonymous" : "authenticated",
|
|
594
|
-
origin: typeof origin === "string" ? origin : void 0,
|
|
595
|
-
field,
|
|
596
|
-
name,
|
|
597
|
-
size,
|
|
598
|
-
contentType
|
|
599
|
-
});
|
|
600
|
-
if (!outcome.ok) {
|
|
601
|
-
const status = outcome.reason === "not_allowed" ? 403 : outcome.reason === "too_large" ? 413 : 400;
|
|
602
|
-
return reply.code(status).send({ error: outcome.reason });
|
|
603
|
-
}
|
|
604
|
-
return reply.code(201).send({
|
|
605
|
-
id: outcome.file.id,
|
|
606
|
-
name: outcome.file.name,
|
|
607
|
-
size: outcome.file.size,
|
|
608
|
-
contentType: outcome.file.contentType,
|
|
609
|
-
storageKey: outcome.file.storageKey,
|
|
610
|
-
uploadUrl: `/f/${encodeURIComponent(path)}/files/${outcome.file.id}`
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
/** Receive the bytes for a file that was offered. */
|
|
614
|
-
app.put("/f/:path/files/:id", async (request, reply) => {
|
|
615
|
-
if (fileStore === void 0) return reply.code(501).send({ error: "uploads_unavailable" });
|
|
616
|
-
const { path, id } = request.params;
|
|
617
|
-
const form = await storage.getFormByPath(path);
|
|
618
|
-
if (form === void 0) return reply.code(404).send({ error: "unknown_form" });
|
|
619
|
-
const file = await storage.getFile(id);
|
|
620
|
-
if (file === void 0 || file.formId !== form.id) return reply.code(404).send({ error: "unknown_file" });
|
|
621
|
-
if (file.state !== "offered") return reply.code(409).send({ error: "already_uploaded" });
|
|
622
|
-
const bytes = request.body;
|
|
623
|
-
if (!Buffer.isBuffer(bytes)) return reply.code(400).send({ error: "no_body" });
|
|
624
|
-
if (bytes.byteLength > maxFileBytes) return reply.code(413).send({ error: "too_large" });
|
|
625
|
-
if (bytes.byteLength !== file.size) return reply.code(400).send({
|
|
626
|
-
error: "size_mismatch",
|
|
627
|
-
message: `This upload was offered as ${String(file.size)} bytes and ${String(bytes.byteLength)} arrived.`
|
|
628
|
-
});
|
|
629
|
-
await fileStore.put(file.storageKey, bytes);
|
|
630
|
-
await storage.updateFile({
|
|
631
|
-
...file,
|
|
632
|
-
state: "stored"
|
|
633
|
-
});
|
|
634
|
-
return reply.code(204).send();
|
|
635
|
-
});
|
|
636
|
-
/**
|
|
637
|
-
* Serve a file back.
|
|
638
|
-
*
|
|
639
|
-
* Always as an attachment, never inline, and `nosniff` alongside it. Stored
|
|
640
|
-
* cross-site scripting through an uploaded HTML or SVG file is the most
|
|
641
|
-
* commonly exploited vulnerability in this product category, and the real
|
|
642
|
-
* answer is a separate hostname — which a single-container deployment does
|
|
643
|
-
* not have. Forcing a download is the accommodation, and it is the one place
|
|
644
|
-
* this deployment model genuinely costs something.
|
|
645
|
-
*/
|
|
646
|
-
app.get("/f/:path/files/:id", async (request, reply) => {
|
|
647
|
-
if (fileStore === void 0) return reply.code(501).send({ error: "uploads_unavailable" });
|
|
648
|
-
const { path, id } = request.params;
|
|
649
|
-
if (await actorOf(request) === void 0) return reply.code(401).send({ error: "unauthenticated" });
|
|
650
|
-
const form = await storage.getFormByPath(path);
|
|
651
|
-
const file = await storage.getFile(id);
|
|
652
|
-
if (form === void 0 || file === void 0 || file.formId !== form.id) return reply.code(404).send({ error: "unknown_file" });
|
|
653
|
-
const stream = await fileStore.open(file.storageKey);
|
|
654
|
-
if (stream === void 0) return reply.code(404).send({ error: "unknown_file" });
|
|
655
|
-
return reply.header("content-type", "application/octet-stream").header("x-content-type-options", "nosniff").header("content-security-policy", "default-src 'none'; sandbox").header("content-disposition", `attachment; filename="${file.name.replace(/["\r\n]/g, "")}"`).send(stream);
|
|
731
|
+
await app.register(fileRoutes, {
|
|
732
|
+
deps,
|
|
733
|
+
fileStore,
|
|
734
|
+
maxFileBytes,
|
|
735
|
+
limit: submissionLimit,
|
|
736
|
+
actorOf
|
|
656
737
|
});
|
|
657
738
|
app.post("/f/:path/submissions", { config: { rateLimit: {
|
|
658
739
|
max: submissionLimit.max,
|
|
@@ -1459,6 +1540,6 @@ function startFileCollector(storage, store, options = {}) {
|
|
|
1459
1540
|
} };
|
|
1460
1541
|
}
|
|
1461
1542
|
//#endregion
|
|
1462
|
-
export { bootstrapSchema as a, createPostgresStorage as i, createLocalFileStore as n,
|
|
1543
|
+
export { bootstrapSchema as a, createPostgresStorage as i, createLocalFileStore as n, createApp as o, sha256 as r, SCHEMA_HASH_HEADER as s, startFileCollector as t };
|
|
1463
1544
|
|
|
1464
|
-
//# sourceMappingURL=file-collector-
|
|
1545
|
+
//# sourceMappingURL=file-collector-CGSJAtGO.mjs.map
|