@cedarjs/uploads 7.0.1-next.314 → 7.0.1-next.466
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/README.md +11 -0
- package/dist/authenticator.d.ts +2 -2
- package/dist/authenticator.d.ts.map +1 -1
- package/dist/cleanupStaleUploads.d.ts +4 -3
- package/dist/cleanupStaleUploads.d.ts.map +1 -1
- package/dist/cleanupStaleUploads.js +8 -2
- package/dist/contentTypes.d.ts +74 -0
- package/dist/contentTypes.d.ts.map +1 -0
- package/dist/contentTypes.js +78 -0
- package/dist/deleteFile.d.ts +19 -4
- package/dist/deleteFile.d.ts.map +1 -1
- package/dist/deleteFile.js +49 -8
- package/dist/detectContentType.d.ts +168 -0
- package/dist/detectContentType.d.ts.map +1 -0
- package/dist/detectContentType.js +443 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/fastify/plugin.d.ts +30 -2
- package/dist/fastify/plugin.d.ts.map +1 -1
- package/dist/fastify/plugin.js +38 -10
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -0
- package/dist/keys.d.ts +3 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +2 -1
- package/dist/profiles.d.ts +8 -2
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +5 -4
- package/dist/providers/s3.d.ts.map +1 -1
- package/dist/providers/s3.js +16 -0
- package/dist/services.d.ts +24 -6
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +45 -10
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/variants.d.ts +207 -0
- package/dist/variants.d.ts.map +1 -0
- package/dist/variants.js +498 -0
- package/dist/verifyStoredContentType.d.ts +19 -0
- package/dist/verifyStoredContentType.d.ts.map +1 -0
- package/dist/verifyStoredContentType.js +41 -0
- package/dist/web/createUppy.d.ts +19 -2
- package/dist/web/createUppy.d.ts.map +1 -1
- package/dist/web/createUppy.js +37 -15
- package/dist/web/hooks/useFsUpload.d.ts.map +1 -1
- package/dist/web/hooks/useFsUpload.js +21 -9
- package/dist/web/hooks/useS3Upload.d.ts.map +1 -1
- package/dist/web/hooks/useS3Upload.js +12 -5
- package/dist/web/hooks/useUploadToken.d.ts +27 -1
- package/dist/web/hooks/useUploadToken.d.ts.map +1 -1
- package/dist/web/hooks/useUploadToken.js +72 -3
- package/dist/web/hooks/useUppyUpload.d.ts.map +1 -1
- package/dist/web/hooks/useUppyUpload.js +2 -8
- package/dist/webhooks/s3.d.ts +17 -8
- package/dist/webhooks/s3.d.ts.map +1 -1
- package/dist/webhooks/s3.js +22 -3
- package/package.json +18 -18
|
@@ -12,7 +12,8 @@ function useFsUpload({
|
|
|
12
12
|
onUploadComplete,
|
|
13
13
|
onUploadError
|
|
14
14
|
}) {
|
|
15
|
-
const { requestToken,
|
|
15
|
+
const { requestToken, acquireToken, hasUsableToken, constraints } = useUploadToken({ profile });
|
|
16
|
+
const fileTokens = useRef(/* @__PURE__ */ new Map());
|
|
16
17
|
const endpointRef = useRef(endpoint ?? defaultEndpoint());
|
|
17
18
|
useEffect(() => {
|
|
18
19
|
endpointRef.current = endpoint ?? defaultEndpoint();
|
|
@@ -22,7 +23,7 @@ function useFsUpload({
|
|
|
22
23
|
provider: "fs",
|
|
23
24
|
constraints,
|
|
24
25
|
endpoint: () => endpointRef.current,
|
|
25
|
-
getUploadToken:
|
|
26
|
+
getUploadToken: (file) => fileTokens.current.get(file.id) ?? null
|
|
26
27
|
}),
|
|
27
28
|
constraints,
|
|
28
29
|
{ onUploadComplete, onUploadError }
|
|
@@ -33,24 +34,35 @@ function useFsUpload({
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
const onFileAdded = () => {
|
|
36
|
-
if (!
|
|
37
|
+
if (!hasUsableToken()) {
|
|
37
38
|
requestToken().catch((e) => {
|
|
38
39
|
onUploadError?.(e instanceof Error ? e : new Error(String(e)));
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
42
|
};
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
43
|
+
const assignTokens = async (fileIDs) => {
|
|
44
|
+
if (fileIDs.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const token = await acquireToken(fileIDs.length);
|
|
48
|
+
for (const fileID of fileIDs) {
|
|
49
|
+
fileTokens.current.set(fileID, token);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const onFileRemoved = (file) => {
|
|
53
|
+
if (file) {
|
|
54
|
+
fileTokens.current.delete(file.id);
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
57
|
uppy.on("file-added", onFileAdded);
|
|
48
|
-
uppy.
|
|
58
|
+
uppy.on("file-removed", onFileRemoved);
|
|
59
|
+
uppy.addPreProcessor(assignTokens);
|
|
49
60
|
return () => {
|
|
50
61
|
uppy.off("file-added", onFileAdded);
|
|
51
|
-
uppy.
|
|
62
|
+
uppy.off("file-removed", onFileRemoved);
|
|
63
|
+
uppy.removePreProcessor(assignTokens);
|
|
52
64
|
};
|
|
53
|
-
}, [uppy,
|
|
65
|
+
}, [uppy, acquireToken, hasUsableToken, requestToken, onUploadError]);
|
|
54
66
|
return { ...upload, requestToken };
|
|
55
67
|
}
|
|
56
68
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useS3Upload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useS3Upload.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE/E,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,yEAAyE;IACzE,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,OAAc,EACd,gBAAgB,EAChB,aAAa,GACd,EAAE,kBAAkB,GAAG,iBAAiB,
|
|
1
|
+
{"version":3,"file":"useS3Upload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useS3Upload.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE/E,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,yEAAyE;IACzE,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,OAAc,EACd,gBAAgB,EAChB,aAAa,GACd,EAAE,kBAAkB,GAAG,iBAAiB,CAiIxC"}
|
|
@@ -10,12 +10,12 @@ function useS3Upload({
|
|
|
10
10
|
onUploadComplete,
|
|
11
11
|
onUploadError
|
|
12
12
|
}) {
|
|
13
|
-
const { requestToken,
|
|
13
|
+
const { requestToken, acquireToken, hasUsableToken, constraints } = useUploadToken({ profile });
|
|
14
14
|
const [createPresignedUploadUrl] = useMutation(CREATE_PRESIGNED_UPLOAD_URL);
|
|
15
15
|
const [confirmUpload] = useMutation(CONFIRM_UPLOAD);
|
|
16
16
|
const getUploadParameters = useCallback(
|
|
17
17
|
async (file) => {
|
|
18
|
-
const uploadToken =
|
|
18
|
+
const uploadToken = await acquireToken();
|
|
19
19
|
const result = await createPresignedUploadUrl({
|
|
20
20
|
variables: {
|
|
21
21
|
input: {
|
|
@@ -32,7 +32,7 @@ function useS3Upload({
|
|
|
32
32
|
}
|
|
33
33
|
return data;
|
|
34
34
|
},
|
|
35
|
-
[createPresignedUploadUrl,
|
|
35
|
+
[createPresignedUploadUrl, acquireToken]
|
|
36
36
|
);
|
|
37
37
|
const getUploadParametersRef = useRef(getUploadParameters);
|
|
38
38
|
useEffect(() => {
|
|
@@ -53,7 +53,7 @@ function useS3Upload({
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
const onFileAdded = () => {
|
|
56
|
-
if (!
|
|
56
|
+
if (!hasUsableToken()) {
|
|
57
57
|
requestToken().catch((e) => {
|
|
58
58
|
onUploadError?.(e instanceof Error ? e : new Error(String(e)));
|
|
59
59
|
});
|
|
@@ -91,7 +91,14 @@ function useS3Upload({
|
|
|
91
91
|
uppy.off("file-added", onFileAdded);
|
|
92
92
|
uppy.removePostProcessor(confirmAll);
|
|
93
93
|
};
|
|
94
|
-
}, [
|
|
94
|
+
}, [
|
|
95
|
+
uppy,
|
|
96
|
+
confirm,
|
|
97
|
+
confirmUpload,
|
|
98
|
+
hasUsableToken,
|
|
99
|
+
requestToken,
|
|
100
|
+
onUploadError
|
|
101
|
+
]);
|
|
95
102
|
return { ...upload, requestToken };
|
|
96
103
|
}
|
|
97
104
|
export {
|
|
@@ -10,6 +10,19 @@ export interface UseUploadTokenResult {
|
|
|
10
10
|
* in-flight request.
|
|
11
11
|
*/
|
|
12
12
|
requestToken: () => Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves with a token that can still create `fileCount` files (default
|
|
15
|
+
* 1) and counts them against it. Reuses the current token while it has
|
|
16
|
+
* files left and is not about to expire, and fetches a fresh one
|
|
17
|
+
* otherwise. Call it once per file (or once per batch, with the batch
|
|
18
|
+
* size) right before the token is sent.
|
|
19
|
+
*/
|
|
20
|
+
acquireToken: (fileCount?: number) => Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the current token is unexpired and has at least one file left,
|
|
23
|
+
* so the next `acquireToken()` call can resolve without a request.
|
|
24
|
+
*/
|
|
25
|
+
hasUsableToken: () => boolean;
|
|
13
26
|
/**
|
|
14
27
|
* Returns the most recently fetched token, or `null`, without triggering
|
|
15
28
|
* a render. Meant for event handlers and Uppy callbacks.
|
|
@@ -25,10 +38,23 @@ export interface UseUploadTokenResult {
|
|
|
25
38
|
loading: boolean;
|
|
26
39
|
error: ErrorLike | undefined;
|
|
27
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads the lifetime (`exp - iat`, in milliseconds) from a signed token's
|
|
43
|
+
* claims without verifying it. The lifetime is anchored to the client's
|
|
44
|
+
* clock when the token is requested, so clock skew between browser and server
|
|
45
|
+
* does not matter. Returns `null` for a token whose claims cannot be read.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readTokenLifetimeMs(token: string): number | null;
|
|
28
48
|
/**
|
|
29
49
|
* Fetches upload tokens for a profile from the `requestUploadToken` query.
|
|
30
50
|
* Tokens expire, so the query runs with `fetchPolicy: 'no-cache'` and only
|
|
31
|
-
* when
|
|
51
|
+
* when a token is needed, never on mount.
|
|
52
|
+
*
|
|
53
|
+
* A token expires and can only create `maxFiles` files, so the hook tracks
|
|
54
|
+
* both for the current token. `acquireToken()` hands the token out only
|
|
55
|
+
* while it has files left and is not about to expire, and fetches a fresh
|
|
56
|
+
* one otherwise, which lets one mounted component upload any number of
|
|
57
|
+
* times.
|
|
32
58
|
*/
|
|
33
59
|
export declare function useUploadToken({ profile, }: UseUploadTokenOptions): UseUploadTokenResult;
|
|
34
60
|
//# sourceMappingURL=useUploadToken.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUploadToken.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useUploadToken.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAI/C,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,eAAe,CAAA;AAEtB,MAAM,WAAW,qBAAqB;IACpC,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IACnC;;;OAGG;IACH,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAC7B,kDAAkD;IAClD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,SAAS,GAAG,SAAS,CAAA;CAC7B;
|
|
1
|
+
{"version":3,"file":"useUploadToken.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useUploadToken.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAI/C,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,eAAe,CAAA;AAEtB,MAAM,WAAW,qBAAqB;IACpC,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IACnC;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACrD;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,CAAA;IAC7B;;;OAGG;IACH,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAC7B,kDAAkD;IAClD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,SAAS,GAAG,SAAS,CAAA;CAC7B;AA+BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0BhE;AA2BD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,GACR,EAAE,qBAAqB,GAAG,oBAAoB,CAoH9C"}
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useLazyQuery } from "@apollo/client/react";
|
|
3
3
|
import { REQUEST_UPLOAD_TOKEN } from "../graphql.js";
|
|
4
|
+
const EXPIRY_MARGIN_MS = 3e4;
|
|
5
|
+
function decodeBase64Url(value) {
|
|
6
|
+
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
7
|
+
const padded = base64.padEnd(
|
|
8
|
+
base64.length + (4 - base64.length % 4) % 4,
|
|
9
|
+
"="
|
|
10
|
+
);
|
|
11
|
+
return atob(padded);
|
|
12
|
+
}
|
|
13
|
+
function readTokenLifetimeMs(token) {
|
|
14
|
+
const [, claims] = token.split(".");
|
|
15
|
+
if (!claims) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(decodeBase64Url(claims));
|
|
20
|
+
if (typeof parsed === "object" && parsed !== null && "exp" in parsed && "iat" in parsed && typeof parsed.exp === "number" && typeof parsed.iat === "number" && parsed.exp > parsed.iat) {
|
|
21
|
+
return (parsed.exp - parsed.iat) * 1e3;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function expiresAtFor(token, requestedAt) {
|
|
28
|
+
const lifetime = readTokenLifetimeMs(token);
|
|
29
|
+
if (lifetime === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return requestedAt + lifetime - Math.min(EXPIRY_MARGIN_MS, lifetime / 2);
|
|
33
|
+
}
|
|
34
|
+
function canCreate(state, fileCount) {
|
|
35
|
+
if (!state) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (state.expiresAt !== null && Date.now() >= state.expiresAt) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return state.used === 0 || state.used + fileCount <= state.maxFiles;
|
|
42
|
+
}
|
|
4
43
|
function useUploadToken({
|
|
5
44
|
profile
|
|
6
45
|
}) {
|
|
@@ -23,6 +62,7 @@ function useUploadToken({
|
|
|
23
62
|
}
|
|
24
63
|
const requestedProfile = profile;
|
|
25
64
|
const request = (async () => {
|
|
65
|
+
const requestedAt = Date.now();
|
|
26
66
|
const result = await execute({ variables: { profile: requestedProfile } });
|
|
27
67
|
if (requestedProfile !== profileRef.current) {
|
|
28
68
|
throw new Error(
|
|
@@ -36,7 +76,12 @@ function useUploadToken({
|
|
|
36
76
|
if (!data) {
|
|
37
77
|
throw new Error("The upload token query returned no data.");
|
|
38
78
|
}
|
|
39
|
-
tokenRef.current =
|
|
79
|
+
tokenRef.current = {
|
|
80
|
+
token: data.token,
|
|
81
|
+
maxFiles: data.maxFiles,
|
|
82
|
+
used: 0,
|
|
83
|
+
expiresAt: expiresAtFor(data.token, requestedAt)
|
|
84
|
+
};
|
|
40
85
|
setToken(data.token);
|
|
41
86
|
setConstraints({
|
|
42
87
|
allowedMimeTypes: data.allowedMimeTypes,
|
|
@@ -52,9 +97,33 @@ function useUploadToken({
|
|
|
52
97
|
inFlight.current = request;
|
|
53
98
|
return request;
|
|
54
99
|
}, [execute, profile]);
|
|
55
|
-
const
|
|
56
|
-
|
|
100
|
+
const acquireToken = useCallback(
|
|
101
|
+
async (fileCount = 1) => {
|
|
102
|
+
for (; ; ) {
|
|
103
|
+
const current = tokenRef.current;
|
|
104
|
+
if (current && canCreate(current, fileCount)) {
|
|
105
|
+
current.used += fileCount;
|
|
106
|
+
return current.token;
|
|
107
|
+
}
|
|
108
|
+
await requestToken();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[requestToken]
|
|
112
|
+
);
|
|
113
|
+
const hasUsableToken = useCallback(() => canCreate(tokenRef.current, 1), []);
|
|
114
|
+
const getToken = useCallback(() => tokenRef.current?.token ?? null, []);
|
|
115
|
+
return {
|
|
116
|
+
requestToken,
|
|
117
|
+
acquireToken,
|
|
118
|
+
hasUsableToken,
|
|
119
|
+
getToken,
|
|
120
|
+
token,
|
|
121
|
+
constraints,
|
|
122
|
+
loading,
|
|
123
|
+
error
|
|
124
|
+
};
|
|
57
125
|
}
|
|
58
126
|
export {
|
|
127
|
+
readTokenLifetimeMs,
|
|
59
128
|
useUploadToken
|
|
60
129
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUppyUpload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useUppyUpload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAA;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,EAChC,WAAW,EAAE,iBAAiB,GAAG,IAAI,EACrC,EAAE,gBAAgB,EAAE,aAAa,EAAE,EAAE,mBAAmB,GACvD,gBAAgB,
|
|
1
|
+
{"version":3,"file":"useUppyUpload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useUppyUpload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAA;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,EAChC,WAAW,EAAE,iBAAiB,GAAG,IAAI,EACrC,EAAE,gBAAgB,EAAE,aAAa,EAAE,EAAE,mBAAmB,GACvD,gBAAgB,CA8FlB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { getCedarUploadId } from "../createUppy.js";
|
|
2
|
+
import { applyUploadConstraints, getCedarUploadId } from "../createUppy.js";
|
|
3
3
|
function useUppyUpload(create, constraints, { onUploadComplete, onUploadError }) {
|
|
4
4
|
const [uppy, setUppy] = useState(null);
|
|
5
5
|
const [completedUploads, setCompletedUploads] = useState([]);
|
|
@@ -33,13 +33,7 @@ function useUppyUpload(create, constraints, { onUploadComplete, onUploadError })
|
|
|
33
33
|
if (!uppy || !constraints) {
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
uppy
|
|
37
|
-
restrictions: {
|
|
38
|
-
allowedFileTypes: constraints.allowedMimeTypes,
|
|
39
|
-
maxFileSize: constraints.maxFileSize,
|
|
40
|
-
maxNumberOfFiles: constraints.maxFiles
|
|
41
|
-
}
|
|
42
|
-
});
|
|
36
|
+
applyUploadConstraints(uppy, constraints);
|
|
43
37
|
}, [uppy, constraints]);
|
|
44
38
|
useEffect(() => {
|
|
45
39
|
if (!uppy) {
|
package/dist/webhooks/s3.d.ts
CHANGED
|
@@ -17,21 +17,30 @@ export interface S3Event {
|
|
|
17
17
|
Records: S3EventRecord[];
|
|
18
18
|
}
|
|
19
19
|
export type S3EventOutcome = 'completed' | 'failed' | 'ignored' | 'not-found' | 'no-target';
|
|
20
|
+
export interface ProcessS3EventRecordOptions {
|
|
21
|
+
db: UploadDatabase;
|
|
22
|
+
targets: StorageTargets;
|
|
23
|
+
/**
|
|
24
|
+
* Checks the object's bytes against the type declared when the upload URL
|
|
25
|
+
* was issued, as `confirmUpload` does. Defaults to `true`.
|
|
26
|
+
*/
|
|
27
|
+
verifyContentType?: boolean;
|
|
28
|
+
}
|
|
20
29
|
/**
|
|
21
30
|
* Settles the `pending` row for one `s3:ObjectCreated:*` record. The row is
|
|
22
31
|
* matched on target plus storage key, the object's size is compared with
|
|
23
|
-
* the size authorized at presign time
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
32
|
+
* the size authorized at presign time and, with `verifyContentType` (the
|
|
33
|
+
* default), its bytes with the type declared then, and the row is flipped
|
|
34
|
+
* `pending` to `completed` (or claimed `failed` and the object deleted on a
|
|
35
|
+
* mismatch). Both transitions are conditional, so a duplicate notification
|
|
36
|
+
* or a row the cleanup job already claimed is left alone.
|
|
27
37
|
*/
|
|
28
|
-
export declare function processS3EventRecord(record: S3EventRecord, { db, targets }:
|
|
29
|
-
db: UploadDatabase;
|
|
30
|
-
targets: StorageTargets;
|
|
31
|
-
}): Promise<S3EventOutcome>;
|
|
38
|
+
export declare function processS3EventRecord(record: S3EventRecord, { db, targets, verifyContentType }: ProcessS3EventRecordOptions): Promise<S3EventOutcome>;
|
|
32
39
|
export interface S3WebhookOptions {
|
|
33
40
|
db: UploadDatabase;
|
|
34
41
|
targets: StorageTargets;
|
|
42
|
+
/** See `ProcessS3EventRecordOptions`. Defaults to `true`. */
|
|
43
|
+
verifyContentType?: boolean;
|
|
35
44
|
/** The trusted SNS topic. Messages from any other topic are rejected. */
|
|
36
45
|
topicArn: string;
|
|
37
46
|
fetchCertificate?: FetchCertificate;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/webhooks/s3.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/webhooks/s3.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AASjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAEhD,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,EAAE,EAAE;QACF,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;QACxB,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KACvC,CAAA;CACF;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB;AAsBD,MAAM,MAAM,cAAc,GACxB,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAA;AAkChE,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,EAAE,EAAE,EAAE,OAAO,EAAE,iBAAwB,EAAE,EAAE,2BAA2B,GACrE,OAAO,CAAC,cAAc,CAAC,CA2EzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,6DAA6D;IAC7D,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,cAAc,GAAG,0BAA0B,GAAG,yBAAyB,CAAA;IAC7E,QAAQ,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CA+D1B"}
|
package/dist/webhooks/s3.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UploadError } from "../errors.js";
|
|
2
|
+
import { verifyStoredContentType } from "../verifyStoredContentType.js";
|
|
2
3
|
import {
|
|
3
4
|
assertTrustedSnsUrl,
|
|
4
5
|
isSnsMessage,
|
|
@@ -28,7 +29,7 @@ function matchTarget(targets, bucket, key) {
|
|
|
28
29
|
}
|
|
29
30
|
return best ? { name: best.name, storageKey: key.slice(best.keyPrefix.length) } : null;
|
|
30
31
|
}
|
|
31
|
-
async function processS3EventRecord(record, { db, targets }) {
|
|
32
|
+
async function processS3EventRecord(record, { db, targets, verifyContentType = true }) {
|
|
32
33
|
if (!record.eventName.startsWith("ObjectCreated")) {
|
|
33
34
|
return "ignored";
|
|
34
35
|
}
|
|
@@ -51,7 +52,7 @@ async function processS3EventRecord(record, { db, targets }) {
|
|
|
51
52
|
const target = targets[match.name];
|
|
52
53
|
const reportedSize = record.s3.object.size;
|
|
53
54
|
const actualSize = typeof reportedSize === "number" ? reportedSize : await target.getObjectSize(match.storageKey);
|
|
54
|
-
|
|
55
|
+
const fail = async () => {
|
|
55
56
|
const { count: count2 } = await db.upload.updateMany({
|
|
56
57
|
where: { id: upload.id, status: "pending" },
|
|
57
58
|
data: { status: "failed" }
|
|
@@ -60,10 +61,28 @@ async function processS3EventRecord(record, { db, targets }) {
|
|
|
60
61
|
await target.delete(match.storageKey);
|
|
61
62
|
}
|
|
62
63
|
return "failed";
|
|
64
|
+
};
|
|
65
|
+
if (actualSize === null || BigInt(actualSize) !== upload.size) {
|
|
66
|
+
return fail();
|
|
67
|
+
}
|
|
68
|
+
let mimeType = upload.mimeType;
|
|
69
|
+
if (verifyContentType) {
|
|
70
|
+
const verified = await verifyStoredContentType(target, match.storageKey, {
|
|
71
|
+
size: actualSize,
|
|
72
|
+
declaredType: upload.mimeType
|
|
73
|
+
});
|
|
74
|
+
if (!verified) {
|
|
75
|
+
return fail();
|
|
76
|
+
}
|
|
77
|
+
mimeType = verified;
|
|
63
78
|
}
|
|
64
79
|
const { count } = await db.upload.updateMany({
|
|
65
80
|
where: { id: upload.id, status: "pending" },
|
|
66
|
-
data: {
|
|
81
|
+
data: {
|
|
82
|
+
status: "completed",
|
|
83
|
+
size: BigInt(actualSize),
|
|
84
|
+
...mimeType !== upload.mimeType ? { mimeType } : {}
|
|
85
|
+
}
|
|
67
86
|
});
|
|
68
87
|
return count === 1 ? "completed" : "ignored";
|
|
69
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/uploads",
|
|
3
|
-
"version": "7.0.1-next.
|
|
3
|
+
"version": "7.0.1-next.466",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/cedarjs/cedar.git",
|
|
@@ -43,43 +43,43 @@
|
|
|
43
43
|
"test:watch": "vitest watch"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@cedarjs/api": "7.0.1-next.
|
|
47
|
-
"@cedarjs/context": "7.0.1-next.
|
|
48
|
-
"@cedarjs/graphql-server": "7.0.1-next.
|
|
46
|
+
"@cedarjs/api": "7.0.1-next.466",
|
|
47
|
+
"@cedarjs/context": "7.0.1-next.466",
|
|
48
|
+
"@cedarjs/graphql-server": "7.0.1-next.466",
|
|
49
49
|
"@fastify/multipart": "9.4.0",
|
|
50
50
|
"graphql": "16.14.2",
|
|
51
51
|
"mime-types": "2.1.35"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@apollo/client": "4.3.
|
|
54
|
+
"@apollo/client": "4.3.3",
|
|
55
55
|
"@arethetypeswrong/cli": "0.18.5",
|
|
56
|
-
"@aws-sdk/client-s3": "3.
|
|
57
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
58
|
-
"@cedarjs/framework-tools": "7.0.1-next.
|
|
59
|
-
"@cedarjs/web": "7.0.1-next.
|
|
56
|
+
"@aws-sdk/client-s3": "3.1149.0",
|
|
57
|
+
"@aws-sdk/s3-request-presigner": "3.1149.0",
|
|
58
|
+
"@cedarjs/framework-tools": "7.0.1-next.466",
|
|
59
|
+
"@cedarjs/web": "7.0.1-next.466",
|
|
60
60
|
"@prisma/adapter-better-sqlite3": "7.10.0",
|
|
61
61
|
"@prisma/client": "7.10.0",
|
|
62
62
|
"@testing-library/dom": "10.4.2",
|
|
63
63
|
"@testing-library/jest-dom": "6.9.1",
|
|
64
64
|
"@testing-library/react": "16.3.3",
|
|
65
|
-
"@types/aws-lambda": "8.10.
|
|
65
|
+
"@types/aws-lambda": "8.10.164",
|
|
66
66
|
"@types/better-sqlite3": "7.6.13",
|
|
67
67
|
"@types/mime-types": "2.1.4",
|
|
68
68
|
"@types/react": "^19.2.0",
|
|
69
69
|
"@types/react-dom": "^19.2.0",
|
|
70
|
-
"@uppy/aws-s3": "6.
|
|
71
|
-
"@uppy/core": "6.0
|
|
72
|
-
"@uppy/dashboard": "6.0.
|
|
70
|
+
"@uppy/aws-s3": "6.2.2",
|
|
71
|
+
"@uppy/core": "6.2.0",
|
|
72
|
+
"@uppy/dashboard": "6.0.1",
|
|
73
73
|
"@uppy/drag-drop": "6.0.0",
|
|
74
74
|
"@uppy/react": "6.0.0",
|
|
75
|
-
"@uppy/xhr-upload": "6.0.
|
|
75
|
+
"@uppy/xhr-upload": "6.0.1",
|
|
76
76
|
"better-sqlite3": "12.11.1",
|
|
77
|
-
"concurrently": "9.2.
|
|
77
|
+
"concurrently": "9.2.5",
|
|
78
78
|
"esbuild": "0.28.2",
|
|
79
79
|
"fastify": "5.12.5",
|
|
80
80
|
"jsdom": "27.4.0",
|
|
81
81
|
"prisma": "7.10.0",
|
|
82
|
-
"publint": "0.3.
|
|
82
|
+
"publint": "0.3.25",
|
|
83
83
|
"react": "19.3.0",
|
|
84
84
|
"react-dom": "19.3.0",
|
|
85
85
|
"typescript": "5.9.3",
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
"zx": "8.8.5"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@apollo/client": "4.3.
|
|
90
|
+
"@apollo/client": "4.3.3",
|
|
91
91
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
92
92
|
"@aws-sdk/s3-request-presigner": "^3.0.0",
|
|
93
|
-
"@cedarjs/web": "7.0.1-next.
|
|
93
|
+
"@cedarjs/web": "7.0.1-next.466",
|
|
94
94
|
"@uppy/aws-s3": "^6.0.0",
|
|
95
95
|
"@uppy/core": "^6.0.0",
|
|
96
96
|
"@uppy/dashboard": "^6.0.0",
|