next-tinacms-dos 23.0.3 → 23.0.4
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/handlers.js +119 -15
- package/dist/media-key.d.ts +26 -0
- package/package.json +4 -4
package/dist/handlers.js
CHANGED
|
@@ -5,9 +5,87 @@ import {
|
|
|
5
5
|
PutObjectCommand,
|
|
6
6
|
DeleteObjectCommand
|
|
7
7
|
} from "@aws-sdk/client-s3";
|
|
8
|
-
import
|
|
8
|
+
import path2 from "path";
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import multer from "multer";
|
|
11
|
+
|
|
12
|
+
// src/media-key.ts
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
var MediaKeyError = class extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "MediaKeyError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function stripSlashes(value) {
|
|
21
|
+
let start = 0;
|
|
22
|
+
let end = value.length;
|
|
23
|
+
while (start < end && value[start] === "/") start++;
|
|
24
|
+
while (end > start && value[end - 1] === "/") end--;
|
|
25
|
+
return value.slice(start, end);
|
|
26
|
+
}
|
|
27
|
+
function normalizeMediaRoot(mediaRoot) {
|
|
28
|
+
if (!mediaRoot) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
return stripSlashes(mediaRoot);
|
|
32
|
+
}
|
|
33
|
+
function resolveKey(mediaRoot, rawKey, options) {
|
|
34
|
+
if (typeof rawKey !== "string" || rawKey.trim() === "") {
|
|
35
|
+
throw new MediaKeyError("a media key is required");
|
|
36
|
+
}
|
|
37
|
+
let decoded;
|
|
38
|
+
if (options?.decode === false) {
|
|
39
|
+
decoded = rawKey;
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
decoded = decodeURIComponent(rawKey);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new MediaKeyError("media key is not valid");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (decoded.includes("\0") || decoded.includes("\\")) {
|
|
48
|
+
throw new MediaKeyError("media key is not valid");
|
|
49
|
+
}
|
|
50
|
+
if (decoded.startsWith("/") || /^[a-zA-Z]:/.test(decoded)) {
|
|
51
|
+
throw new MediaKeyError("absolute media keys are not allowed");
|
|
52
|
+
}
|
|
53
|
+
const normalized = path.posix.normalize(decoded).replace(/^\.\//, "");
|
|
54
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
55
|
+
throw new MediaKeyError("media key may not traverse directories");
|
|
56
|
+
}
|
|
57
|
+
if (normalized === "" || normalized === ".") {
|
|
58
|
+
throw new MediaKeyError("a media key is required");
|
|
59
|
+
}
|
|
60
|
+
const root = normalizeMediaRoot(mediaRoot);
|
|
61
|
+
if (!root) {
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
const scoped = normalized === root || normalized.startsWith(root + "/") ? normalized : path.posix.join(root, normalized);
|
|
65
|
+
if (scoped !== root && !scoped.startsWith(root + "/")) {
|
|
66
|
+
throw new MediaKeyError("media key escapes mediaRoot");
|
|
67
|
+
}
|
|
68
|
+
return scoped;
|
|
69
|
+
}
|
|
70
|
+
function resolveDirectory(rawDirectory) {
|
|
71
|
+
if (typeof rawDirectory !== "string" || rawDirectory.trim() === "") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
if (rawDirectory.includes("\0") || rawDirectory.includes("\\")) {
|
|
75
|
+
throw new MediaKeyError("media directory is not valid");
|
|
76
|
+
}
|
|
77
|
+
const trimmed = stripSlashes(rawDirectory);
|
|
78
|
+
const normalized = path.posix.normalize(trimmed).replace(/^\.\//, "");
|
|
79
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
80
|
+
throw new MediaKeyError("media directory may not traverse directories");
|
|
81
|
+
}
|
|
82
|
+
if (normalized === "" || normalized === ".") {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
return normalized + "/";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/handlers.ts
|
|
11
89
|
import { promisify } from "util";
|
|
12
90
|
var mediaHandlerConfig = {
|
|
13
91
|
api: {
|
|
@@ -40,7 +118,7 @@ var createMediaHandler = (config, options) => {
|
|
|
40
118
|
case "POST":
|
|
41
119
|
return uploadMedia(req, res, client, bucket, mediaRoot, cdnUrl);
|
|
42
120
|
case "DELETE":
|
|
43
|
-
return deleteAsset(req, res, client, bucket);
|
|
121
|
+
return deleteAsset(req, res, client, bucket, mediaRoot);
|
|
44
122
|
default:
|
|
45
123
|
res.end(404);
|
|
46
124
|
}
|
|
@@ -68,10 +146,19 @@ async function uploadMedia(req, res, client, bucket, mediaRoot, cdnUrl) {
|
|
|
68
146
|
const filePath = req.file.path;
|
|
69
147
|
const fileType = req.file?.mimetype;
|
|
70
148
|
const blob = fs.readFileSync(filePath);
|
|
71
|
-
const filename =
|
|
149
|
+
const filename = path2.basename(filePath);
|
|
150
|
+
let objectKey;
|
|
151
|
+
try {
|
|
152
|
+
objectKey = resolveKey(mediaRoot, prefix + filename, { decode: false });
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e instanceof MediaKeyError) {
|
|
155
|
+
return res.status(400).json({ message: e.message });
|
|
156
|
+
}
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
72
159
|
const params = {
|
|
73
160
|
Bucket: bucket,
|
|
74
|
-
Key:
|
|
161
|
+
Key: objectKey,
|
|
75
162
|
Body: blob,
|
|
76
163
|
ACL: "public-read",
|
|
77
164
|
ContentType: fileType || "application/octet-stream"
|
|
@@ -90,7 +177,7 @@ async function uploadMedia(req, res, client, bucket, mediaRoot, cdnUrl) {
|
|
|
90
177
|
"400x400": src,
|
|
91
178
|
"1000x1000": src
|
|
92
179
|
},
|
|
93
|
-
src: cdnUrl +
|
|
180
|
+
src: cdnUrl + objectKey
|
|
94
181
|
});
|
|
95
182
|
} catch (e) {
|
|
96
183
|
console.error("Error uploading media");
|
|
@@ -121,12 +208,20 @@ async function listMedia(req, res, client, bucket, mediaRoot, cdnUrl) {
|
|
|
121
208
|
limit = 500,
|
|
122
209
|
offset
|
|
123
210
|
} = req.query;
|
|
124
|
-
let prefix
|
|
125
|
-
|
|
211
|
+
let prefix;
|
|
212
|
+
try {
|
|
213
|
+
prefix = resolveDirectory(directory);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
if (e instanceof MediaKeyError) {
|
|
216
|
+
res.status(400).json({ message: e.message });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
126
221
|
const params = {
|
|
127
222
|
Bucket: bucket,
|
|
128
223
|
Delimiter: "/",
|
|
129
|
-
Prefix: mediaRoot ?
|
|
224
|
+
Prefix: mediaRoot ? path2.join(mediaRoot, prefix) : prefix,
|
|
130
225
|
Marker: offset?.toString(),
|
|
131
226
|
MaxKeys: directory && !offset ? +limit + 1 : +limit
|
|
132
227
|
};
|
|
@@ -140,8 +235,8 @@ async function listMedia(req, res, client, bucket, mediaRoot, cdnUrl) {
|
|
|
140
235
|
items.push({
|
|
141
236
|
id: Prefix,
|
|
142
237
|
type: "dir",
|
|
143
|
-
filename:
|
|
144
|
-
directory:
|
|
238
|
+
filename: path2.basename(strippedPrefix),
|
|
239
|
+
directory: path2.dirname(strippedPrefix)
|
|
145
240
|
});
|
|
146
241
|
});
|
|
147
242
|
items.push(
|
|
@@ -168,12 +263,21 @@ var findErrorMessage = (e) => {
|
|
|
168
263
|
if (e.error && e.error.message) return e.error.message;
|
|
169
264
|
return "an error occurred";
|
|
170
265
|
};
|
|
171
|
-
async function deleteAsset(req, res, client, bucket) {
|
|
266
|
+
async function deleteAsset(req, res, client, bucket, mediaRoot) {
|
|
172
267
|
const { media } = req.query;
|
|
173
|
-
let [,
|
|
268
|
+
let [, rawKey] = media;
|
|
174
269
|
const objectKeyIsSplit = media && media.length > 2 && typeof media !== "string";
|
|
175
270
|
if (objectKeyIsSplit) {
|
|
176
|
-
|
|
271
|
+
rawKey = media.slice(1).join("/");
|
|
272
|
+
}
|
|
273
|
+
let objectKey;
|
|
274
|
+
try {
|
|
275
|
+
objectKey = resolveKey(mediaRoot, rawKey, { decode: false });
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (e instanceof MediaKeyError) {
|
|
278
|
+
return res.status(400).json({ message: e.message });
|
|
279
|
+
}
|
|
280
|
+
throw e;
|
|
177
281
|
}
|
|
178
282
|
const params = {
|
|
179
283
|
Bucket: bucket,
|
|
@@ -193,8 +297,8 @@ async function deleteAsset(req, res, client, bucket) {
|
|
|
193
297
|
function getDOSToTinaFunc(cdnUrl, mediaRoot) {
|
|
194
298
|
return function dosToTina(file) {
|
|
195
299
|
const strippedKey = stripMediaRoot(mediaRoot, file.Key);
|
|
196
|
-
const filename =
|
|
197
|
-
const directory =
|
|
300
|
+
const filename = path2.basename(strippedKey);
|
|
301
|
+
const directory = path2.dirname(strippedKey) + "/";
|
|
198
302
|
const src = cdnUrl + file.Key;
|
|
199
303
|
return {
|
|
200
304
|
id: file.Key,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class MediaKeyError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a caller-supplied media key against the configured mediaRoot.
|
|
6
|
+
*
|
|
7
|
+
* @throws {MediaKeyError} when the key is empty, absolute, contains a NUL
|
|
8
|
+
* byte, uses path traversal, or escapes mediaRoot.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveKey(mediaRoot: string, rawKey: unknown, options?: {
|
|
11
|
+
decode?: boolean;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a caller-supplied listing directory into a relative prefix.
|
|
15
|
+
*
|
|
16
|
+
* The directory is optional: an empty / root directory returns `''` (list the
|
|
17
|
+
* mediaRoot itself). Otherwise leading/trailing slashes are stripped (matching
|
|
18
|
+
* the historical handler behaviour) and the result is normalised so it cannot
|
|
19
|
+
* traverse upward — `path.join(mediaRoot, directory)` would otherwise collapse
|
|
20
|
+
* `..` and list objects outside mediaRoot. Returns a prefix with a trailing
|
|
21
|
+
* slash so it can be joined onto mediaRoot directly.
|
|
22
|
+
*
|
|
23
|
+
* @throws {MediaKeyError} when the directory contains a NUL byte or backslash,
|
|
24
|
+
* or uses upward path traversal.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDirectory(rawDirectory: unknown): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-tinacms-dos",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "23.0.
|
|
4
|
+
"version": "23.0.4",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"files": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"tinacms": "3.9.
|
|
22
|
+
"tinacms": "3.9.4"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/crypto-js": "^3.1.47",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"react": "^18.3.1",
|
|
30
30
|
"react-dom": "^18.3.1",
|
|
31
31
|
"typescript": "^5.7.3",
|
|
32
|
-
"
|
|
33
|
-
"tinacms": "
|
|
32
|
+
"tinacms": "3.9.4",
|
|
33
|
+
"@tinacms/scripts": "1.6.2"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"registry": "https://registry.npmjs.org"
|