@cedarjs/uploads 7.0.1-next.312 → 7.0.1-next.333

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.
@@ -37,8 +37,11 @@ export interface FsUppyOptions extends BaseUppyOptions {
37
37
  provider: 'fs';
38
38
  /** Full URL of the api's `POST {prefix}/fs` route, or a function returning it. */
39
39
  endpoint: string | (() => string);
40
- /** The upload token to send. Called per request so a refreshed token is used. */
41
- getUploadToken: () => string | null;
40
+ /**
41
+ * The upload token to send with `file`. Called per request, so each file
42
+ * can carry a different token.
43
+ */
44
+ getUploadToken: (file: Pick<CedarUppyFile, 'id'>) => string | null;
42
45
  }
43
46
  export type CreateUppyOptions = S3UppyOptions | FsUppyOptions;
44
47
  /** Response body of the api's `POST {prefix}/fs` route. */
@@ -49,6 +52,20 @@ export interface FsUploadResponseBody {
49
52
  filename: string;
50
53
  }[];
51
54
  }
55
+ /**
56
+ * Applies a profile's constraints to `uppy` as restrictions. The file-count
57
+ * limit is checked against the files still waiting to upload, not every file
58
+ * in the queue: each upload batch gets a token of its own, so files that
59
+ * already finished uploading do not count against the next batch. Uppy's own
60
+ * `maxNumberOfFiles` counts finished files too, so it is not used.
61
+ *
62
+ * The limit is added on top of the instance's current `onBeforeFileAdded`
63
+ * callback, which still runs first: returning `false` from it rejects the
64
+ * file, and returning a file object adds that file instead. Calling this
65
+ * again with new constraints replaces the limit without wrapping the same
66
+ * callback twice.
67
+ */
68
+ export declare function applyUploadConstraints(uppy: CedarUppy, constraints: UploadConstraints): void;
52
69
  /**
53
70
  * Creates an Uppy instance pre-configured for one of Cedar's upload flows.
54
71
  * Direct-to-S3 uploads use `@uppy/aws-s3` with the presigned URLs the api
@@ -1 +1 @@
1
- {"version":3,"file":"createUppy.d.ts","sourceRoot":"","sources":["../../src/web/createUppy.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,YAAY,CAAA;AAC7B,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAErD,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,IAAI,CAAA;AAE5B,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,kEAAkE;AAClE,eAAO,MAAM,sBAAsB,eAAe,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,WAAW,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACtC,sEAAsE;IACtE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd;;;OAGG;IACH,mBAAmB,EAAE,CACnB,IAAI,EAAE,aAAa,KAChB,OAAO,CAAC,yBAAyB,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd,kFAAkF;IAClF,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACjC,iFAAiF;IACjF,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;CACpC;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,aAAa,CAAA;AAE7D,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5D;AAcD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,CAAC,CA2EpB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAInE"}
1
+ {"version":3,"file":"createUppy.d.ts","sourceRoot":"","sources":["../../src/web/createUppy.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,YAAY,CAAA;AAC7B,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAErD,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,IAAI,CAAA;AAE5B,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,kEAAkE;AAClE,eAAO,MAAM,sBAAsB,eAAe,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,WAAW,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACtC,sEAAsE;IACtE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd;;;OAGG;IACH,mBAAmB,EAAE,CACnB,IAAI,EAAE,aAAa,KAChB,OAAO,CAAC,yBAAyB,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd,kFAAkF;IAClF,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACjC;;;OAGG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;CACnE;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,aAAa,CAAA;AAE7D,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5D;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,WAAW,EAAE,iBAAiB,QAwC/B;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,CAAC,CA2EpB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAInE"}
@@ -2,23 +2,44 @@ import Uppy from "@uppy/core";
2
2
  import { UPLOAD_TOKEN_HEADER } from "../constants.js";
3
3
  import { UPLOAD_TOKEN_HEADER as UPLOAD_TOKEN_HEADER2 } from "../constants.js";
4
4
  const DEFAULT_FS_UPLOAD_PATH = "/upload/fs";
5
- function restrictionsFor(constraints) {
6
- if (!constraints) {
7
- return {};
8
- }
9
- return {
10
- allowedFileTypes: constraints.allowedMimeTypes,
11
- maxFileSize: constraints.maxFileSize,
12
- maxNumberOfFiles: constraints.maxFiles
5
+ const wrappedCallbacks = /* @__PURE__ */ new WeakMap();
6
+ function applyUploadConstraints(uppy, constraints) {
7
+ const current = uppy.opts.onBeforeFileAdded;
8
+ const inner = current ? wrappedCallbacks.get(current) ?? current : void 0;
9
+ const onBeforeFileAdded = (file, files) => {
10
+ const result = inner ? inner(file, files) : !Object.hasOwn(files, file.id);
11
+ if (result === false) {
12
+ return false;
13
+ }
14
+ const pending = Object.values(files).filter(
15
+ (queued) => !queued.isGhost && !queued.progress.uploadComplete
16
+ );
17
+ if (pending.length >= constraints.maxFiles) {
18
+ uppy.info(
19
+ uppy.i18n("youCanOnlyUploadX", { smart_count: constraints.maxFiles }),
20
+ "error"
21
+ );
22
+ return false;
23
+ }
24
+ return result;
13
25
  };
26
+ if (inner) {
27
+ wrappedCallbacks.set(onBeforeFileAdded, inner);
28
+ }
29
+ uppy.setOptions({
30
+ restrictions: {
31
+ allowedFileTypes: constraints.allowedMimeTypes,
32
+ maxFileSize: constraints.maxFileSize
33
+ },
34
+ onBeforeFileAdded
35
+ });
14
36
  }
