@humaan/payload-storage-imagekit 2.8.1 → 2.9.0

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 CHANGED
@@ -171,8 +171,13 @@ were not affected by this adapter issue.
171
171
  Client uploads now use ImageKit Upload API V2 and Payload's signed upload receipts.
172
172
  Payload chooses the sanitized filename and adds numeric suffixes for duplicates.
173
173
  For PNG, JPEG, GIF, and WebP images, the browser handler checks the file signature
174
- before authorization and corrects a mismatched MIME label or extension. ImageKit
175
- still verifies the actual content against the signed MIME and size checks.
174
+ before authorization and corrects a mismatched MIME label or extension. The signed
175
+ ImageKit checks bind the exact size and require ImageKit's detected content type to
176
+ match the collection's `upload.mimeTypes` (using Payload's prefix matching) and not
177
+ be an XML-family type. The browser's MIME type is not required to match ImageKit's
178
+ detection, because browsers derive it from the file extension and the two can
179
+ legitimately disagree (a macro-enabled `.xltm` template is detected as `.xlsx`, for
180
+ example). The detected type is checked again on completion, and is what Payload stores.
176
181
  Uploads go directly into the collection folder. Authorization binds the collection,
177
182
  user, path, filename, MIME type, size, privacy mode, expiry, a per-upload ImageKit tag,
178
183
  and create-only upload options. ImageKit metadata is checked on every completed upload,
