@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 +65 -33
- package/dist/clientUploadVerification.js +3 -1
- package/dist/clientUploadVerification.js.map +1 -1
- package/dist/endpoints/createImageKitUploadAuthHandler.d.ts +10 -0
- package/dist/endpoints/createImageKitUploadAuthHandler.js +36 -1
- package/dist/endpoints/createImageKitUploadAuthHandler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts +30 -45
- package/dist/migrate.js +220 -241
- package/dist/migrate.js.map +1 -1
- package/package.json +16 -16
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.
|
|
175
|
-
|
|
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
|
|
246
|
+
## Folder and account migration
|
|
242
247
|
|
|
243
|
-
Use `migrateFolder` to copy
|
|
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
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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):
|
|
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` (
|
|
287
|
-
- `signedUrlExpiresIn` (optional, default `900`):
|
|
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
|
|
293
|
-
|
|
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
|
|
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 ||
|
|
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":"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;QAEtC5D,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,gBACDA,iBAAiBxB,QAAQqC,gBAAgB,IACzC,OAAOX,iBAAiB,YACxB,CAACY,OAAOC,aAAa,CAACb,iBACtBA,iBAAiB1B,QAAQwC,YAAY,IACpCX,iBAAiBjC,MAAM,GAAG,KAAK,CAAC5B,iBAAiBwD,cAAcK,qBAChExB,QAAQiB,aAAamB,aAAa,MAAM5B,mBAAmBC,SAAS,IACpE,OAAOQ,aAAaoB,GAAG,KAAK,YAC5BpB,aAAaoB,GAAG,CAAC9C,MAAM,KAAK,GAC5B;YACA,MAAM,IAAI9B,SAAS,oEAAoE;QACzF;QAEA,MAAM6E,WAAmC;YACvC1D,MAAMqC,aAAarC,IAAI;YACvBU;YACAd,UAAUoC;YACV2B,UAAUtB,aAAasB,QAAQ;YAC/BzE,UAAUqD;YACVG,MAAMD;YACNmB,cAAc,OAAOvB,aAAawB,SAAS,KAAK,WAAWxB,aAAawB,SAAS,GAAGjD;YACpF6C,KAAKpB,aAAaoB,GAAG;QACvB;QAEAhC,iBAAiBqC,GAAG,CAAC/C,SAAS2C;QAC9B,OAAOA;IACT;AACF,EAAC"}
|
|
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":"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"}
|
|
@@ -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:
|
|
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 ||
|
|
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({
|