@cosmicdrift/kumiko-bundled-features 0.197.0 → 0.197.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.197.
|
|
3
|
+
"version": "0.197.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -125,12 +125,12 @@
|
|
|
125
125
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
126
126
|
},
|
|
127
127
|
"dependencies": {
|
|
128
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.197.
|
|
129
|
-
"@cosmicdrift/kumiko-framework": "0.197.
|
|
130
|
-
"@cosmicdrift/kumiko-headless": "0.197.
|
|
131
|
-
"@cosmicdrift/kumiko-renderer": "0.197.
|
|
132
|
-
"@cosmicdrift/kumiko-renderer-web": "0.197.
|
|
133
|
-
"@cosmicdrift/kumiko-types": "0.197.
|
|
128
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.197.1",
|
|
129
|
+
"@cosmicdrift/kumiko-framework": "0.197.1",
|
|
130
|
+
"@cosmicdrift/kumiko-headless": "0.197.1",
|
|
131
|
+
"@cosmicdrift/kumiko-renderer": "0.197.1",
|
|
132
|
+
"@cosmicdrift/kumiko-renderer-web": "0.197.1",
|
|
133
|
+
"@cosmicdrift/kumiko-types": "0.197.1",
|
|
134
134
|
"@mollie/api-client": "^4.5.0",
|
|
135
135
|
"@node-rs/argon2": "^2.0.2",
|
|
136
136
|
"@types/mailparser": "^3.4.6",
|
|
@@ -68,6 +68,9 @@ const widgetEntity = createEntity({
|
|
|
68
68
|
full: { maxEdge: 4096, format: "webp" },
|
|
69
69
|
plan: { maxEdge: 4096, format: "webp" },
|
|
70
70
|
heroWide: { maxEdge: 3200, format: "webp" },
|
|
71
|
+
// No format — resize-without-reformat, the legitimate reason
|
|
72
|
+
// spec.format can be omitted (see #2021).
|
|
73
|
+
raw: { maxEdge: 160 },
|
|
71
74
|
},
|
|
72
75
|
}),
|
|
73
76
|
plain: createImageField(),
|
|
@@ -133,10 +136,15 @@ async function uploadFile(
|
|
|
133
136
|
asUser: typeof userA,
|
|
134
137
|
attach: { entityType: string; entityId: string },
|
|
135
138
|
fieldName = "img",
|
|
139
|
+
mimeType = "image/jpeg",
|
|
136
140
|
): Promise<string> {
|
|
137
141
|
const token = await stack.jwt.sign(asUser);
|
|
138
142
|
const fd = new FormData();
|
|
139
|
-
|
|
143
|
+
// Bun's server-side multipart parser derives File.type from the filename
|
|
144
|
+
// extension, not the declared part Content-Type — so the extension must
|
|
145
|
+
// match `mimeType` for the upload to actually carry it through.
|
|
146
|
+
const extension = mimeType.split("/")[1]?.split("+")[0] ?? "bin";
|
|
147
|
+
fd.append("file", new File([Buffer.from([1, 2, 3])], `img.${extension}`, { type: mimeType }));
|
|
140
148
|
fd.append("entityType", attach.entityType);
|
|
141
149
|
fd.append("entityId", attach.entityId);
|
|
142
150
|
fd.append("fieldName", fieldName);
|
|
@@ -330,6 +338,25 @@ describe("GET /media/:fileRefId/:variant (anonymous, default-deny)", () => {
|
|
|
330
338
|
expect(res.status).toBe(404);
|
|
331
339
|
});
|
|
332
340
|
|
|
341
|
+
// #2021 — the "raw" variant has no `format`, so its mimeType comes from
|
|
342
|
+
// outputMimeType's fallback branch. `img` has no `accept`, so the source
|
|
343
|
+
// mimeType is exactly the client-controlled `file.type` from the upload —
|
|
344
|
+
// this proves the anonymous route no longer echoes it verbatim.
|
|
345
|
+
test("a variant with no spec.format never echoes a non-safe, client-controlled source mimeType", async () => {
|
|
346
|
+
const fileId = await uploadFile(
|
|
347
|
+
userA,
|
|
348
|
+
{ entityType: "widget", entityId: PUBLIC_WIDGET_ID },
|
|
349
|
+
"img",
|
|
350
|
+
"image/svg+xml",
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const res = await stack.app.request(`http://${HOST_A}/media/${fileId}/raw`);
|
|
354
|
+
|
|
355
|
+
expect(res.status).toBe(200);
|
|
356
|
+
expect(res.headers.get("Content-Type")).not.toBe("image/svg+xml");
|
|
357
|
+
expect(res.headers.get("Content-Type")).toBe("application/octet-stream");
|
|
358
|
+
});
|
|
359
|
+
|
|
333
360
|
test("more than `limit` requests from the same IP within the window → 429 (Step-1 regression)", async () => {
|
|
334
361
|
const fileId = await uploadFile(userA, { entityType: "widget", entityId: PUBLIC_WIDGET_ID });
|
|
335
362
|
const xff = "203.0.113.9";
|