@@ -238,19 +243,19 @@ upload: {
238
243
  The adapter streams Payload temporary files to ImageKit for server-side uploads,
239
244
  rather than loading those files into a second buffer.
240
245
 
241
- ## Folder Migration
246
+ ## Folder and account migration
242
247
 
243
- Use `migrateFolder` to copy uploads from one ImageKit root folder to another (e.g. staging to production).
248
+ Use `migrateFolder` to copy referenced originals between folders or ImageKit accounts (e.g. staging to production). It updates the supplied Payload database in place and retains source assets.
244
249
 
245
250
  Behavior:
246
251
 
247
- - Copies files one-by-one per collection
248
- - Creates destination folders automatically if they do not exist
249
- - Optional database update via Payload local API
250
- - Dry-run is enabled by default (`dryRun: true`)
251
- - Resolves each source asset by the stored `imagekit.fileId`, then copies the ImageKit-returned file path into the destination folder. This avoids mismatches between Payload's `filename` field and ImageKit's normalized file names.
252
- - Reruns are idempotent, including dry-runs: if a document's stored `imagekit.fileId` already resolves to a file in the destination collection folder, that document is skipped.
253
- - Handles [private files](#private-files): mark a collection `{ private: true }` and migrated documents are stored with `imagekit.isPrivate: true`. The fast server-side copy preserves the source file's ImageKit privacy; when the copy falls back to re-uploading from the source URL, the source URL is signed (so a private source can be read) and the file is re-uploaded with `isPrivateFile: true`. Because ImageKit privacy is fixed at upload, migrating assumes the destination collection's privacy matches the source — copying a public file into a `{ private: true }` collection keeps the file public on ImageKit.
252
+ - Scans documents in pages and skips assets already in the destination account's collection folder.
253
+ - Resolves stored file IDs in the source account and only transfers assets under the configured source collection folder.
254
+ - Dry-run is enabled by default and checks source assets and destination conflicts without migration writes.
255
+ - Transfers original bytes using URL uploads (`orig-true`), signing private source URLs with the source credentials.
256
+ - Tags uploads so reruns can recover even if an upload succeeded but its response or the database update failed.
257
+ - Reports unrelated destination filename conflicts instead of overwriting files.
258
+ - Updates ImageKit references, filenames, prefixes, legacy object keys and thumbnails together. Source privacy is preserved unless explicitly overridden per collection.
254
259
 
255
260
  Example:
256
261
 
@@ -261,39 +266,66 @@ import { getPayload } from 'payload'
261
266
 
262
267
  const payload = await getPayload({ config })
263
268
 
264
- const results = await migrateFolder({
265
- collections: { media: true },
266
- dryRun: true,
267
- from: process.env.IMAGEKIT_FOLDER!,
268
- payload,
269
- privateKey: process.env.IMAGEKIT_PRIVATE_KEY!,
270
- to: process.env.IMAGEKIT_FOLDER_MIGRATION_DESTINATION!,
271
- urlEndpoint: process.env.IMAGEKIT_URL_ENDPOINT!,
272
- })
273
-
274
- console.log(results)
275
- await payload.destroy()
269
+ try {
270
+ const results = await migrateFolder({
271
+ collections: { media: true },
272
+ dryRun: process.env.DRY_RUN !== 'false',
273
+ payload,
274
+ from: {
275
+ folder: process.env.IMAGEKIT_MIGRATION_FROM_FOLDER!,
276
+ privateKey: process.env.IMAGEKIT_MIGRATION_FROM_PRIVATE_KEY!,
277
+ urlEndpoint: process.env.IMAGEKIT_MIGRATION_FROM_ENDPOINT!,
278
+ },
279
+ to: {
280
+ folder: process.env.IMAGEKIT_MIGRATION_TO_FOLDER!,
281
+ privateKey: process.env.IMAGEKIT_MIGRATION_TO_PRIVATE_KEY!,
282
+ urlEndpoint: process.env.IMAGEKIT_MIGRATION_TO_ENDPOINT!,
283
+ },
284
+ })
285
+ console.log(results)
286
+ if (results.some(({ failed }) => failed > 0)) process.exitCode = 1
287
+ } finally {
288
+ await payload.destroy()
289
+ }
276
290
  ```
277
291
 
278
292
  `migrateFolder(options)` options:
279
293
 
280
- - `collections` (required): `Partial<Record<UploadCollectionSlug, true | { private?: boolean }>>`
281
- - `from` (required): source root folder, e.g. `/payload-uploads-staging`
282
- - `to` (required): destination root folder, e.g. `/payload-uploads-production`
283
- - `privateKey` (required): ImageKit private API key
284
- - `urlEndpoint` (required): ImageKit URL endpoint
294
+ - `collections` (required): `Partial<Record<UploadCollectionSlug, true | { private?: boolean }>>`. `true` preserves source privacy; an explicit `private` boolean sets destination privacy on transferred files.
295
+ - `from` / `to` (required): `{ folder, privateKey, urlEndpoint }`. Folder roots include collection subfolders. Use the same credentials for same-account migrations; identical folders in different accounts are supported. No public key is needed.
285
296
  - `dryRun` (optional, default `true`): when `true`, logs what would happen without writes
286
- - `payload` (optional): when provided and `dryRun` is `false`, updates `url` + `imagekit.*` fields
287
- - `signedUrlExpiresIn` (optional, default `900`): expiry in seconds for the signed URL used to read a private source file during the re-upload fallback
297
+ - `payload` (required): the database containing the records to update. Boot it with the destination storage configuration, particularly for private delivery.
298
+ - `signedUrlExpiresIn` (optional, default `900`): private source URL expiry in seconds.
299
+
300
+ Each collection returns `{ collectionSlug, planned, migrated, reused, skipped, failed, errors }`.
301
+ `reused` counts recovered uploads (also included in `migrated` after a live database update).
302
+ Per-document failures are reported in `errors`; configuration/query errors throw. There are no background jobs.
303
+
304
+ **Updating wrappers for 2.9:** replace string `from`/`to` and top-level credentials with the objects
305
+ above, always supply `payload`, and use the result counts instead of `status`/`jobId`.
306
+ `from.folder` now filters source assets; the old implementation only logged it.
307
+ The storage plugin's configuration is unchanged.
308
+
309
+ For the production → staging → production database-cloning workflow, run against the cloned
310
+ production database with staging as `from` and production as `to`. Existing production assets are
311
+ skipped; staging additions are transferred. Set normal app storage settings to the destination for
312
+ future uploads.
313
+
314
+ Transfers use a tagged URL upload in both same-account and cross-account cases, rather than a
315
+ native server-side copy. Keep migration tags intact until all records have been updated. A source
316
+ version change between attempts becomes a conflict rather than reusing stale content. Nested source
317
+ paths are flattened into the destination collection folder; collisions are reported. File version
318
+ history and account-specific custom metadata schemas are not transferred. Run one migration per
319
+ destination at a time; concurrent runs may report a conflict recoverable on a subsequent run.
288
320
 
289
321
  Run a migration script:
290
322
 
291
323
  ```bash
292
- # dry-run (default)
293
- pnpx tsx --env-file=../.env imagekit-migration.ts
324
+ # From the plugin repository root; dry-run is the default
325
+ DRY_RUN=true pnpm dlx tsx --env-file=.env.local dev/migration/imagekit-migration.ts
294
326
 
295
327
  # live run
296
- DRY_RUN=false pnpx tsx --env-file=../.env imagekit-migration.ts
328
+ DRY_RUN=false pnpm dlx tsx --env-file=.env.local dev/migration/imagekit-migration.ts
297
329
  ```
298
330
 
299
331
  ## Notes
@@ -77,13 +77,15 @@ export const createImageKitClientUploadVerifier = ({ client, collections })=>{
77
77
  const uploadedFile = await client.files.get(fileId);
78
78
  const uploadedMime = typeof uploadedFile.mime === 'string' ? getMimeTypeEssence(uploadedFile.mime) : '';
79
79
  const uploadedSize = uploadedFile.size;
80
+ // ImageKit's detected type is authoritative. It may differ from the browser's
81
+ // extension-based type, so validate it against the collection, not the claim.
80
82
  assertClientUploadAllowed({
81
83
  collection: collectionSecurity.collection,
82
84
  filename: context.filename,
83
85
  mimeType: uploadedMime
84
86
  });
85
87
  const allowedMimeTypes = typeof collectionSecurity.collection.upload === 'object' && Array.isArray(collectionSecurity.collection.upload.mimeTypes) ? collectionSecurity.collection.upload.mimeTypes : [];
86
- if (uploadedFile.fileId !== fileId || uploadedFile.filePath !== expectedFilePath || uploadedFile.name !== context.filename || context.uploadTag !== undefined && !uploadedFile.tags?.includes(context.uploadTag) || !uploadedMime || uploadedMime !== context.expectedMimeType || typeof uploadedSize !== 'number' || !Number.isSafeInteger(uploadedSize) || uploadedSize !== context.expectedSize || allowedMimeTypes.length > 0 && !validateMimeType(uploadedMime, allowedMimeTypes) || Boolean(uploadedFile.isPrivateFile) !== collectionSecurity.isPrivate || typeof uploadedFile.url !== 'string' || uploadedFile.url.length === 0) {
88
+ if (uploadedFile.fileId !== fileId || uploadedFile.filePath !== expectedFilePath || uploadedFile.name !== context.filename || context.uploadTag !== undefined && !uploadedFile.tags?.includes(context.uploadTag) || !uploadedMime || typeof uploadedSize !== 'number' || !Number.isSafeInteger(uploadedSize) || uploadedSize !== context.expectedSize || allowedMimeTypes.length > 0 && !validateMimeType(uploadedMime, allowedMimeTypes) || Boolean(uploadedFile.isPrivateFile) !== collectionSecurity.isPrivate || typeof uploadedFile.url !== 'string' || uploadedFile.url.length === 0) {
87
89
  throw new APIError('ImageKit client upload metadata did not match its authorization.', 400);
88
90
  }
89
91
  const verified = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/clientUploadVerification.ts"],"sourcesContent":["import type ImageKit from '@imagekit/nodejs'\nimport type { CollectionConfig } from 'payload'\n\nimport { buildStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { APIError } from 'payload'\nimport { assertClientUploadAllowed } from 'payload/internal'\nimport { validateMimeType } from 'payload/shared'\n\nimport type { ImageKitReceiptContext } from './clientUploads.js'\n\nimport { isImageKitReceiptContext } from './clientUploads.js'\n\nexport type VerifiedImageKitUpload = {\n fileId: string\n filePath: string\n fileType?: string\n mimeType: string\n name: string\n size: number\n thumbnailUrl?: string\n url: string\n}\n\ntype CollectionSecurityConfig = {\n collection: CollectionConfig\n isPrivate: boolean\n prefix: string\n}\n\nconst getMimeTypeEssence = (mimeType: string): string =>\n (mimeType.split(';', 1)[0] || '').trim().toLowerCase()\n\nconst toImageKitPath = (storageFilePath: string): string =>\n `/${storageFilePath.replace(/^\\/+/, '')}`\n\nconst assetLookupRetryDelays = [250, 500, 1000, 2000]\n\nconst findFileIDByPath = async (\n client: ImageKit,\n filePath: string,\n): Promise<string | undefined> => {\n const parentPath = `${filePath.slice(0, filePath.lastIndexOf('/') + 1)}`\n const name = filePath.slice(filePath.lastIndexOf('/') + 1)\n const limit = 100\n\n for (let skip = 0; ; skip += limit) {\n const assets = await client.assets.list({\n type: 'file',\n limit,\n path: parentPath,\n skip,\n })\n const found = assets.find(\n (asset) => 'fileId' in asset && asset.filePath === filePath && asset.name === name,\n )\n\n if (found && 'fileId' in found) {\n return found.fileId\n }\n\n if (assets.length < limit) {\n return undefined\n }\n }\n}\n\nexport const getClientUploadStorageFilePath = ({\n collectionPrefix,\n context,\n}: {\n collectionPrefix: string\n context: ImageKitReceiptContext\n}): string =>\n buildStoragePathData({\n collectionPrefix,\n docPrefix: [context.prefix, context._objectKey].filter(Boolean).join('/'),\n filename: context.filename,\n }).storageFilePath\n\nexport const createImageKitClientUploadVerifier = ({\n client,\n collections,\n}: {\n client: ImageKit\n collections: Record<string, CollectionSecurityConfig>\n}) => {\n const verifiedContexts = new WeakMap<object, VerifiedImageKitUpload>()\n\n return async ({\n collectionSlug,\n context,\n }: {\n collectionSlug: string\n context: unknown\n }): Promise<VerifiedImageKitUpload> => {\n if (!isImageKitReceiptContext(context)) {\n throw new APIError('Invalid ImageKit client upload context.', 400)\n }\n\n const collectionSecurity = collections[collectionSlug]\n if (\n !collectionSecurity ||\n context.collectionSlug !== collectionSlug ||\n context.isPrivate !== collectionSecurity.isPrivate\n ) {\n throw new APIError('ImageKit client upload reference does not match this collection.', 400)\n }\n\n const cached = verifiedContexts.get(context)\n if (cached) {\n return cached\n }\n\n const storageFilePath = getClientUploadStorageFilePath({\n collectionPrefix: collectionSecurity.prefix,\n context,\n })\n const expectedFilePath = toImageKitPath(storageFilePath)\n let fileId = context.fileId\n\n // Receipts without a file ID can still be verified by their signed path and binding.\n // The completion handshake avoids depending on asset listing visibility.\n if (!fileId) {\n fileId = await findFileIDByPath(client, expectedFilePath)\n for (const delay of assetLookupRetryDelays) {\n if (fileId) {\n break\n }\n await new Promise((resolve) => setTimeout(resolve, delay))\n fileId = await findFileIDByPath(client, expectedFilePath)\n }\n }\n\n if (!fileId) {\n throw new APIError('ImageKit client upload was not found at its authorized path.', 400)\n }\n\n const uploadedFile = await client.files.get(fileId)\n const uploadedMime =\n typeof uploadedFile.mime === 'string' ? getMimeTypeEssence(uploadedFile.mime) : ''\n const uploadedSize = uploadedFile.size\n\n assertClientUploadAllowed({\n collection: collectionSecurity.collection,\n filename: context.filename,\n mimeType: uploadedMime,\n })\n const allowedMimeTypes =\n typeof collectionSecurity.collection.upload === 'object' &&\n Array.isArray(collectionSecurity.collection.upload.mimeTypes)\n ? collectionSecurity.collection.upload.mimeTypes\n : []\n\n if (\n uploadedFile.fileId !== fileId ||\n uploadedFile.filePath !== expectedFilePath ||\n uploadedFile.name !== context.filename ||\n (context.uploadTag !== undefined && !uploadedFile.tags?.includes(context.uploadTag)) ||\n !uploadedMime ||\n uploadedMime !== context.expectedMimeType ||\n typeof uploadedSize !== 'number' ||\n !Number.isSafeInteger(uploadedSize) ||\n uploadedSize !== context.expectedSize ||\n (allowedMimeTypes.length > 0 && !validateMimeType(uploadedMime, allowedMimeTypes)) ||\n Boolean(uploadedFile.isPrivateFile) !== collectionSecurity.isPrivate ||\n typeof uploadedFile.url !== 'string' ||\n uploadedFile.url.length === 0\n ) {\n throw new APIError('ImageKit client upload metadata did not match its authorization.', 400)\n }\n\n const verified: VerifiedImageKitUpload = {\n name: uploadedFile.name,\n fileId,\n filePath: expectedFilePath,\n fileType: uploadedFile.fileType,\n mimeType: uploadedMime,\n size: uploadedSize,\n thumbnailUrl: typeof uploadedFile.thumbnail === 'string' ? uploadedFile.thumbnail : undefined,\n url: uploadedFile.url,\n }\n\n verifiedContexts.set(context, verified)\n return verified\n }\n}\n"],"names":["buildStoragePathData","APIError","assertClientUploadAllowed","validateMimeType","isImageKitReceiptContext","getMimeTypeEssence","mimeType","split","trim","toLowerCase","toImageKitPath","storageFilePath","replace","assetLookupRetryDelays","findFileIDByPath","client","filePath","parentPath","slice","lastIndexOf","name","limit","skip","assets","list","type","path","found","find","asset","fileId","length","undefined","getClientUploadStorageFilePath","collectionPrefix","context","docPrefix","prefix","_objectKey","filter","Boolean","join","filename","createImageKitClientUploadVerifier","collections","verifiedContexts","WeakMap","collectionSlug","collectionSecurity","isPrivate","cached","get","expectedFilePath","delay","Promise","resolve","setTimeout","uploadedFile","files","uploadedMime","mime","uploadedSize","size","collection","allowedMimeTypes","upload","Array","isArray","mimeTypes","uploadTag","tags","includes","expectedMimeType","Number","isSafeInteger","expectedSize","isPrivateFile","url","verified","fileType","thumbnailUrl","thumbnail","set"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/clientUploadVerification.ts"],"sourcesContent":["import type ImageKit from '@imagekit/nodejs'\nimport type { CollectionConfig } from 'payload'\n\nimport { buildStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { APIError } from 'payload'\nimport { assertClientUploadAllowed } from 'payload/internal'\nimport { validateMimeType } from 'payload/shared'\n\nimport type { ImageKitReceiptContext } from './clientUploads.js'\n\nimport { isImageKitReceiptContext } from './clientUploads.js'\n\nexport type VerifiedImageKitUpload = {\n fileId: string\n filePath: string\n fileType?: string\n mimeType: string\n name: string\n size: number\n thumbnailUrl?: string\n url: string\n}\n\ntype CollectionSecurityConfig = {\n collection: CollectionConfig\n isPrivate: boolean\n prefix: string\n}\n\nconst getMimeTypeEssence = (mimeType: string): string =>\n (mimeType.split(';', 1)[0] || '').trim().toLowerCase()\n\nconst toImageKitPath = (storageFilePath: string): string =>\n `/${storageFilePath.replace(/^\\/+/, '')}`\n\nconst assetLookupRetryDelays = [250, 500, 1000, 2000]\n\nconst findFileIDByPath = async (\n client: ImageKit,\n filePath: string,\n): Promise<string | undefined> => {\n const parentPath = `${filePath.slice(0, filePath.lastIndexOf('/') + 1)}`\n const name = filePath.slice(filePath.lastIndexOf('/') + 1)\n const limit = 100\n\n for (let skip = 0; ; skip += limit) {\n const assets = await client.assets.list({\n type: 'file',\n limit,\n path: parentPath,\n skip,\n })\n const found = assets.find(\n (asset) => 'fileId' in asset && asset.filePath === filePath && asset.name === name,\n )\n\n if (found && 'fileId' in found) {\n return found.fileId\n }\n\n if (assets.length < limit) {\n return undefined\n }\n }\n}\n\nexport const getClientUploadStorageFilePath = ({\n collectionPrefix,\n context,\n}: {\n collectionPrefix: string\n context: ImageKitReceiptContext\n}): string =>\n buildStoragePathData({\n collectionPrefix,\n docPrefix: [context.prefix, context._objectKey].filter(Boolean).join('/'),\n filename: context.filename,\n }).storageFilePath\n\nexport const createImageKitClientUploadVerifier = ({\n client,\n collections,\n}: {\n client: ImageKit\n collections: Record<string, CollectionSecurityConfig>\n}) => {\n const verifiedContexts = new WeakMap<object, VerifiedImageKitUpload>()\n\n return async ({\n collectionSlug,\n context,\n }: {\n collectionSlug: string\n context: unknown\n }): Promise<VerifiedImageKitUpload> => {\n if (!isImageKitReceiptContext(context)) {\n throw new APIError('Invalid ImageKit client upload context.', 400)\n }\n\n const collectionSecurity = collections[collectionSlug]\n if (\n !collectionSecurity ||\n context.collectionSlug !== collectionSlug ||\n context.isPrivate !== collectionSecurity.isPrivate\n ) {\n throw new APIError('ImageKit client upload reference does not match this collection.', 400)\n }\n\n const cached = verifiedContexts.get(context)\n if (cached) {\n return cached\n }\n\n const storageFilePath = getClientUploadStorageFilePath({\n collectionPrefix: collectionSecurity.prefix,\n context,\n })\n const expectedFilePath = toImageKitPath(storageFilePath)\n let fileId = context.fileId\n\n // Receipts without a file ID can still be verified by their signed path and binding.\n // The completion handshake avoids depending on asset listing visibility.\n if (!fileId) {\n fileId = await findFileIDByPath(client, expectedFilePath)\n for (const delay of assetLookupRetryDelays) {\n if (fileId) {\n break\n }\n await new Promise((resolve) => setTimeout(resolve, delay))\n fileId = await findFileIDByPath(client, expectedFilePath)\n }\n }\n\n if (!fileId) {\n throw new APIError('ImageKit client upload was not found at its authorized path.', 400)\n }\n\n const uploadedFile = await client.files.get(fileId)\n const uploadedMime =\n typeof uploadedFile.mime === 'string' ? getMimeTypeEssence(uploadedFile.mime) : ''\n const uploadedSize = uploadedFile.size\n\n // ImageKit's detected type is authoritative. It may differ from the browser's\n // extension-based type, so validate it against the collection, not the claim.\n assertClientUploadAllowed({\n collection: collectionSecurity.collection,\n filename: context.filename,\n mimeType: uploadedMime,\n })\n const allowedMimeTypes =\n typeof collectionSecurity.collection.upload === 'object' &&\n Array.isArray(collectionSecurity.collection.upload.mimeTypes)\n ? collectionSecurity.collection.upload.mimeTypes\n : []\n\n if (\n uploadedFile.fileId !== fileId ||\n uploadedFile.filePath !== expectedFilePath ||\n uploadedFile.name !== context.filename ||\n (context.uploadTag !== undefined && !uploadedFile.tags?.includes(context.uploadTag)) ||\n !uploadedMime ||\n typeof uploadedSize !== 'number' ||\n !Number.isSafeInteger(uploadedSize) ||\n uploadedSize !== context.expectedSize ||\n (allowedMimeTypes.length > 0 && !validateMimeType(uploadedMime, allowedMimeTypes)) ||\n Boolean(uploadedFile.isPrivateFile) !== collectionSecurity.isPrivate ||\n typeof uploadedFile.url !== 'string' ||\n uploadedFile.url.length === 0\n ) {\n throw new APIError('ImageKit client upload metadata did not match its authorization.', 400)\n }\n\n const verified: VerifiedImageKitUpload = {\n name: uploadedFile.name,\n fileId,\n filePath: expectedFilePath,\n fileType: uploadedFile.fileType,\n mimeType: uploadedMime,\n size: uploadedSize,\n thumbnailUrl: typeof uploadedFile.thumbnail === 'string' ? uploadedFile.thumbnail : undefined,\n url: uploadedFile.url,\n }\n\n verifiedContexts.set(context, verified)\n return verified\n }\n}\n"],"names":["buildStoragePathData","APIError","assertClientUploadAllowed","validateMimeType","isImageKitReceiptContext","getMimeTypeEssence","mimeType","split","trim","toLowerCase","toImageKitPath","storageFilePath","replace","assetLookupRetryDelays","findFileIDByPath","client","filePath","parentPath","slice","lastIndexOf","name","limit","skip","assets","list","type","path","found","find","asset","fileId","length","undefined","getClientUploadStorageFilePath","collectionPrefix","context","docPrefix","prefix","_objectKey","filter","Boolean","join","filename","createImageKitClientUploadVerifier","collections","verifiedContexts","WeakMap","collectionSlug","collectionSecurity","isPrivate","cached","get","expectedFilePath","delay","Promise","resolve","setTimeout","uploadedFile","files","uploadedMime","mime","uploadedSize","size","collection","allowedMimeTypes","upload","Array","isArray","mimeTypes","uploadTag","tags","includes","Number","isSafeInteger","expectedSize","isPrivateFile","url","verified","fileType","thumbnailUrl","thumbnail","set"],"mappings":"AAGA,SAASA,oBAAoB,QAAQ,6CAA4C;AACjF,SAASC,QAAQ,QAAQ,UAAS;AAClC,SAASC,yBAAyB,QAAQ,mBAAkB;AAC5D,SAASC,gBAAgB,QAAQ,iBAAgB;AAIjD,SAASC,wBAAwB,QAAQ,qBAAoB;AAmB7D,MAAMC,qBAAqB,CAACC,WAC1B,AAACA,CAAAA,SAASC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAC,EAAGC,IAAI,GAAGC,WAAW;AAEtD,MAAMC,iBAAiB,CAACC,kBACtB,CAAC,CAAC,EAAEA,gBAAgBC,OAAO,CAAC,QAAQ,KAAK;AAE3C,MAAMC,yBAAyB;IAAC;IAAK;IAAK;IAAM;CAAK;AAErD,MAAMC,mBAAmB,OACvBC,QACAC;IAEA,MAAMC,aAAa,GAAGD,SAASE,KAAK,CAAC,GAAGF,SAASG,WAAW,CAAC,OAAO,IAAI;IACxE,MAAMC,OAAOJ,SAASE,KAAK,CAACF,SAASG,WAAW,CAAC,OAAO;IACxD,MAAME,QAAQ;IAEd,IAAK,IAAIC,OAAO,IAAKA,QAAQD,MAAO;QAClC,MAAME,SAAS,MAAMR,OAAOQ,MAAM,CAACC,IAAI,CAAC;YACtCC,MAAM;YACNJ;YACAK,MAAMT;YACNK;QACF;QACA,MAAMK,QAAQJ,OAAOK,IAAI,CACvB,CAACC,QAAU,YAAYA,SAASA,MAAMb,QAAQ,KAAKA,YAAYa,MAAMT,IAAI,KAAKA;QAGhF,IAAIO,SAAS,YAAYA,OAAO;YAC9B,OAAOA,MAAMG,MAAM;QACrB;QAEA,IAAIP,OAAOQ,MAAM,GAAGV,OAAO;YACzB,OAAOW;QACT;IACF;AACF;AAEA,OAAO,MAAMC,iCAAiC,CAAC,EAC7CC,gBAAgB,EAChBC,OAAO,EAIR,GACCnC,qBAAqB;QACnBkC;QACAE,WAAW;YAACD,QAAQE,MAAM;YAAEF,QAAQG,UAAU;SAAC,CAACC,MAAM,CAACC,SAASC,IAAI,CAAC;QACrEC,UAAUP,QAAQO,QAAQ;IAC5B,GAAG/B,eAAe,CAAA;AAEpB,OAAO,MAAMgC,qCAAqC,CAAC,EACjD5B,MAAM,EACN6B,WAAW,EAIZ;IACC,MAAMC,mBAAmB,IAAIC;IAE7B,OAAO,OAAO,EACZC,cAAc,EACdZ,OAAO,EAIR;QACC,IAAI,CAAC/B,yBAAyB+B,UAAU;YACtC,MAAM,IAAIlC,SAAS,2CAA2C;QAChE;QAEA,MAAM+C,qBAAqBJ,WAAW,CAACG,eAAe;QACtD,IACE,CAACC,sBACDb,QAAQY,cAAc,KAAKA,kBAC3BZ,QAAQc,SAAS,KAAKD,mBAAmBC,SAAS,EAClD;YACA,MAAM,IAAIhD,SAAS,oEAAoE;QACzF;QAEA,MAAMiD,SAASL,iBAAiBM,GAAG,CAAChB;QACpC,IAAIe,QAAQ;YACV,OAAOA;QACT;QAEA,MAAMvC,kBAAkBsB,+BAA+B;YACrDC,kBAAkBc,mBAAmBX,MAAM;YAC3CF;QACF;QACA,MAAMiB,mBAAmB1C,eAAeC;QACxC,IAAImB,SAASK,QAAQL,MAAM;QAE3B,qFAAqF;QACrF,yEAAyE;QACzE,IAAI,CAACA,QAAQ;YACXA,SAAS,MAAMhB,iBAAiBC,QAAQqC;YACxC,KAAK,MAAMC,SAASxC,uBAAwB;gBAC1C,IAAIiB,QAAQ;oBACV;gBACF;gBACA,MAAM,IAAIwB,QAAQ,CAACC,UAAYC,WAAWD,SAASF;gBACnDvB,SAAS,MAAMhB,iBAAiBC,QAAQqC;YAC1C;QACF;QAEA,IAAI,CAACtB,QAAQ;YACX,MAAM,IAAI7B,SAAS,gEAAgE;QACrF;QAEA,MAAMwD,eAAe,MAAM1C,OAAO2C,KAAK,CAACP,GAAG,CAACrB;QAC5C,MAAM6B,eACJ,OAAOF,aAAaG,IAAI,KAAK,WAAWvD,mBAAmBoD,aAAaG,IAAI,IAAI;QAClF,MAAMC,eAAeJ,aAAaK,IAAI;QAEtC,8EAA8E;QAC9E,8EAA8E;QAC9E5D,0BAA0B;YACxB6D,YAAYf,mBAAmBe,UAAU;YACzCrB,UAAUP,QAAQO,QAAQ;YAC1BpC,UAAUqD;QACZ;QACA,MAAMK,mBACJ,OAAOhB,mBAAmBe,UAAU,CAACE,MAAM,KAAK,YAChDC,MAAMC,OAAO,CAACnB,mBAAmBe,UAAU,CAACE,MAAM,CAACG,SAAS,IACxDpB,mBAAmBe,UAAU,CAACE,MAAM,CAACG,SAAS,GAC9C,EAAE;QAER,IACEX,aAAa3B,MAAM,KAAKA,UACxB2B,aAAazC,QAAQ,KAAKoC,oBAC1BK,aAAarC,IAAI,KAAKe,QAAQO,QAAQ,IACrCP,QAAQkC,SAAS,KAAKrC,aAAa,CAACyB,aAAaa,IAAI,EAAEC,SAASpC,QAAQkC,SAAS,KAClF,CAACV,gBACD,OAAOE,iBAAiB,YACxB,CAACW,OAAOC,aAAa,CAACZ,iBACtBA,iBAAiB1B,QAAQuC,YAAY,IACpCV,iBAAiBjC,MAAM,GAAG,KAAK,CAAC5B,iBAAiBwD,cAAcK,qBAChExB,QAAQiB,aAAakB,aAAa,MAAM3B,mBAAmBC,SAAS,IACpE,OAAOQ,aAAamB,GAAG,KAAK,YAC5BnB,aAAamB,GAAG,CAAC7C,MAAM,KAAK,GAC5B;YACA,MAAM,IAAI9B,SAAS,oEAAoE;QACzF;QAEA,MAAM4E,WAAmC;YACvCzD,MAAMqC,aAAarC,IAAI;YACvBU;YACAd,UAAUoC;YACV0B,UAAUrB,aAAaqB,QAAQ;YAC/BxE,UAAUqD;YACVG,MAAMD;YACNkB,cAAc,OAAOtB,aAAauB,SAAS,KAAK,WAAWvB,aAAauB,SAAS,GAAGhD;YACpF4C,KAAKnB,aAAamB,GAAG;QACvB;QAEA/B,iBAAiBoC,GAAG,CAAC9C,SAAS0C;QAC9B,OAAOA;IACT;AACF,EAAC"}
@@ -12,5 +12,15 @@ type CreateImageKitUploadAuthHandlerArgs = {
12
12
  publicKey: string;
13
13
  verifyClientUpload: ReturnType<typeof createImageKitClientUploadVerifier>;
14
14
  };
15
+ /**
16
+ * ImageKit evaluates `file.mime` against its own content detection, which can differ from
17
+ * the browser's extension-based type (e.g. `.xltm` is detected as `.xlsx`). Constrain the
18
+ * detected type with the rules completion verification applies, not the browser's claim.
19
+ */
20
+ export declare const getUploadChecks: ({ allowedMimeTypes, allowRestrictedFileTypes, filesize, }: {
21
+ allowedMimeTypes: string[];
22
+ allowRestrictedFileTypes: boolean;
23
+ filesize: number;
24
+ }) => string;
15
25
  export declare const createImageKitUploadAuthHandler: ({ access, collections, privateKey, publicKey, verifyClientUpload, }: CreateImageKitUploadAuthHandlerArgs) => PayloadHandler;
16
26
  export {};
@@ -10,6 +10,37 @@ const defaultAccess = ({ req })=>Boolean(req.user);
10
10
  const bytesToMB = (bytes)=>(bytes / 1024 / 1024).toFixed(2);
11
11
  const getMimeTypeEssence = (mimeType)=>(mimeType.split(';', 1)[0] || '').trim().toLowerCase();
12
12
  const getImageKitSafeFilename = (filename)=>getSanitizedUploadFilename(filename).replace(/[^\w.-]/g, '_');
13
+ // ImageKit can't match Payload's `+xml` suffix rule, so name the XML-family types it may detect.
14
+ const XML_FAMILY_MIME_TYPES = [
15
+ 'application/atom+xml',
16
+ 'application/mathml+xml',
17
+ 'application/rdf+xml',
18
+ 'application/rss+xml',
19
+ 'application/xhtml+xml',
20
+ 'application/xml',
21
+ 'application/xslt+xml',
22
+ 'image/svg+xml',
23
+ 'text/xml'
24
+ ];
25
+ /**
26
+ * ImageKit evaluates `file.mime` against its own content detection, which can differ from
27
+ * the browser's extension-based type (e.g. `.xltm` is detected as `.xlsx`). Constrain the
28
+ * detected type with the rules completion verification applies, not the browser's claim.
29
+ */ export const getUploadChecks = ({ allowedMimeTypes, allowRestrictedFileTypes, filesize })=>{
30
+ const checks = [
31
+ `"file.size" = ${filesize}`
32
+ ];
33
+ // Mirror Payload's validateMimeType, which drops the first `*` and matches by prefix.
34
+ // Entries with quotes or backslashes can't prefix a valid MIME type, so omit them.
35
+ const prefixes = allowedMimeTypes.map((mimeType)=>mimeType.replace('*', '')).filter((prefix)=>!/["\\]/.test(prefix));
36
+ if (allowedMimeTypes.length > 0 && !prefixes.includes('')) {
37
+ checks.push(`(${prefixes.map((prefix)=>`"file.mime" : "${prefix}"`).join(' OR ')})`);
38
+ }
39
+ if (!allowRestrictedFileTypes) {
40
+ checks.push(`"file.mime" NOT IN [${XML_FAMILY_MIME_TYPES.map((type)=>`"${type}"`).join(', ')}]`);
41
+ }
42
+ return checks.join(' AND ');
43
+ };
13
44
  // Continue Payload's numeric suffix convention when the conflict exists only in ImageKit.
14
45
  const nextFilename = (filename)=>{
15
46
  const { name, ext } = posix.parse(filename);
@@ -134,7 +165,11 @@ export const createImageKitUploadAuthHandler = ({ access = defaultAccess, collec
134
165
  const folder = `/${resolved.storageFilePath.slice(0, -(sanitizedFilename.length + 1))}`;
135
166
  const uploadTag = `payload-upload-${randomUUID()}`;
136
167
  const uploadPayload = {
137
- checks: `"file.size" = ${requestedSize} AND "file.mime" = "${expectedMimeType}"`,
168
+ checks: getUploadChecks({
169
+ allowedMimeTypes,
170
+ allowRestrictedFileTypes: typeof collectionConfig?.upload === 'object' && Boolean(collectionConfig.upload.allowRestrictedFileTypes),
171
+ filesize: requestedSize
172
+ }),
138
173
  fileName: sanitizedFilename,
139
174
  folder,
140
175
  isPrivateFile: imageKitCollection.isPrivate ? 'true' : 'false',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/createImageKitUploadAuthHandler.ts"],"sourcesContent":["import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types'\nimport type { PayloadHandler, UploadCollectionSlug } from 'payload'\n\nimport { buildUploadStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { randomUUID } from 'node:crypto'\nimport { posix } from 'node:path'\nimport { APIError, Forbidden } from 'payload'\nimport {\n assertClientUploadAccess,\n assertClientUploadAllowed,\n assertClientUploadFileSize,\n createClientUploadReceipt,\n getSafeFileName,\n verifyClientUploadReceipt,\n} from 'payload/internal'\nimport { getSanitizedUploadFilename, validateMimeType } from 'payload/shared'\n\nimport type {\n ImageKitReceiptContext,\n ImageKitUploadAuthorization,\n ImageKitUploadCompletion,\n ImageKitUploadPayload,\n} from '../clientUploads.js'\nimport type { createImageKitClientUploadVerifier } from '../clientUploadVerification.js'\n\nimport { isImageKitReceiptContext } from '../clientUploads.js'\nimport { createImageKitUploadToken } from '../imageKitUploadToken.js'\n\ntype ImageKitUploadCollection = {\n isPrivate: boolean\n prefix: string\n}\n\ntype CreateImageKitUploadAuthHandlerArgs = {\n access?: ClientUploadsAccess\n collections: Partial<Record<UploadCollectionSlug, ImageKitUploadCollection>>\n privateKey: string\n publicKey: string\n verifyClientUpload: ReturnType<typeof createImageKitClientUploadVerifier>\n}\n\nconst defaultAccess: ClientUploadsAccess = ({ req }) => Boolean(req.user)\n\nconst bytesToMB = (bytes: number): string => (bytes / 1024 / 1024).toFixed(2)\n\nconst getMimeTypeEssence = (mimeType: string): string =>\n (mimeType.split(';', 1)[0] || '').trim().toLowerCase()\n\nconst getImageKitSafeFilename = (filename: string): string =>\n getSanitizedUploadFilename(filename).replace(/[^\\w.-]/g, '_')\n\n// Continue Payload's numeric suffix convention when the conflict exists only in ImageKit.\nconst nextFilename = (filename: string): string => {\n const { name, ext } = posix.parse(filename)\n const suffix = /^(.*)-(\\d+)$/.exec(name)\n return `${suffix ? `${suffix[1]}-${Number(suffix[2]) + 1}` : `${name}-1`}${ext}`\n}\n\nexport const createImageKitUploadAuthHandler = ({\n access = defaultAccess,\n collections,\n privateKey,\n publicKey,\n verifyClientUpload,\n}: CreateImageKitUploadAuthHandlerArgs): PayloadHandler => {\n return async (req) => {\n if (!req.json) {\n throw new APIError('Content-Type expected to be application/json', 400)\n }\n\n const body = (await req.json()) as {\n action?: unknown\n collectionSlug?: unknown\n fileId?: unknown\n filename?: unknown\n filesize?: unknown\n mimeType?: unknown\n signedReceipt?: unknown\n }\n const { action, collectionSlug, fileId, signedReceipt } = body\n let { filename, filesize, mimeType } = body\n\n if (typeof collectionSlug !== 'string' || !Object.hasOwn(collections, collectionSlug)) {\n throw new APIError('Invalid upload payload.', 400)\n }\n\n await assertClientUploadAccess({ collectionSlug, req })\n\n const imageKitCollection = collections[collectionSlug]\n if (!imageKitCollection) {\n throw new APIError(`Collection ${collectionSlug} was not found in ImageKit options.`, 400)\n }\n\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n\n if (action === 'complete') {\n if (typeof fileId !== 'string' || !fileId.trim() || typeof signedReceipt !== 'string') {\n throw new APIError('Invalid ImageKit upload completion payload.', 400)\n }\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n if (\n !isImageKitReceiptContext(receipt.context) ||\n receipt.filename !== receipt.context.filename ||\n (receipt.context.fileId !== undefined && receipt.context.fileId !== fileId)\n ) {\n throw new APIError('Invalid ImageKit client upload context.', 400)\n }\n\n // The browser supplies a lookup ID, not proof. Check provider state against the\n // original signed authorization before including that ID in a new signed receipt.\n const context: ImageKitReceiptContext = { ...receipt.context, fileId }\n await verifyClientUpload({ collectionSlug, context })\n delete context.uploadTag\n const response: ImageKitUploadCompletion = {\n clientUploadContext: {\n _objectKey: context._objectKey,\n prefix: context.prefix,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context,\n filename: context.filename,\n req,\n }),\n },\n }\n return Response.json(response, { headers: { 'Cache-Control': 'no-store' } })\n }\n\n if (action === 'retry') {\n if (typeof signedReceipt !== 'string') {\n throw new APIError('Invalid ImageKit upload retry payload.', 400)\n }\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n if (\n !isImageKitReceiptContext(receipt.context) ||\n receipt.filename !== receipt.context.filename ||\n receipt.context.fileId !== undefined\n ) {\n throw new APIError('Invalid ImageKit upload retry payload.', 400)\n }\n filename = nextFilename(receipt.context.filename)\n filesize = receipt.context.expectedSize\n mimeType = receipt.context.expectedMimeType\n }\n\n if (\n (action !== undefined && action !== 'retry') ||\n typeof filename !== 'string' ||\n typeof mimeType !== 'string'\n ) {\n throw new APIError('Invalid upload payload.', 400)\n }\n\n assertClientUploadAllowed({\n collection: req.payload.collections[collectionSlug]?.config,\n filename,\n mimeType,\n })\n assertClientUploadFileSize(filesize)\n\n const requestedSize = filesize as number\n const expectedMimeType = getMimeTypeEssence(mimeType)\n if (!expectedMimeType) {\n throw new APIError('A valid MIME type is required for ImageKit client uploads.', 400)\n }\n\n const collectionConfig = req.payload.collections[collectionSlug]?.config\n const allowedMimeTypes =\n collectionConfig &&\n typeof collectionConfig.upload === 'object' &&\n Array.isArray(collectionConfig.upload.mimeTypes)\n ? collectionConfig.upload.mimeTypes\n : []\n\n if (allowedMimeTypes.length > 0 && !validateMimeType(expectedMimeType, allowedMimeTypes)) {\n throw new APIError(`File type ${expectedMimeType} is not allowed.`, 400)\n }\n\n const filesizeLimit = req.payload.config.upload.limits?.fileSize\n\n if (\n typeof filesizeLimit === 'number' &&\n Number.isFinite(filesizeLimit) &&\n requestedSize > filesizeLimit\n ) {\n throw new APIError(\n `Exceeded file size limit. Limit: ${bytesToMB(filesizeLimit)}MB, got: ${bytesToMB(requestedSize)}MB`,\n 400,\n )\n }\n\n const sanitizedFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: getImageKitSafeFilename(filename),\n req,\n })\n const resolved = buildUploadStoragePathData({\n collectionPrefix: imageKitCollection.prefix,\n docPrefix: imageKitCollection.prefix,\n filename: sanitizedFilename,\n })\n const folder = `/${resolved.storageFilePath.slice(0, -(sanitizedFilename.length + 1))}`\n const uploadTag = `payload-upload-${randomUUID()}`\n const uploadPayload: ImageKitUploadPayload = {\n checks: `\"file.size\" = ${requestedSize} AND \"file.mime\" = \"${expectedMimeType}\"`,\n fileName: sanitizedFilename,\n folder,\n isPrivateFile: imageKitCollection.isPrivate ? 'true' : 'false',\n overwriteFile: 'false',\n tags: uploadTag,\n useUniqueFileName: 'false',\n }\n const receiptContext: ImageKitReceiptContext = {\n collectionSlug,\n expectedMimeType,\n expectedSize: requestedSize,\n filename: sanitizedFilename,\n isPrivate: imageKitCollection.isPrivate,\n prefix: resolved.sanitizedDocPrefix,\n uploadTag,\n }\n const response: ImageKitUploadAuthorization = {\n clientUploadContext: {\n prefix: resolved.sanitizedDocPrefix,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context: receiptContext,\n filename: sanitizedFilename,\n req,\n }),\n },\n filename: sanitizedFilename,\n token: createImageKitUploadToken({ privateKey, publicKey, uploadPayload }),\n uploadPayload,\n }\n\n return Response.json(response, {\n headers: {\n 'Cache-Control': 'no-store',\n },\n })\n }\n}\n"],"names":["buildUploadStoragePathData","randomUUID","posix","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","assertClientUploadFileSize","createClientUploadReceipt","getSafeFileName","verifyClientUploadReceipt","getSanitizedUploadFilename","validateMimeType","isImageKitReceiptContext","createImageKitUploadToken","defaultAccess","req","Boolean","user","bytesToMB","bytes","toFixed","getMimeTypeEssence","mimeType","split","trim","toLowerCase","getImageKitSafeFilename","filename","replace","nextFilename","name","ext","parse","suffix","exec","Number","createImageKitUploadAuthHandler","access","collections","privateKey","publicKey","verifyClientUpload","json","body","action","collectionSlug","fileId","signedReceipt","filesize","Object","hasOwn","imageKitCollection","t","receipt","context","undefined","uploadTag","response","clientUploadContext","_objectKey","prefix","Response","headers","expectedSize","expectedMimeType","collection","payload","config","requestedSize","collectionConfig","allowedMimeTypes","upload","Array","isArray","mimeTypes","length","filesizeLimit","limits","fileSize","isFinite","sanitizedFilename","desiredFilename","resolved","collectionPrefix","docPrefix","folder","storageFilePath","slice","uploadPayload","checks","fileName","isPrivateFile","isPrivate","overwriteFile","tags","useUniqueFileName","receiptContext","sanitizedDocPrefix","token"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/createImageKitUploadAuthHandler.ts"],"sourcesContent":["import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types'\nimport type { PayloadHandler, UploadCollectionSlug } from 'payload'\n\nimport { buildUploadStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { randomUUID } from 'node:crypto'\nimport { posix } from 'node:path'\nimport { APIError, Forbidden } from 'payload'\nimport {\n assertClientUploadAccess,\n assertClientUploadAllowed,\n assertClientUploadFileSize,\n createClientUploadReceipt,\n getSafeFileName,\n verifyClientUploadReceipt,\n} from 'payload/internal'\nimport { getSanitizedUploadFilename, validateMimeType } from 'payload/shared'\n\nimport type {\n ImageKitReceiptContext,\n ImageKitUploadAuthorization,\n ImageKitUploadCompletion,\n ImageKitUploadPayload,\n} from '../clientUploads.js'\nimport type { createImageKitClientUploadVerifier } from '../clientUploadVerification.js'\n\nimport { isImageKitReceiptContext } from '../clientUploads.js'\nimport { createImageKitUploadToken } from '../imageKitUploadToken.js'\n\ntype ImageKitUploadCollection = {\n isPrivate: boolean\n prefix: string\n}\n\ntype CreateImageKitUploadAuthHandlerArgs = {\n access?: ClientUploadsAccess\n collections: Partial<Record<UploadCollectionSlug, ImageKitUploadCollection>>\n privateKey: string\n publicKey: string\n verifyClientUpload: ReturnType<typeof createImageKitClientUploadVerifier>\n}\n\nconst defaultAccess: ClientUploadsAccess = ({ req }) => Boolean(req.user)\n\nconst bytesToMB = (bytes: number): string => (bytes / 1024 / 1024).toFixed(2)\n\nconst getMimeTypeEssence = (mimeType: string): string =>\n (mimeType.split(';', 1)[0] || '').trim().toLowerCase()\n\nconst getImageKitSafeFilename = (filename: string): string =>\n getSanitizedUploadFilename(filename).replace(/[^\\w.-]/g, '_')\n\n// ImageKit can't match Payload's `+xml` suffix rule, so name the XML-family types it may detect.\nconst XML_FAMILY_MIME_TYPES = [\n 'application/atom+xml',\n 'application/mathml+xml',\n 'application/rdf+xml',\n 'application/rss+xml',\n 'application/xhtml+xml',\n 'application/xml',\n 'application/xslt+xml',\n 'image/svg+xml',\n 'text/xml',\n]\n\n/**\n * ImageKit evaluates `file.mime` against its own content detection, which can differ from\n * the browser's extension-based type (e.g. `.xltm` is detected as `.xlsx`). Constrain the\n * detected type with the rules completion verification applies, not the browser's claim.\n */\nexport const getUploadChecks = ({\n allowedMimeTypes,\n allowRestrictedFileTypes,\n filesize,\n}: {\n allowedMimeTypes: string[]\n allowRestrictedFileTypes: boolean\n filesize: number\n}): string => {\n const checks = [`\"file.size\" = ${filesize}`]\n // Mirror Payload's validateMimeType, which drops the first `*` and matches by prefix.\n // Entries with quotes or backslashes can't prefix a valid MIME type, so omit them.\n const prefixes = allowedMimeTypes\n .map((mimeType) => mimeType.replace('*', ''))\n .filter((prefix) => !/[\"\\\\]/.test(prefix))\n\n if (allowedMimeTypes.length > 0 && !prefixes.includes('')) {\n checks.push(`(${prefixes.map((prefix) => `\"file.mime\" : \"${prefix}\"`).join(' OR ')})`)\n }\n\n if (!allowRestrictedFileTypes) {\n checks.push(`\"file.mime\" NOT IN [${XML_FAMILY_MIME_TYPES.map((type) => `\"${type}\"`).join(', ')}]`)\n }\n\n return checks.join(' AND ')\n}\n\n// Continue Payload's numeric suffix convention when the conflict exists only in ImageKit.\nconst nextFilename = (filename: string): string => {\n const { name, ext } = posix.parse(filename)\n const suffix = /^(.*)-(\\d+)$/.exec(name)\n return `${suffix ? `${suffix[1]}-${Number(suffix[2]) + 1}` : `${name}-1`}${ext}`\n}\n\nexport const createImageKitUploadAuthHandler = ({\n access = defaultAccess,\n collections,\n privateKey,\n publicKey,\n verifyClientUpload,\n}: CreateImageKitUploadAuthHandlerArgs): PayloadHandler => {\n return async (req) => {\n if (!req.json) {\n throw new APIError('Content-Type expected to be application/json', 400)\n }\n\n const body = (await req.json()) as {\n action?: unknown\n collectionSlug?: unknown\n fileId?: unknown\n filename?: unknown\n filesize?: unknown\n mimeType?: unknown\n signedReceipt?: unknown\n }\n const { action, collectionSlug, fileId, signedReceipt } = body\n let { filename, filesize, mimeType } = body\n\n if (typeof collectionSlug !== 'string' || !Object.hasOwn(collections, collectionSlug)) {\n throw new APIError('Invalid upload payload.', 400)\n }\n\n await assertClientUploadAccess({ collectionSlug, req })\n\n const imageKitCollection = collections[collectionSlug]\n if (!imageKitCollection) {\n throw new APIError(`Collection ${collectionSlug} was not found in ImageKit options.`, 400)\n }\n\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n\n if (action === 'complete') {\n if (typeof fileId !== 'string' || !fileId.trim() || typeof signedReceipt !== 'string') {\n throw new APIError('Invalid ImageKit upload completion payload.', 400)\n }\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n if (\n !isImageKitReceiptContext(receipt.context) ||\n receipt.filename !== receipt.context.filename ||\n (receipt.context.fileId !== undefined && receipt.context.fileId !== fileId)\n ) {\n throw new APIError('Invalid ImageKit client upload context.', 400)\n }\n\n // The browser supplies a lookup ID, not proof. Check provider state against the\n // original signed authorization before including that ID in a new signed receipt.\n const context: ImageKitReceiptContext = { ...receipt.context, fileId }\n await verifyClientUpload({ collectionSlug, context })\n delete context.uploadTag\n const response: ImageKitUploadCompletion = {\n clientUploadContext: {\n _objectKey: context._objectKey,\n prefix: context.prefix,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context,\n filename: context.filename,\n req,\n }),\n },\n }\n return Response.json(response, { headers: { 'Cache-Control': 'no-store' } })\n }\n\n if (action === 'retry') {\n if (typeof signedReceipt !== 'string') {\n throw new APIError('Invalid ImageKit upload retry payload.', 400)\n }\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n if (\n !isImageKitReceiptContext(receipt.context) ||\n receipt.filename !== receipt.context.filename ||\n receipt.context.fileId !== undefined\n ) {\n throw new APIError('Invalid ImageKit upload retry payload.', 400)\n }\n filename = nextFilename(receipt.context.filename)\n filesize = receipt.context.expectedSize\n mimeType = receipt.context.expectedMimeType\n }\n\n if (\n (action !== undefined && action !== 'retry') ||\n typeof filename !== 'string' ||\n typeof mimeType !== 'string'\n ) {\n throw new APIError('Invalid upload payload.', 400)\n }\n\n assertClientUploadAllowed({\n collection: req.payload.collections[collectionSlug]?.config,\n filename,\n mimeType,\n })\n assertClientUploadFileSize(filesize)\n\n const requestedSize = filesize as number\n const expectedMimeType = getMimeTypeEssence(mimeType)\n if (!expectedMimeType) {\n throw new APIError('A valid MIME type is required for ImageKit client uploads.', 400)\n }\n\n const collectionConfig = req.payload.collections[collectionSlug]?.config\n const allowedMimeTypes =\n collectionConfig &&\n typeof collectionConfig.upload === 'object' &&\n Array.isArray(collectionConfig.upload.mimeTypes)\n ? collectionConfig.upload.mimeTypes\n : []\n\n if (allowedMimeTypes.length > 0 && !validateMimeType(expectedMimeType, allowedMimeTypes)) {\n throw new APIError(`File type ${expectedMimeType} is not allowed.`, 400)\n }\n\n const filesizeLimit = req.payload.config.upload.limits?.fileSize\n\n if (\n typeof filesizeLimit === 'number' &&\n Number.isFinite(filesizeLimit) &&\n requestedSize > filesizeLimit\n ) {\n throw new APIError(\n `Exceeded file size limit. Limit: ${bytesToMB(filesizeLimit)}MB, got: ${bytesToMB(requestedSize)}MB`,\n 400,\n )\n }\n\n const sanitizedFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: getImageKitSafeFilename(filename),\n req,\n })\n const resolved = buildUploadStoragePathData({\n collectionPrefix: imageKitCollection.prefix,\n docPrefix: imageKitCollection.prefix,\n filename: sanitizedFilename,\n })\n const folder = `/${resolved.storageFilePath.slice(0, -(sanitizedFilename.length + 1))}`\n const uploadTag = `payload-upload-${randomUUID()}`\n const uploadPayload: ImageKitUploadPayload = {\n checks: getUploadChecks({\n allowedMimeTypes,\n allowRestrictedFileTypes:\n typeof collectionConfig?.upload === 'object' &&\n Boolean(collectionConfig.upload.allowRestrictedFileTypes),\n filesize: requestedSize,\n }),\n fileName: sanitizedFilename,\n folder,\n isPrivateFile: imageKitCollection.isPrivate ? 'true' : 'false',\n overwriteFile: 'false',\n tags: uploadTag,\n useUniqueFileName: 'false',\n }\n const receiptContext: ImageKitReceiptContext = {\n collectionSlug,\n expectedMimeType,\n expectedSize: requestedSize,\n filename: sanitizedFilename,\n isPrivate: imageKitCollection.isPrivate,\n prefix: resolved.sanitizedDocPrefix,\n uploadTag,\n }\n const response: ImageKitUploadAuthorization = {\n clientUploadContext: {\n prefix: resolved.sanitizedDocPrefix,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context: receiptContext,\n filename: sanitizedFilename,\n req,\n }),\n },\n filename: sanitizedFilename,\n token: createImageKitUploadToken({ privateKey, publicKey, uploadPayload }),\n uploadPayload,\n }\n\n return Response.json(response, {\n headers: {\n 'Cache-Control': 'no-store',\n },\n })\n }\n}\n"],"names":["buildUploadStoragePathData","randomUUID","posix","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","assertClientUploadFileSize","createClientUploadReceipt","getSafeFileName","verifyClientUploadReceipt","getSanitizedUploadFilename","validateMimeType","isImageKitReceiptContext","createImageKitUploadToken","defaultAccess","req","Boolean","user","bytesToMB","bytes","toFixed","getMimeTypeEssence","mimeType","split","trim","toLowerCase","getImageKitSafeFilename","filename","replace","XML_FAMILY_MIME_TYPES","getUploadChecks","allowedMimeTypes","allowRestrictedFileTypes","filesize","checks","prefixes","map","filter","prefix","test","length","includes","push","join","type","nextFilename","name","ext","parse","suffix","exec","Number","createImageKitUploadAuthHandler","access","collections","privateKey","publicKey","verifyClientUpload","json","body","action","collectionSlug","fileId","signedReceipt","Object","hasOwn","imageKitCollection","t","receipt","context","undefined","uploadTag","response","clientUploadContext","_objectKey","Response","headers","expectedSize","expectedMimeType","collection","payload","config","requestedSize","collectionConfig","upload","Array","isArray","mimeTypes","filesizeLimit","limits","fileSize","isFinite","sanitizedFilename","desiredFilename","resolved","collectionPrefix","docPrefix","folder","storageFilePath","slice","uploadPayload","fileName","isPrivateFile","isPrivate","overwriteFile","tags","useUniqueFileName","receiptContext","sanitizedDocPrefix","token"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export type PayloadStorageImageKitConfig = {
41
41
  };
42
42
  export declare const imagekitStorage: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
43
43
  export { migrateFolder } from './migrate.js';
44
- export type { MigrateFolderOptions, MigrationJobResult } from './migrate.js';
44
+ export type { MigrateFolderOptions, MigrationJobResult, MigrationLocation } from './migrate.js';
45
45
  export declare const payloadStorageImagekit: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
46
46
  export declare const payloadStorageImageKit: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
47
47
  export type PayloadStorageImagekitConfig = PayloadStorageImageKitConfig;
package/dist/index.js CHANGED
@@ -538,7 +538,7 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
538
538
  }
