@cedarjs/uploads 8.0.0-canary.3402 → 8.0.0-canary.3403

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.
@@ -15,7 +15,11 @@ export interface UploadProfile {
15
15
  target: string;
16
16
  /**
17
17
  * MIME types the profile accepts. Exact types (`image/png`) and wildcard
18
- * subtypes (`image/*`) are supported.
18
+ * subtypes (`image/*`) are supported, and either spelling of an aliased
19
+ * type (`image/jpeg` or `image/jpg`) matches both. When files are checked
20
+ * by their bytes (`verifyContentType`, on by default), types that can carry
21
+ * script, such as `image/svg+xml` or `text/html`, are only accepted when
22
+ * listed exactly; see `SCRIPTABLE_CONTENT_TYPES`.
19
23
  */
20
24
  allowedMimeTypes: string[];
21
25
  /** Largest file the profile accepts, in bytes. */
@@ -43,7 +47,9 @@ export declare function resolveProfile(profiles: UploadProfiles, name: string):
43
47
  /**
44
48
  * Whether `mimeType` matches one of `allowed`. Matching is case-insensitive
45
49
  * and ignores parameters such as `; charset=utf-8`. An entry of `image/*`
46
- * matches every `image/` subtype.
50
+ * matches every `image/` subtype. Aliases in `CONTENT_TYPE_ALIASES` are the
51
+ * same type, so `image/jpg` matches an entry of `image/jpeg` and the other
52
+ * way round.
47
53
  */
48
54
  export declare function isMimeTypeAllowed(allowed: string[], mimeType: string): boolean;
49
55
  //# sourceMappingURL=profiles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAIxD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAc,CAAA;AAE3C,2CAA2C;AAC3C,eAAO,MAAM,+BAA+B,EAAE,oBAA2B,CAAA;AAEzE;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,oBAAoB,CAAA;CACjC;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE1D,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,IAAI,EAAE,MAAM,CAAA;CACb;AAiCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAM7E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,GACX,qBAAqB,CAavB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,WAoBpE"}
1
+ {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAKxD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAc,CAAA;AAE3C,2CAA2C;AAC3C,eAAO,MAAM,+BAA+B,EAAE,oBAA2B,CAAA;AAEzE;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,oBAAoB,CAAA;CACjC;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE1D,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,IAAI,EAAE,MAAM,CAAA;CACb;AAiCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAM7E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,GACX,qBAAqB,CAavB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,WAoBpE"}
package/dist/profiles.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { canonicalContentType } from "./contentTypes.js";
1
2
  import { UploadError } from "./errors.js";
2
3
  const DB_MAX_FILE_SIZE = 1024 * 1024;
3
4
  const DEFAULT_UPLOAD_TOKEN_EXPIRES_IN = "5m";
@@ -44,8 +45,8 @@ function resolveProfile(profiles, name) {
44
45
  return { ...profile, name };
45
46
  }
46
47
  function isMimeTypeAllowed(allowed, mimeType) {
47
- const normalized = mimeType.split(";")[0].trim().toLowerCase();
48
- if (!normalized) {
48
+ const type = canonicalContentType(mimeType);
49
+ if (!type) {
49
50
  return false;
50
51
  }
51
52
  return allowed.some((entry) => {
@@ -54,9 +55,9 @@ function isMimeTypeAllowed(allowed, mimeType) {
54
55
  return true;
55
56
  }
56
57
  if (pattern.endsWith("/*")) {
57
- return normalized.startsWith(pattern.slice(0, -1));
58
+ return type.startsWith(pattern.slice(0, -1));
58
59
  }
59
- return pattern === normalized;
60
+ return canonicalContentType(pattern) === type;
60
61
  });
61
62
  }
62
63
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAmBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,eAAe,CAwHzE"}
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAmBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,eAAe,CA4IzE"}
@@ -45,6 +45,22 @@ function createS3Provider(opts) {
45
45
  }
46
46
  return Buffer.from(await res.Body.transformToByteArray());
47
47
  },
48
+ async readRange(key, { offset, length }) {
49
+ if (length <= 0) {
50
+ return Buffer.alloc(0);
51
+ }
52
+ const res = await client.send(
53
+ new GetObjectCommand({
54
+ Bucket: bucket,
55
+ Key: fullKey(key),
56
+ Range: `bytes=${offset}-${offset + length - 1}`
57
+ })
58
+ );
59
+ if (!res.Body) {
60
+ return Buffer.alloc(0);
61
+ }
62
+ return Buffer.from(await res.Body.transformToByteArray());
63
+ },
48
64
  async readStream(key) {
49
65
  const res = await client.send(
50
66
  new GetObjectCommand({ Bucket: bucket, Key: fullKey(key) })
@@ -40,6 +40,16 @@ export interface CreatePresignedUploadOptions {
40
40
  /** The validated token, as attached to context by `@requireUploadToken`. */
41
41
  tokenPayload: UploadTokenPayload;
42
42
  input: CreatePresignedUploadInput;
43
+ /**
44
+ * Holds the declared type to the rules `acceptedContentType()` applies to
45
+ * a type it can't check by bytes: no wildcard types, and types that can
46
+ * carry script (`SCRIPTABLE_CONTENT_TYPES`) only when the profile lists
47
+ * them exactly. Pair it with `verifyContentType` on `confirmUpload` (and
48
+ * the S3 webhook), which checks the bytes against the declared type.
49
+ * Defaults to `true`; `false` checks the declared type with
50
+ * `isMimeTypeAllowed()` only.
51
+ */
52
+ verifyContentType?: boolean;
43
53
  }
44
54
  export interface PresignedUploadResponse {
45
55
  uploadId: string;
@@ -52,19 +62,27 @@ export interface PresignedUploadResponse {
52
62
  * a presigned URL the client PUTs the bytes to. This is what the generated
53
63
  * `createPresignedUploadUrl` resolver calls.
54
64
  */
55
- export declare function createPresignedUpload({ db, targets, tokenPayload, input, }: CreatePresignedUploadOptions): Promise<PresignedUploadResponse>;
65
+ export declare function createPresignedUpload({ db, targets, tokenPayload, input, verifyContentType, }: CreatePresignedUploadOptions): Promise<PresignedUploadResponse>;
56
66
  export interface ConfirmUploadOptions {
57
67
  db: UploadDatabase;
58
68
  targets: StorageTargets;
59
69
  uploadId: string;
60
70
  currentUser: UploadActor | null | undefined;
71
+ /**
72
+ * Checks the object's bytes against the type the client declared for it
73
+ * when it asked for the upload URL, with a ranged read of its head (and of
74
+ * its tail for ZIPs), and records the canonical spelling of that type. A
75
+ * mismatch fails the row and deletes the object. Defaults to `true`.
76
+ */
77
+ verifyContentType?: boolean;
61
78
  }
62
79
  /**
63
80
  * Client-side confirmation of a direct upload. Verifies that the object
64
- * exists in storage and that its size matches what the token authorized,
65
- * then flips the row `pending` to `completed`. On a size mismatch the row is
66
- * claimed `failed` and the object deleted. Only the user (and organization)
67
- * the upload was issued to can confirm it.
81
+ * exists in storage, that its size matches what the token authorized, and
82
+ * (with `verifyContentType`, the default) that its bytes are the type the
83
+ * client declared, then flips the row `pending` to `completed`. On a
84
+ * mismatch the row is claimed `failed` and the object deleted. Only the user
85
+ * (and organization) the upload was issued to can confirm it.
68
86
  */
69
- export declare function confirmUpload({ db, targets, uploadId, currentUser, }: ConfirmUploadOptions): Promise<UploadRecord>;
87
+ export declare function confirmUpload({ db, targets, uploadId, currentUser, verifyContentType, }: ConfirmUploadOptions): Promise<UploadRecord>;
70
88
  //# sourceMappingURL=services.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,KAAK,EAEV,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAA;AAEnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,cAAc,CAAA;IACxB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;IAC3C,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,OAAO,EAAE,WAAW,EACpB,MAAM,EACN,WAAW,EACX,cAAc,GACf,EAAE,uBAAuB,GAAG,mBAAmB,CA+B/C;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,4EAA4E;IAC5E,YAAY,EAAE,kBAAkB,CAAA;IAChC,KAAK,EAAE,0BAA0B,CAAA;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,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;AAUD;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,EAAE,EACF,OAAO,EACP,YAAY,EACZ,KAAK,GACN,EAAE,4BAA4B,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAkCjE;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;CAC5C;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,EAClC,EAAE,EACF,OAAO,EACP,QAAQ,EACR,WAAW,GACZ,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoE9C"}
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,KAAK,EAGV,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAA;AAEnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAG1D,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,cAAc,CAAA;IACxB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;IAC3C,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,OAAO,EAAE,WAAW,EACpB,MAAM,EACN,WAAW,EACX,cAAc,GACf,EAAE,uBAAuB,GAAG,mBAAmB,CA+B/C;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,4EAA4E;IAC5E,YAAY,EAAE,kBAAkB,CAAA;IAChC,KAAK,EAAE,0BAA0B,CAAA;IACjC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,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;AAUD;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,EAAE,EACF,OAAO,EACP,YAAY,EACZ,KAAK,EACL,iBAAwB,GACzB,EAAE,4BAA4B,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAiDjE;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;IAC3C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAqBD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,EAClC,EAAE,EACF,OAAO,EACP,QAAQ,EACR,WAAW,EACX,iBAAwB,GACzB,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4F9C"}
package/dist/services.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isClaimedContentTypeAllowed } from "./detectContentType.js";
1
2
  import { UploadError } from "./errors.js";
2
3
  import { generateStorageKey } from "./keys.js";
3
4
  import {
@@ -8,6 +9,7 @@ import {
8
9
  import { resolveProfile } from "./profiles.js";
9
10
  import { resolveTarget } from "./targets.js";
10
11
  import { createUploadToken } from "./uploadToken.js";
12
+ import { verifyStoredContentType } from "./verifyStoredContentType.js";
11
13
  function issueUploadToken({
12
14
  profiles,
13
15
  profile: profileName,
@@ -54,10 +56,20 @@ async function createPresignedUpload({
54
56
  db,
55
57
  targets,
56
58
  tokenPayload,
57
- input
59
+ input,
60
+ verifyContentType = true
58
61
  }) {
59
62
  const size = toBigInt(input.size, "file size");
60
63
  assertTokenAllowsFile(tokenPayload, { contentType: input.contentType, size });
64
+ if (verifyContentType && !isClaimedContentTypeAllowed(
65
+ tokenPayload.allowedMimeTypes,
66
+ input.contentType
67
+ )) {
68
+ throw new UploadError(
69
+ "MIME_TYPE_NOT_ALLOWED",
70
+ `File type '${input.contentType}' is not allowed for upload profile '${tokenPayload.profile}'. Types that can carry script must be listed exactly, not through a wildcard.`
71
+ );
72
+ }
61
73
  const target = resolveTarget(targets, tokenPayload.target);
62
74
  const storageKey = generateStorageKey(input.contentType);
63
75
  const upload = await createPendingUpload({
@@ -80,11 +92,21 @@ async function createPresignedUpload({
80
92
  }
81
93
  return { uploadId: upload.id, ...presigned };
82
94
  }
95
+ async function failUpload(db, target, upload) {
96
+ const { count } = await db.upload.updateMany({
97
+ where: { id: upload.id, status: "pending" },
98
+ data: { status: "failed" }
99
+ });
100
+ if (count === 1) {
101
+ await target.delete(upload.storageKey);
102
+ }
103
+ }
83
104
  async function confirmUpload({
84
105
  db,
85
106
  targets,
86
107
  uploadId,
87
- currentUser
108
+ currentUser,
109
+ verifyContentType = true
88
110
  }) {
89
111
  const upload = await db.upload.findFirst({
90
112
  where: { id: uploadId },
@@ -102,22 +124,35 @@ async function confirmUpload({
102
124
  throw new UploadError("NOT_IN_STORAGE", "Upload not found in storage.");
103
125
  }
104
126
  const actualSize = await target.getObjectSize(upload.storageKey);
127
+ const storedUpload = { ...upload, storageKey: upload.storageKey };
105
128
  if (actualSize === null || BigInt(actualSize) !== upload.size) {
106
- const { count: count2 } = await db.upload.updateMany({
107
- where: { id: uploadId, status: "pending" },
108
- data: { status: "failed" }
109
- });
110
- if (count2 === 1) {
111
- await target.delete(upload.storageKey);
112
- }
129
+ await failUpload(db, target, storedUpload);
113
130
  throw new UploadError(
114
131
  "SIZE_MISMATCH",
115
132
  `Uploaded object is ${actualSize ?? "of unknown"} bytes but ${upload.size} bytes were authorized.`
116
133
  );
117
134
  }
135
+ let mimeType = upload.mimeType;
136
+ if (verifyContentType) {
137
+ const verified = await verifyStoredContentType(target, upload.storageKey, {
138
+ size: actualSize,
139
+ declaredType: upload.mimeType
140
+ });
141
+ if (!verified) {
142
+ await failUpload(db, target, storedUpload);
143
+ throw new UploadError(
144
+ "MIME_TYPE_NOT_ALLOWED",
145
+ `Uploaded file is not of the type '${upload.mimeType}' it was declared as, so it was deleted.`
146
+ );
147
+ }
148
+ mimeType = verified;
149
+ }
118
150
  const { count } = await db.upload.updateMany({
119
151
  where: { id: uploadId, status: "pending" },
120
- data: { status: "completed" }
152
+ data: {
153
+ status: "completed",
154
+ ...mimeType !== upload.mimeType ? { mimeType } : {}
155
+ }
121
156
  });
122
157
  const confirmed = await db.upload.findFirst({
123
158
  where: { id: uploadId },
package/dist/types.d.ts CHANGED
@@ -63,6 +63,7 @@ export interface UploadWhere {
63
63
  export interface UploadUpdateData {
64
64
  status?: UploadStatus;
65
65
  size?: bigint;
66
+ mimeType?: string;
66
67
  storageKey?: string | null;
67
68
  }
68
69
  /**
@@ -157,6 +158,15 @@ export interface StorageProvider {
157
158
  }): Promise<void>;
158
159
  /** Read the whole object into memory. */
159
160
  read(key: string): Promise<Buffer>;
161
+ /**
162
+ * Read `length` bytes starting at `offset` (fewer when the object ends
163
+ * first). Optional; checking a direct upload's content type falls back to
164
+ * `read()` when a provider does not implement it.
165
+ */
166
+ readRange?(key: string, range: {
167
+ offset: number;
168
+ length: number;
169
+ }): Promise<Buffer>;
160
170
  /**
161
171
  * Stream the object. Optional; the serve route falls back to `read()` when
162
172
  * a provider does not implement it.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { StorageProvider } from './types.js';
2
+ /**
3
+ * Checks an object that was uploaded straight to storage against the type
4
+ * the client declared for it when it asked for the upload URL. The declared
5
+ * type was checked against the upload profile then, and is the
6
+ * `Content-Type` the object was stored with, so the bytes must be that type
7
+ * (or an alias of it). Reads the first `DETECT_CONTENT_TYPE_BYTES`, plus the
8
+ * last `DETECT_CONTENT_TYPE_TAIL_BYTES` when the head is a ZIP, plus the
9
+ * start of the ZIP's central directory when that begins before the tail
10
+ * (at most `MAX_ZIP_DIRECTORY_BYTES`).
11
+ *
12
+ * Returns the canonical type to record, or `null` when the bytes aren't the
13
+ * declared type.
14
+ */
15
+ export declare function verifyStoredContentType(target: StorageProvider, key: string, { size, declaredType }: {
16
+ size: number;
17
+ declaredType: string;
18
+ }): Promise<string | null>;
19
+ //# sourceMappingURL=verifyStoredContentType.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifyStoredContentType.d.ts","sourceRoot":"","sources":["../src/verifyStoredContentType.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAmBjD;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,MAAM,EACX,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsCxB"}
@@ -0,0 +1,41 @@
1
+ import {
2
+ acceptedContentType,
3
+ DETECT_CONTENT_TYPE_BYTES,
4
+ DETECT_CONTENT_TYPE_TAIL_BYTES,
5
+ findZipCentralDirectory
6
+ } from "./detectContentType.js";
7
+ const ZIP_LOCAL_HEADER = [80, 75, 3, 4];
8
+ async function readRange(target, key, offset, length) {
9
+ if (target.readRange) {
10
+ return target.readRange(key, { offset, length });
11
+ }
12
+ return (await target.read(key)).subarray(offset, offset + length);
13
+ }
14
+ async function verifyStoredContentType(target, key, { size, declaredType }) {
15
+ const headLength = Math.min(size, DETECT_CONTENT_TYPE_BYTES);
16
+ const head = await readRange(target, key, 0, headLength);
17
+ let tail;
18
+ const isZip = ZIP_LOCAL_HEADER.every((byte, i) => head[i] === byte);
19
+ if (isZip && size > head.length) {
20
+ const tailLength = Math.min(size, DETECT_CONTENT_TYPE_TAIL_BYTES);
21
+ const tailStart = size - tailLength;
22
+ tail = await readRange(target, key, tailStart, tailLength);
23
+ const directory = findZipCentralDirectory(tail);
24
+ if (directory && directory.offset < tailStart && directory.offset + directory.size > tailStart) {
25
+ const missing = await readRange(
26
+ target,
27
+ key,
28
+ directory.offset,
29
+ tailStart - directory.offset
30
+ );
31
+ const joined = new Uint8Array(missing.length + tail.length);
32
+ joined.set(missing);
33
+ joined.set(tail, missing.length);
34
+ tail = joined;
35
+ }
36
+ }
37
+ return acceptedContentType(head, [declaredType], declaredType, { tail });
38
+ }
39
+ export {
40
+ verifyStoredContentType
41
+ };
@@ -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, and the row is flipped `pending` to
24
- * `completed` (or claimed `failed` and the object deleted on a mismatch).
25
- * Both transitions are conditional, so a duplicate notification or a row
26
- * the cleanup job already claimed is left alone.
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;AAQjE,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;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC/D,OAAO,CAAC,cAAc,CAAC,CAoDzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,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"}
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"}
@@ -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
- if (actualSize === null || BigInt(actualSize) !== upload.size) {
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: { status: "completed", size: BigInt(actualSize) }
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": "8.0.0-canary.3402",
3
+ "version": "8.0.0-canary.3403",
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": "8.0.0-canary.3402",
47
- "@cedarjs/context": "8.0.0-canary.3402",
48
- "@cedarjs/graphql-server": "8.0.0-canary.3402",
46
+ "@cedarjs/api": "8.0.0-canary.3403",
47
+ "@cedarjs/context": "8.0.0-canary.3403",
48
+ "@cedarjs/graphql-server": "8.0.0-canary.3403",
49
49
  "@fastify/multipart": "9.4.0",
50
50
  "graphql": "16.14.2",
51
51
  "mime-types": "2.1.35"
@@ -55,8 +55,8 @@
55
55
  "@arethetypeswrong/cli": "0.18.5",
56
56
  "@aws-sdk/client-s3": "3.1148.0",
57
57
  "@aws-sdk/s3-request-presigner": "3.1148.0",
58
- "@cedarjs/framework-tools": "8.0.0-canary.3402",
59
- "@cedarjs/web": "8.0.0-canary.3402",
58
+ "@cedarjs/framework-tools": "8.0.0-canary.3403",
59
+ "@cedarjs/web": "8.0.0-canary.3403",
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.3",
91
91
  "@aws-sdk/client-s3": "^3.0.0",
92
92
  "@aws-sdk/s3-request-presigner": "^3.0.0",
93
- "@cedarjs/web": "8.0.0-canary.3402",
93
+ "@cedarjs/web": "8.0.0-canary.3403",
94
94
  "@uppy/aws-s3": "^6.0.0",
95
95
  "@uppy/core": "^6.0.0",
96
96
  "@uppy/dashboard": "^6.0.0",