15
37
  async function createUppy(options) {
16
38
  const { constraints, autoProceed = true, debug = false } = options;
17
- const uppy = new Uppy({
18
- autoProceed,
19
- debug,
20
- restrictions: restrictionsFor(constraints)
21
- });
39
+ const uppy = new Uppy({ autoProceed, debug });
40
+ if (constraints) {
41
+ applyUploadConstraints(uppy, constraints);
42
+ }
22
43
  if (options.provider === "s3") {
23
44
  const { default: AwsS3 } = await import("@uppy/aws-s3");
24
45
  uppy.use(AwsS3, {
@@ -52,9 +73,9 @@ async function createUppy(options) {
52
73
  formData: true,
53
74
  fieldName: "file",
54
75
  bundle: false,
55
- headers: () => {
76
+ headers: (file) => {
56
77
  const headers = {};
57
- const token = options.getUploadToken();
78
+ const token = options.getUploadToken(file);
58
79
  if (token) {
59
80
  headers[UPLOAD_TOKEN_HEADER] = token;
60
81
  }
@@ -77,6 +98,7 @@ function getCedarUploadId(file) {
77
98
  export {
78
99
  DEFAULT_FS_UPLOAD_PATH,
79
100
  UPLOAD_TOKEN_HEADER2 as UPLOAD_TOKEN_HEADER,
101
+ applyUploadConstraints,
80
102
  createUppy,
81
103
  getCedarUploadId
82
104
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useFsUpload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useFsUpload.ts"],"names":[],"mappings":"AAMA,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,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,aAAa,GACd,EAAE,kBAAkB,GAAG,iBAAiB,CAyDxC"}
1
+ {"version":3,"file":"useFsUpload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useFsUpload.ts"],"names":[],"mappings":"AAMA,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,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,aAAa,GACd,EAAE,kBAAkB,GAAG,iBAAiB,CA6ExC"}
@@ -12,7 +12,8 @@ function useFsUpload({
12
12
  onUploadComplete,
13
13
  onUploadError
14
14
  }) {
15
- const { requestToken, getToken, constraints } = useUploadToken({ profile });
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: getToken
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 (!getToken()) {
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 ensureToken = async () => {
43
- if (!getToken()) {
44
- await requestToken();
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.addPreProcessor(ensureToken);
58
+ uppy.on("file-removed", onFileRemoved);
59
+ uppy.addPreProcessor(assignTokens);
49
60
  return () => {
50
61
  uppy.off("file-added", onFileAdded);
51
- uppy.removePreProcessor(ensureToken);
62
+ uppy.off("file-removed", onFileRemoved);
63
+ uppy.removePreProcessor(assignTokens);
52
64
  };
53
- }, [uppy, getToken, requestToken, onUploadError]);
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,CAwHxC"}
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, getToken, constraints } = useUploadToken({ profile });
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 = getToken() ?? await requestToken();
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, getToken, requestToken]
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 (!getToken()) {
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
- }, [uppy, confirm, confirmUpload, getToken, requestToken, onUploadError]);
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 `requestToken()` is called, never on mount.
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;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,GACR,EAAE,qBAAqB,GAAG,oBAAoB,CA4E9C"}
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 = data.token;
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 getToken = useCallback(() => tokenRef.current, []);
56
- return { requestToken, getToken, token, constraints, loading, error };
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,CAoGlB"}
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.setOptions({
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/uploads",
3
- "version": "7.0.1-next.312",
3
+ "version": "7.0.1-next.333",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -43,9 +43,9 @@
43
43
  "test:watch": "vitest watch"
44
44
  },
45
45
  "dependencies": {
46
- "@cedarjs/api": "7.0.1-next.312",
47
- "@cedarjs/context": "7.0.1-next.312",
48
- "@cedarjs/graphql-server": "7.0.1-next.312",
46
+ "@cedarjs/api": "7.0.1-next.333",
47
+ "@cedarjs/context": "7.0.1-next.333",
48
+ "@cedarjs/graphql-server": "7.0.1-next.333",
49
49
  "@fastify/multipart": "9.4.0",
50
50
  "graphql": "16.14.2",
51
51
  "mime-types": "2.1.35"
@@ -53,10 +53,10 @@
53
53
  "devDependencies": {
54
54
  "@apollo/client": "4.3.1",
55
55
  "@arethetypeswrong/cli": "0.18.5",
56
- "@aws-sdk/client-s3": "3.1138.0",
57
- "@aws-sdk/s3-request-presigner": "3.1138.0",
58
- "@cedarjs/framework-tools": "7.0.1-next.312",
59
- "@cedarjs/web": "7.0.1-next.312",
56
+ "@aws-sdk/client-s3": "3.1139.0",
57
+ "@aws-sdk/s3-request-presigner": "3.1139.0",
58
+ "@cedarjs/framework-tools": "7.0.1-next.333",
59
+ "@cedarjs/web": "7.0.1-next.333",
60
60
  "@prisma/adapter-better-sqlite3": "7.10.0",
61
61
  "@prisma/client": "7.10.0",
62
62
  "@testing-library/dom": "10.4.2",
@@ -90,7 +90,7 @@
90
90
  "@apollo/client": "4.3.1",
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.312",
93
+ "@cedarjs/web": "7.0.1-next.333",
94
94
  "@uppy/aws-s3": "^6.0.0",
95
95
  "@uppy/core": "^6.0.0",
96
96
  "@uppy/dashboard": "^6.0.0",