539
539
  const context = req.file.clientUploadContext;
540
540
  const requestMimeType = req.file.mimetype.split(';', 1)[0]?.trim().toLowerCase();
541
- if (!isImageKitReceiptContext(context) || context.filename !== data.filename || context.filename !== req.file.name || context.expectedMimeType !== requestMimeType || // Payload can clear req.file.size when preparing an in-memory image.
541
+ if (!isImageKitReceiptContext(context) || context.filename !== data.filename || context.filename !== req.file.name || // Payload can clear req.file.size when preparing an in-memory image.
542
542
  // generateFileData preserves the original size on the document.
543
543
  context.expectedSize !== data.filesize) {
544
544
  throw new APIError('ImageKit client upload reference does not match this request.', 400);
@@ -547,6 +547,11 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
547
547
  collectionSlug: collection.slug,
548
548
  context
549
549
  });
550
+ // Payload keeps the browser's MIME unless it fetched the file content, in which
551
+ // case it uses the verified ImageKit-detected type the static handler served.
552
+ if (requestMimeType !== context.expectedMimeType && requestMimeType !== uploaded.mimeType) {
553
+ throw new APIError('ImageKit client upload reference does not match this request.', 400);
554
+ }
550
555
  // A provider file can only be claimed once, including through legacy receipts.
551
556
  // The unique file ID index also rejects concurrent claims.
552
557
  const existing = await req.payload.db.findOne({