@humaan/payload-storage-imagekit 2.9.1 → 3.0.0-canary.1

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.
Files changed (31) hide show
  1. package/README.md +73 -7
  2. package/dist/clientUploads.d.ts +4 -41
  3. package/dist/clientUploads.js +1 -8
  4. package/dist/clientUploads.js.map +1 -1
  5. package/dist/components/BeforeDashboardServer.d.ts +1 -1
  6. package/dist/components/BeforeDashboardServer.js +6 -1
  7. package/dist/components/BeforeDashboardServer.js.map +1 -1
  8. package/dist/components/ImageKitUploadHandler.d.ts +3 -4
  9. package/dist/components/ImageKitUploadHandler.js +69 -80
  10. package/dist/components/ImageKitUploadHandler.js.map +1 -1
  11. package/dist/endpoints/completeImageKitUpload.d.ts +10 -0
  12. package/dist/endpoints/completeImageKitUpload.js +84 -0
  13. package/dist/endpoints/completeImageKitUpload.js.map +1 -0
  14. package/dist/endpoints/createImageKitUploadInstructions.d.ts +14 -0
  15. package/dist/endpoints/createImageKitUploadInstructions.js +117 -0
  16. package/dist/endpoints/createImageKitUploadInstructions.js.map +1 -0
  17. package/dist/imageKitFileStore.d.ts +129 -0
  18. package/dist/imageKitFileStore.js +134 -0
  19. package/dist/imageKitFileStore.js.map +1 -0
  20. package/dist/index.d.ts +6 -28
  21. package/dist/index.js +465 -640
  22. package/dist/index.js.map +1 -1
  23. package/dist/migrate.js +4 -1
  24. package/dist/migrate.js.map +1 -1
  25. package/package.json +13 -13
  26. package/dist/clientUploadVerification.d.ts +0 -30
  27. package/dist/clientUploadVerification.js +0 -106
  28. package/dist/clientUploadVerification.js.map +0 -1
  29. package/dist/endpoints/createImageKitUploadAuthHandler.d.ts +0 -26
  30. package/dist/endpoints/createImageKitUploadAuthHandler.js +0 -215
  31. package/dist/endpoints/createImageKitUploadAuthHandler.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,691 +1,516 @@
1
1
  import ImageKit, { toFile } from '@imagekit/nodejs';
2
2
  import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage';
3
- import { buildStoragePathData, buildUploadStoragePathData, getStorageFilePath, initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities';
3
+ import { buildStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities';
4
4
  import { openAsBlob } from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { APIError } from 'payload';
7
- import { getRangeRequestInfo, isXmlMimeType, UPLOAD_CONTENT_SECURITY_POLICY } from 'payload/internal';
8
- import { validateMimeType } from 'payload/shared';
9
- import { imageKitUploadAuthPath, isImageKitReceiptContext } from './clientUploads.js';
10
- import { createImageKitClientUploadVerifier, getClientUploadStorageFilePath } from './clientUploadVerification.js';
11
- import { createImageKitUploadAuthHandler } from './endpoints/createImageKitUploadAuthHandler.js';
7
+ import { assertClientUploadAllowed, getRangeRequestInfo, isXmlMimeType, uploadContentSecurityPolicy, verifyClientUploadReceipt } from 'payload/internal';
8
+ import { completeImageKitUpload } from './endpoints/completeImageKitUpload.js';
9
+ import { createImageKitUploadInstructions } from './endpoints/createImageKitUploadInstructions.js';
12
10
  import { getAdminThumbnailURL, getPrivateAdminThumbnailURL, isPdfMimeType, isVideoMimeType } from './getAdminThumbnailURL.js';
11
+ import { createImageKitFileStore } from './imageKitFileStore.js';
13
12
  import { getImageKitThumbnailSrc, getVideoPosterSrc } from './imageTransforms.js';
14
- const DEFAULT_SIGNED_URL_EXPIRES_IN = 900;
15
- /**
16
- * ImageKit re-encodes some uploads and serves the smaller derivative from the plain
17
- * delivery URL — a 1.9 MB MP4 can come back as 868 KB. That is exactly what we want
18
- * for visitors, but not when reading the file back to verify a client upload: the
19
- * bytes and the byte count would both belong to a file ImageKit's API never reported.
20
- * `orig-true` asks for the untouched original and does not disable optimisation for
21
- * anyone else.
22
- */ const ORIGINAL_FILE_TRANSFORMATION = 'orig-true';
23
- const PRIVATE_COLLECTION_DESCRIPTION = 'Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.';
24
- const isPrivateCollectionConfig = (value)=>value !== true && typeof value === 'object' && value?.private === true;
25
- const XML_FAMILY_MIME_TYPES = [
26
- 'application/xml',
27
- 'image/svg+xml',
28
- 'text/xml'
29
- ];
30
- const collectionCanStoreXml = (collection)=>{
31
- const upload = collection.upload;
32
- if (!upload || upload === true) {
33
- return Boolean(upload);
34
- }
35
- // Payload skips MIME and content restrictions when this escape hatch is enabled.
36
- if (upload.allowRestrictedFileTypes) {
37
- return true;
38
- }
39
- const allowedMimeTypes = upload.mimeTypes;
40
- return !allowedMimeTypes?.length || allowedMimeTypes.some((mimeType)=>mimeType.includes('*') || isXmlMimeType(mimeType)) || XML_FAMILY_MIME_TYPES.some((mimeType)=>validateMimeType(mimeType, allowedMimeTypes));
41
- };
42
- /**
43
- * Whether a specific document is private. The flag stored on the document at upload time
44
- * (`imagekit.isPrivate`) is the source of truth — a file's privacy on ImageKit is fixed
45
- * when it is uploaded and does not change if the collection's `private` setting changes
46
- * later. Falls back to the collection setting for documents uploaded before this flag existed.
47
- */ const isDocumentPrivate = (doc, collectionIsPrivate)=>{
48
- const stored = doc?.imagekit?.isPrivate;
49
- return typeof stored === 'boolean' ? stored : collectionIsPrivate;
50
- };
51
- const normalizeFolder = (folder)=>{
52
- const normalized = folder.trim().replace(/^\/+|\/+$/g, '');
53
- if (!normalized) {
54
- throw new Error('imagekitStorage: `folder` is required.');
55
- }
56
- return `/${normalized}`;
57
- };
58
- const getCollectionFolder = (rootFolder, collectionSlug)=>path.posix.join(rootFolder, collectionSlug);
59
- const findImageKitFileId = async (imagekit, filePath)=>{
60
- const parentPath = path.posix.dirname(filePath);
61
- const name = path.posix.basename(filePath);
62
- const limit = 100;
63
- for(let skip = 0;; skip += limit){
64
- const assets = await imagekit.assets.list({
65
- type: 'file',
66
- limit,
67
- path: parentPath === '/' ? '/' : `${parentPath}/`,
68
- skip
69
- });
70
- const found = assets.find((asset)=>'fileId' in asset && asset.filePath === filePath && asset.name === name);
71
- if (found && 'fileId' in found) {
72
- return found.fileId;
73
- }
74
- if (assets.length < limit) {
75
- break;
13
+ /** Payload 4 storage capability. Register under config.storage, not config.plugins. */ export const imagekitStorage = (options)=>({
14
+ name: 'imagekit',
15
+ collections: Object.keys(options.collections),
16
+ init: (config)=>initializeStorage(config, options)
17
+ });
18
+ export { migrateFolder } from './migrate.js';
19
+ export const payloadStorageImagekit = imagekitStorage;
20
+ export const payloadStorageImageKit = imagekitStorage;
21
+ function initializeStorage(config, options) {
22
+ for (const key of [
23
+ 'privateKey',
24
+ 'publicKey',
25
+ 'urlEndpoint',
26
+ 'folder'
27
+ ]){
28
+ if (!options[key]?.trim()) {
29
+ throw new Error(`imagekitStorage: \`${key}\` is required.`);
76
30
  }
77
31
  }
78
- return undefined;
79
- };
80
- const isRecord = (value)=>!!value && typeof value === 'object';
81
- const getStoredImageKitURL = (doc)=>{
82
- const storedURL = doc?.imagekit?.url;
83
- return typeof storedURL === 'string' && storedURL.trim().length > 0 ? storedURL : undefined;
84
- };
85
- const hasStoredImageKitURL = (doc)=>!!getStoredImageKitURL(doc);
86
- const findUploadDocumentByFilename = async ({ collection, filename, prefix, req })=>{
87
- const constraints = [
88
- {
89
- filename: {
90
- equals: filename
91
- }
92
- }
93
- ];
94
- if (typeof prefix === 'string') {
95
- constraints.push({
96
- prefix: {
97
- equals: prefix
98
- }
99
- });
32
+ const folder = options.folder.trim().replace(/^\/+|\/+$/g, '');
33
+ if (folder.split('/').some((segment)=>!segment || segment === '.' || segment === '..')) {
34
+ throw new Error('imagekitStorage: `folder` must not contain empty or dot segments.');
100
35
  }
101
- return req.payload.db.findOne({
102
- collection,
103
- req,
104
- where: {
105
- and: constraints
106
- }
36
+ const client = new ImageKit({
37
+ privateKey: options.privateKey
107
38
  });
108
- };
109
- const applyVerifiedClientUploadToData = (data, uploaded, buildThumbnailURL = getAdminThumbnailURL, isPrivate)=>{
110
- data.imagekit = {
111
- fileId: uploaded.fileId,
112
- isPrivate,
113
- thumbnailUrl: uploaded.thumbnailUrl,
114
- url: uploaded.url
39
+ const collections = options.collections;
40
+ if (!Object.keys(collections).length) {
41
+ throw new Error('imagekitStorage: `collections` must not be empty.');
42
+ }
43
+ const configured = new Set(Object.keys(collections));
44
+ const apiRoute = config.routes?.api ?? '/api';
45
+ const expiresIn = options.signedUrlExpiresIn ?? 900;
46
+ const prefixFor = (slug)=>path.posix.join(folder, slug);
47
+ const collectionIsPrivate = (slug)=>{
48
+ const value = collections[slug];
49
+ return value !== true && value?.private === true;
115
50
  };
116
- // Preserve the admin preview when the original file was already uploaded client-side.
117
- data.thumbnailURL = buildThumbnailURL(data);
118
- return data;
119
- };
120
- const imagekitAdapter = ({ apiRoute, client, clientUploads, privateSlugs, rootFolder, signedUrlExpiresIn, urlEndpoint, verifyClientUpload })=>{
121
- const fetchUploadedFile = async ({ fileSize, fileURL, mimeType, range })=>{
122
- const rangeResult = getRangeRequestInfo({
123
- fileSize,
124
- rangeHeader: range ?? null
125
- });
126
- if (rangeResult.type === 'invalid') {
127
- return new Response(null, {
128
- headers: new Headers(rangeResult.headers),
129
- status: rangeResult.status
130
- });
131
- }
132
- const normalizedRange = rangeResult.type === 'partial' ? `bytes=${rangeResult.rangeStart}-${rangeResult.rangeEnd}` : undefined;
133
- // Payload cancels image probes after reading a bounded prefix. Next.js memoizes
134
- // GET responses by teeing their streams; cancelling one branch can then wait
135
- // forever for its unread cached sibling. An explicit signal opts out of that
136
- // memoization, while no-store also excludes these reads from the data cache.
137
- const response = await fetch(fileURL, {
138
- cache: 'no-store',
139
- ...normalizedRange ? {
140
- headers: {
141
- Range: normalizedRange
51
+ const files = createImageKitFileStore(client, options.urlEndpoint);
52
+ const completeUpload = completeImageKitUpload({
53
+ access: typeof options.clientUploads === 'object' ? options.clientUploads.access : undefined,
54
+ collections: Object.fromEntries(Object.keys(collections).map((slug)=>[
55
+ slug,
56
+ {
57
+ isPrivate: collectionIsPrivate(slug),
58
+ prefix: prefixFor(slug)
142
59
  }
143
- } : {},
144
- signal: new AbortController().signal
145
- });
146
- if (!response.ok || response.status !== rangeResult.status) {
147
- void response.body?.cancel().catch(()=>undefined);
148
- throw new Error(`imagekitStorage: ImageKit returned an unexpected file response (${response.status}).`);
149
- }
150
- const upstreamLength = response.headers.get('Content-Length');
151
- const expectedLength = rangeResult.headers['Content-Length'];
152
- const upstreamRange = response.headers.get('Content-Range');
153
- const expectedRange = rangeResult.type === 'partial' ? rangeResult.headers['Content-Range'] : undefined;
154
- if (upstreamLength !== null && upstreamLength !== expectedLength || expectedRange && upstreamRange !== expectedRange) {
155
- void response.body?.cancel().catch(()=>undefined);
156
- throw new Error('imagekitStorage: ImageKit returned mismatched file content metadata.');
157
- }
158
- const headers = new Headers(rangeResult.headers);
159
- headers.set('Content-Type', mimeType);
160
- if (isXmlMimeType(mimeType)) {
161
- headers.set('Content-Security-Policy', UPLOAD_CONTENT_SECURITY_POLICY);
162
- }
163
- return new Response(response.body, {
164
- headers,
165
- status: response.status
166
- });
60
+ ])),
61
+ files
62
+ });
63
+ const sourceConfig = {
64
+ ...config,
65
+ collections: (config.collections ?? []).map((collection)=>{
66
+ if (!configured.has(collection.slug)) {
67
+ return collection;
68
+ }
69
+ const isPrivate = collectionIsPrivate(collection.slug);
70
+ if (options.disablePayloadAccessControl && (isPrivate || canStoreXml(collection))) {
71
+ throw new Error('imagekitStorage: private and XML-capable collections require Payload access control.');
72
+ }
73
+ const setting = collections[collection.slug];
74
+ const focalPoint = setting !== true ? setting?.focalPoint : undefined;
75
+ const description = collection.admin?.description;
76
+ return {
77
+ ...collection,
78
+ admin: isPrivate ? {
79
+ ...collection.admin,
80
+ description: typeof description === 'string' ? `${description} Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.` : description ?? 'Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.'
81
+ } : collection.admin,
82
+ hooks: {
83
+ ...collection.hooks,
84
+ beforeChange: [
85
+ ...collection.hooks?.beforeChange ?? [],
86
+ async ({ data, operation, originalDoc, req })=>{
87
+ if (operation === 'validate') {
88
+ return data;
89
+ }
90
+ const verified = req.context.imagekitVerifiedUpload;
91
+ if (!verified) {
92
+ return data;
93
+ }
94
+ const existing = await req.payload.db.findOne({
95
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Generated consumer configs narrow CollectionSlug, unlike the standalone package build.
96
+ collection: collection.slug,
97
+ req,
98
+ where: {
99
+ 'imagekit.fileId': {
100
+ equals: verified.fileId
101
+ }
102
+ }
103
+ });
104
+ if (existing && existing.id !== originalDoc?.id) {
105
+ throw new APIError('This ImageKit upload reference has already been used.', 400);
106
+ }
107
+ return applyMetadata(data, verified, isPrivate, collection.slug, apiRoute);
108
+ }
109
+ ]
110
+ },
111
+ upload: {
112
+ focalPoint: focalPoint ?? options.focalPoint ?? false,
113
+ ...typeof collection.upload === 'object' ? collection.upload : {},
114
+ adminThumbnail: ({ doc })=>isPrivateDocument(doc, isPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc),
115
+ crop: false
116
+ }
117
+ };
118
+ })
167
119
  };
168
- return ({ collection, prefix = getCollectionFolder(rootFolder, collection.slug) })=>{
169
- const isPrivate = privateSlugs.has(collection.slug);
170
- // Sign a relative ImageKit path against the configured urlEndpoint. We always sign
171
- // a computed path (never a stored absolute URL) because ImageKit's signature is
172
- // derived from the path after stripping urlEndpoint, so the host must match.
173
- const buildSignedURL = (src, tr)=>client.helper.buildSrc({
174
- expiresIn: signedUrlExpiresIn,
175
- signed: true,
176
- src,
177
- transformation: tr ? [
120
+ const adapter = ({ collection, prefix = prefixFor(collection.slug) })=>{
121
+ const isPrivate = collectionIsPrivate(collection.slug);
122
+ const buildURL = (key, signed, transformation)=>client.helper.buildSrc({
123
+ expiresIn: signed ? expiresIn : undefined,
124
+ signed,
125
+ src: `/${key.replace(/^\/+/, '')}`,
126
+ transformation: transformation ? [
178
127
  {
179
- raw: tr
128
+ raw: transformation
180
129
  }
181
130
  ] : undefined,
182
- urlEndpoint
131
+ urlEndpoint: options.urlEndpoint
132
+ });
133
+ const deleteFile = ({ req, storageFilePath })=>files.remove({
134
+ key: storageFilePath,
135
+ req
183
136
  });
184
- const buildThumbnailURL = (doc)=>isDocumentPrivate(doc, isPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc);
185
137
  return {
186
138
  name: 'imagekit',
187
- clientUploads,
188
- fields: [
189
- {
190
- // Presentational wrapper: tucks the ImageKit details into a closed panel without
191
- // changing the `imagekit.*` data paths.
192
- type: 'collapsible',
193
- admin: {
194
- condition: (data)=>hasStoredImageKitURL(data),
195
- initCollapsed: true
196
- },
197
- fields: [
198
- {
199
- name: 'imagekit',
200
- type: 'group',
201
- admin: {
202
- disableListColumn: true,
203
- disableListFilter: true
204
- },
205
- fields: [
206
- {
207
- name: 'fileId',
208
- type: 'text',
209
- admin: {
210
- hidden: true
211
- }
212
- },
213
- {
214
- name: 'url',
215
- type: 'text',
216
- admin: {
217
- readOnly: true
218
- },
219
- label: 'URL'
220
- },
221
- {
222
- name: 'isPrivate',
223
- type: 'checkbox',
224
- admin: {
225
- condition: (_data, siblingData)=>isDocumentPrivate({
226
- imagekit: siblingData
227
- }, isPrivate),
228
- description: "Indicates whether this file was uploaded to ImageKit as private. Private files are delivered through signed URLs. Changes to this collection's private option in payload.config only affect future uploads.",
229
- readOnly: true
230
- },
231
- label: 'Private file'
232
- },
233
- {
234
- name: 'signedUrl',
235
- type: 'text',
236
- admin: {
237
- // Only relevant for private files; the plain `url` works directly for public ones.
238
- condition: (_data, siblingData)=>isDocumentPrivate({
239
- imagekit: siblingData
240
- }, isPrivate),
241
- description: `Temporary signed URL for reaching this private file directly on ImageKit. Expires ${signedUrlExpiresIn} seconds after load and is regenerated on every read. Note: anyone with this URL can access the file until it expires — it is not gated by Payload access control.`,
242
- readOnly: true
243
- },
244
- hooks: {
245
- // Computed server-side on read so it is always freshly signed and never persisted.
246
- afterRead: [
247
- ({ data, siblingData })=>{
248
- if (!isDocumentPrivate({
249
- imagekit: siblingData
250
- }, isPrivate)) {
251
- return undefined;
252
- }
253
- const filename = data?.filename;
254
- if (!filename) {
255
- return undefined;
256
- }
257
- const source = data || {};
258
- const documentPrefix = path.posix.join(source.prefix || prefix, source._objectKey || '');
259
- const storageFilePath = buildStoragePathData({
260
- collectionPrefix: prefix,
261
- docPrefix: documentPrefix,
262
- filename
263
- }).storageFilePath;
264
- return buildSignedURL(`/${storageFilePath}`);
265
- }
266
- ]
267
- },
268
- label: 'Signed URL',
269
- virtual: true
270
- },
271
- {
272
- name: 'thumbnailUrl',
273
- type: 'text',
274
- admin: {
275
- hidden: true
276
- }
277
- }
278
- ],
279
- interfaceName: 'imagekitFields',
280
- label: false
281
- }
282
- ],
283
- label: 'Storage: ImageKit'
139
+ copyFile: async ({ from, req, to })=>{
140
+ if (await files.find({
141
+ key: to,
142
+ req
143
+ })) {
144
+ throw new Error(`ImageKit destination already exists: ${to}`);
284
145
  }
285
- ],
286
- generateURL: ({ filename, prefix: documentPrefix = '' })=>client.helper.buildSrc({
287
- src: `/${buildStoragePathData({
288
- collectionPrefix: prefix,
289
- docPrefix: documentPrefix,
290
- filename
291
- }).storageFilePath}`,
292
- urlEndpoint
293
- }),
294
- handleDelete: async ({ doc, storageFilePath })=>{
295
- const storedFileId = doc.imagekit?.fileId;
296
- const fileId = storedFileId || await findImageKitFileId(client, `/${storageFilePath.replace(/^\/+/, '')}`);
297
- if (!fileId) {
298
- return;
146
+ const source = await files.find({
147
+ key: from,
148
+ req
149
+ });
150
+ if (!source) {
151
+ throw new Error(`ImageKit source not found: ${from}`);
299
152
  }
300
- await client.files.delete(fileId);
153
+ // Upload by URL copies the exact original without buffering it in the app process.
154
+ // overwriteFile:false protects a destination created concurrently with the lookup.
155
+ await files.upload({
156
+ body: {
157
+ customMetadata: source.customMetadata ?? undefined,
158
+ file: buildURL(from, Boolean(source.isPrivateFile), 'orig-true'),
159
+ fileName: path.posix.basename(to),
160
+ folder: `/${path.posix.dirname(to)}`,
161
+ isPrivateFile: source.isPrivateFile ?? false,
162
+ overwriteFile: false,
163
+ tags: source.tags ?? undefined,
164
+ useUniqueFileName: false
165
+ },
166
+ key: to,
167
+ req
168
+ });
301
169
  },
302
- handleUpload: async ({ clientUploadContext, data, file, storageFilePath })=>{
303
- if (clientUploadContext) {
304
- const uploaded = await verifyClientUpload({
305
- collectionSlug: collection.slug,
306
- context: clientUploadContext
307
- });
308
- return applyVerifiedClientUploadToData(data, uploaded, buildThumbnailURL, isPrivate);
309
- }
310
- // ImageKit's `toFile()` consumes async iterables before starting the request. A file-backed
311
- // Blob keeps temporary-file uploads lazy so FormData can stream them from disk instead.
170
+ deleteFile,
171
+ fields: storageFields({
172
+ buildURL,
173
+ expiresIn,
174
+ isPrivate,
175
+ prefix
176
+ }),
177
+ generateURL: ({ filename, prefix: docPrefix })=>buildURL(buildStoragePathData({
178
+ collectionPrefix: prefix,
179
+ docPrefix,
180
+ filename
181
+ }).storageFilePath, false),
182
+ handleDelete: deleteFile,
183
+ handleUpload: async ({ data, file, req, storageFilePath })=>{
312
184
  const uploadSource = file.tempFilePath ? await openAsBlob(file.tempFilePath, {
313
185
  type: file.mimeType
314
186
  }) : file.buffer;
315
- const uploadFile = await toFile(uploadSource, file.filename, {
316
- type: file.mimeType
317
- });
318
- const uploadFolder = path.posix.dirname(`/${storageFilePath.replace(/^\/+/, '')}`);
319
- const uploadFilename = path.posix.basename(storageFilePath);
320
- const uploaded = await client.files.upload({
321
- file: uploadFile,
322
- fileName: uploadFilename,
323
- folder: uploadFolder,
324
- useUniqueFileName: false,
325
- ...isPrivate ? {
326
- isPrivateFile: true
327
- } : {}
187
+ const uploaded = await files.upload({
188
+ body: {
189
+ file: await toFile(uploadSource, file.filename, {
190
+ type: file.mimeType
191
+ }),
192
+ fileName: path.posix.basename(storageFilePath),
193
+ folder: `/${path.posix.dirname(storageFilePath)}`,
194
+ isPrivateFile: isPrivate,
195
+ overwriteFile: false,
196
+ useUniqueFileName: false
197
+ },
198
+ key: storageFilePath,
199
+ req
328
200
  });
329
- data.imagekit = {
201
+ return applyMetadata(data, {
202
+ name: path.posix.basename(storageFilePath),
330
203
  fileId: uploaded.fileId,
331
- isPrivate,
332
- thumbnailUrl: uploaded.thumbnailUrl,
204
+ isPrivateFile: isPrivate,
205
+ mime: file.mimeType,
206
+ size: file.filesize,
207
+ thumbnail: uploaded.thumbnailUrl,
333
208
  url: uploaded.url
334
- };
335
- // Set the root-level thumbnailURL that Payload's admin interface expects.
336
- // `buildThumbnailURL` needs the filename, which Payload may not have copied onto
337
- // `data` yet at this point, so fall back to the uploaded file's name.
338
- data.thumbnailURL = buildThumbnailURL({
339
- ...data,
340
- filename: data.filename || uploadFilename
341
- });
342
- return data;
209
+ }, isPrivate, collection.slug, apiRoute);
343
210
  },
344
- requiresClientUploadReceipt: true,
345
- staticHandler: async (req, args)=>{
346
- const requestURL = new URL(req.url || 'http://localhost');
347
- const tr = requestURL.searchParams.get('tr');
348
- const hasClientUploadContext = 'clientUploadContext' in args.params;
349
- if (hasClientUploadContext) {
350
- const clientUploadContext = args.params.clientUploadContext;
351
- if (!isImageKitReceiptContext(clientUploadContext)) {
352
- throw new APIError('Invalid ImageKit client upload context.', 400);
211
+ staticHandler: async (req, { doc, params })=>{
212
+ if (params.uploadReference) {
213
+ try {
214
+ const reference = params.uploadReference;
215
+ const receipt = verifyClientUploadReceipt({
216
+ collectionSlug: collection.slug,
217
+ req,
218
+ signedReceipt: reference.signedReceipt
219
+ });
220
+ const expectedPath = buildStoragePathData({
221
+ collectionPrefix: prefix,
222
+ docPrefix: path.posix.join(receipt.filePrefix, receipt._objectKey ?? ''),
223
+ filename: params.filename
224
+ }).storageFilePath;
225
+ if (!receipt._objectKey || expectedPath !== receipt.storageFilePath || !expectedPath.startsWith(`${prefix.replace(/^\/+/, '')}/`)) {
226
+ throw new APIError('Invalid ImageKit upload reference.', 400);
227
+ }
228
+ const uploaded = await findUploadedFile(files, expectedPath, req);
229
+ if (!uploaded || !uploaded.mime || Boolean(uploaded.isPrivateFile) !== isPrivate) {
230
+ throw new APIError('ImageKit upload did not match its authorization.', 400);
231
+ }
232
+ assertClientUploadAllowed({
233
+ collection,
234
+ filename: uploaded.name,
235
+ mimeType: uploaded.mime
236
+ });
237
+ req.context.imagekitVerifiedUpload = uploaded;
238
+ return await fetchOriginal({
239
+ file: uploaded,
240
+ req,
241
+ url: buildURL(expectedPath, isPrivate, 'orig-true')
242
+ });
243
+ } catch (error) {
244
+ if (error instanceof APIError) {
245
+ return Response.json({
246
+ message: error.message
247
+ }, {
248
+ status: error.status
249
+ });
250
+ }
251
+ throw error;
353
252
  }
354
- const uploadedFile = await verifyClientUpload({
355
- collectionSlug: collection.slug,
356
- context: clientUploadContext
357
- });
358
- const expectedFilePath = getClientUploadStorageFilePath({
359
- collectionPrefix: prefix,
360
- context: clientUploadContext
361
- });
362
- const verifiedFileURL = isPrivate ? buildSignedURL(`/${expectedFilePath}`, ORIGINAL_FILE_TRANSFORMATION) : client.helper.buildSrc({
363
- src: `/${expectedFilePath}`,
364
- transformation: [
365
- {
366
- raw: ORIGINAL_FILE_TRANSFORMATION
367
- }
368
- ],
369
- urlEndpoint
370
- });
371
- return fetchUploadedFile({
372
- fileSize: uploadedFile.size,
373
- fileURL: verifiedFileURL,
374
- mimeType: uploadedFile.mimeType,
375
- range: req.headers.get('Range')
376
- });
377
253
  }
378
- const doc = args.doc || await findUploadDocumentByFilename({
379
- collection: args.params.collection,
380
- filename: args.params.filename,
381
- prefix: args.params.prefix,
382
- req
254
+ const document = doc ?? await req.payload.db.findOne({
255
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Generated consumer configs narrow CollectionSlug, unlike the standalone package build.
256
+ collection: collection.slug,
257
+ req,
258
+ where: {
259
+ filename: {
260
+ equals: params.filename
261
+ }
262
+ }
383
263
  });
384
- const storageFilePath = await getStorageFilePath({
385
- collection,
264
+ const source = getFileLocation(document, params.filename);
265
+ const key = buildStoragePathData({
386
266
  collectionPrefix: prefix,
387
- doc: doc || undefined,
388
- filename: args.params.filename,
389
- prefixQueryParam: args.params.prefix,
390
- req
391
- });
392
- const imageKitPath = `/${storageFilePath.replace(/^\/+/, '')}`;
393
- const mimeType = doc?.mimeType;
394
- // XML-family uploads (including SVG) must pass through Payload so its CSP is
395
- // present even when ImageKit uses a custom, same-origin delivery domain.
396
- if (typeof mimeType === 'string' && isXmlMimeType(mimeType)) {
397
- const filesize = doc?.filesize;
398
- if (typeof filesize !== 'number' || !Number.isSafeInteger(filesize) || filesize < 0) {
399
- throw new APIError('Stored ImageKit file metadata is incomplete.', 500);
400
- }
401
- const fileURL = isDocumentPrivate(doc, isPrivate) ? buildSignedURL(imageKitPath, ORIGINAL_FILE_TRANSFORMATION) : client.helper.buildSrc({
402
- src: imageKitPath,
403
- transformation: [
404
- {
405
- raw: ORIGINAL_FILE_TRANSFORMATION
406
- }
407
- ],
408
- urlEndpoint
267
+ docPrefix: path.posix.join(params.prefix ?? source.prefix ?? prefix, source._objectKey ?? ''),
268
+ filename: params.filename
269
+ }).storageFilePath;
270
+ const privateFile = isPrivateDocument(document, isPrivate);
271
+ if (params.operation === 'transform' || isXmlMimeType(source.mimeType ?? '')) {
272
+ const file = await files.find({
273
+ key,
274
+ req
409
275
  });
410
- return fetchUploadedFile({
411
- fileSize: filesize,
412
- fileURL,
413
- mimeType,
414
- range: req.headers.get('Range')
276
+ if (!file?.mime) {
277
+ throw new APIError('ImageKit source not found.', 404);
278
+ }
279
+ return fetchOriginal({
280
+ file,
281
+ req,
282
+ url: buildURL(key, privateFile, 'orig-true')
415
283
  });
416
284
  }
417
- // Sign based on the document's stored status, not the collection's current
418
- // setting, so files keep working if the collection's privacy is changed later.
419
- if (isDocumentPrivate(doc, isPrivate)) {
420
- let signSrc = imageKitPath;
421
- // A `tr` request for a video/PDF is a thumbnail request (the plain transform
422
- // can't render a preview); serve ImageKit's generated thumbnail instead.
423
- if (tr && isVideoMimeType(mimeType)) {
424
- signSrc = getVideoPosterSrc(signSrc);
425
- } else if (tr && isPdfMimeType(mimeType)) {
426
- signSrc = getImageKitThumbnailSrc(signSrc);
427
- }
428
- return Response.redirect(buildSignedURL(signSrc, tr), 302);
285
+ const tr = new URL(req.url ?? 'http://localhost').searchParams.get('tr');
286
+ let src = key;
287
+ if (tr && isVideoMimeType(source.mimeType)) {
288
+ src = getVideoPosterSrc(src);
289
+ } else if (tr && isPdfMimeType(source.mimeType)) {
290
+ src = getImageKitThumbnailSrc(src);
429
291
  }
430
- const imageKitURL = client.helper.buildSrc({
431
- src: getStoredImageKitURL(doc) || imageKitPath,
432
- transformation: tr ? [
433
- {
434
- raw: tr
435
- }
436
- ] : undefined,
437
- urlEndpoint
438
- });
439
- return Response.redirect(imageKitURL, 302);
292
+ // Existing v3 files may live under an earlier folder or delivery endpoint.
293
+ const storedURL = document?.imagekit?.url;
294
+ if (!privateFile && !tr && !(document && 'original' in document && document.original) && storedURL) {
295
+ return Response.redirect(storedURL, 302);
296
+ }
297
+ return Response.redirect(buildURL(src, privateFile, tr), 302);
298
+ },
299
+ supportsTempFiles: true,
300
+ uploadInstructions: {
301
+ adminHandler: {
302
+ path: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler'
303
+ },
304
+ enabled: options.enabled !== false && Boolean(options.clientUploads),
305
+ endpoint: {
306
+ handler: completeUpload,
307
+ path: '/imagekit-upload-complete'
308
+ },
309
+ generate: createImageKitUploadInstructions({
310
+ access: typeof options.clientUploads === 'object' ? options.clientUploads.access : undefined,
311
+ isPrivate,
312
+ prefix,
313
+ privateKey: options.privateKey,
314
+ publicKey: options.publicKey
315
+ }),
316
+ requiresUploadReceipt: true,
317
+ useInAdmin: true
440
318
  }
441
319
  };
442
320
  };
443
- };
444
- const toCloudStorageCollections = (collections, adapter, privateSlugs, rootFolder, disablePayloadAccessControl)=>{
445
- const mappedCollections = {};
446
- for (const collectionSlug of Object.keys(collections)){
447
- mappedCollections[collectionSlug] = {
448
- adapter,
449
- disableLocalStorage: true,
450
- // Private files require Payload access control so URLs can be signed at serve time.
451
- disablePayloadAccessControl: privateSlugs.has(collectionSlug) ? undefined : disablePayloadAccessControl,
452
- prefix: getCollectionFolder(rootFolder, collectionSlug)
453
- };
454
- }
455
- return mappedCollections;
456
- };
457
- export const imagekitStorage = (pluginOptions)=>(config)=>{
458
- const { alwaysInsertFields, clientUploads, collections, disablePayloadAccessControl, enabled, focalPoint, folder, privateKey, publicKey, signedUrlExpiresIn: signedUrlExpiresInOption, urlEndpoint } = pluginOptions;
459
- if (!privateKey?.trim()) {
460
- throw new Error('imagekitStorage: `privateKey` is required.');
461
- }
462
- if (!publicKey?.trim()) {
463
- throw new Error('imagekitStorage: `publicKey` is required.');
464
- }
465
- if (!urlEndpoint?.trim()) {
466
- throw new Error('imagekitStorage: `urlEndpoint` is required.');
467
- }
468
- if (!collections || Object.keys(collections).length === 0) {
469
- throw new Error('imagekitStorage: `collections` must include at least one upload collection.');
470
- }
471
- const privateSlugs = new Set(Object.entries(collections).filter(([, value])=>isPrivateCollectionConfig(value)).map(([slug])=>slug));
472
- const configuredSlugs = new Set(Object.keys(collections));
473
- if (disablePayloadAccessControl && privateSlugs.size > 0) {
474
- throw new Error('imagekitStorage: `disablePayloadAccessControl` cannot be used with private collections. Private files require Payload access control to remain enabled so URLs can be signed at serve time.');
475
- }
476
- if (disablePayloadAccessControl) {
477
- const xmlCollectionSlugs = (config.collections || []).filter((collection)=>configuredSlugs.has(collection.slug) && collectionCanStoreXml(collection)).map((collection)=>collection.slug);
478
- if (xmlCollectionSlugs.length > 0) {
479
- throw new Error(`imagekitStorage: \`disablePayloadAccessControl\` cannot be used with collections that can store XML-family files (${xmlCollectionSlugs.join(', ')}). XML-family files require Payload's file route so its Content Security Policy is applied. Configure a MIME allow list that excludes SVG/XML, or keep Payload access control enabled.`);
480
- }
481
- }
482
- const signedUrlExpiresIn = typeof signedUrlExpiresInOption === 'number' && signedUrlExpiresInOption > 0 ? signedUrlExpiresInOption : DEFAULT_SIGNED_URL_EXPIRES_IN;
483
- const apiRoute = config.routes?.api ?? '/api';
484
- const rootFolder = normalizeFolder(folder);
485
- const imageKitClient = new ImageKit({
486
- privateKey
487
- });
488
- const collectionSecurity = Object.fromEntries((config.collections || []).filter((collection)=>configuredSlugs.has(collection.slug)).map((collection)=>[
489
- collection.slug,
321
+ const output = cloudStoragePlugin({
322
+ collections: Object.fromEntries(Object.keys(collections).map((slug)=>[
323
+ slug,
490
324
  {
491
- collection,
492
- isPrivate: privateSlugs.has(collection.slug),
493
- prefix: getCollectionFolder(rootFolder, collection.slug)
325
+ adapter,
326
+ disableLocalStorage: true,
327
+ disablePayloadAccessControl: options.disablePayloadAccessControl,
328
+ prefix: prefixFor(slug)
494
329
  }
495
- ]));
496
- const verifyClientUpload = createImageKitClientUploadVerifier({
497
- client: imageKitClient,
498
- collections: collectionSecurity
330
+ ])),
331
+ enabled: options.enabled
332
+ })(sourceConfig);
333
+ // Do not invent a prefix on reads of legacy documents. A stored prefix is retained;
334
+ // new uploads get their normalized destination from the file-operation pipeline.
335
+ output.collections = output.collections?.map((collection)=>!configured.has(collection.slug) ? collection : {
336
+ ...collection,
337
+ fields: collection.fields.map((field)=>field.type === 'text' && field.name === 'prefix' ? {
338
+ ...field,
339
+ defaultValue: ({ req })=>req.file && typeof field.defaultValue === 'string' ? field.defaultValue : ''
340
+ } : field)
499
341
  });
500
- const adapter = imagekitAdapter({
501
- apiRoute,
502
- client: imageKitClient,
503
- clientUploads,
504
- privateSlugs,
505
- rootFolder,
506
- signedUrlExpiresIn,
507
- urlEndpoint,
508
- verifyClientUpload
342
+ return output;
343
+ }
344
+ async function findUploadedFile(files, key, req) {
345
+ // ImageKit asset listings can lag a successful upload. Only admission retries;
346
+ // ordinary missing reads/deletes and collision lookups must stay immediate.
347
+ let file = await files.find({
348
+ key,
349
+ req
350
+ });
351
+ for (const delay of [
352
+ 250,
353
+ 500,
354
+ 1000,
355
+ 2000
356
+ ]){
357
+ if (file) {
358
+ break;
359
+ }
360
+ await new Promise((resolve)=>setTimeout(resolve, delay));
361
+ file = await files.find({
362
+ key,
363
+ req
509
364
  });
510
- const configWithCropDisabled = {
511
- ...config,
512
- collections: (config.collections || []).map((collection)=>{
513
- if (!configuredSlugs.has(collection.slug)) {
514
- return collection;
515
- }
516
- const isCollectionPrivate = privateSlugs.has(collection.slug);
517
- const buildThumbnailURL = (doc)=>isDocumentPrivate(doc, isCollectionPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc);
518
- const collectionOptions = collections[collection.slug];
519
- const collectionFocalPoint = collectionOptions !== true && typeof collectionOptions === 'object' && typeof collectionOptions.focalPoint === 'boolean' ? collectionOptions.focalPoint : undefined;
520
- // Surface a "this is a private collection" note in the list and edit views.
521
- // Preserve an existing string description by appending; leave custom
522
- // component descriptions untouched.
523
- const existingDescription = collection.admin?.description;
524
- const baseAdmin = isCollectionPrivate ? {
525
- ...collection.admin || {},
526
- description: typeof existingDescription === 'string' && existingDescription.trim().length > 0 ? `${existingDescription} ${PRIVATE_COLLECTION_DESCRIPTION}` : existingDescription ?? PRIVATE_COLLECTION_DESCRIPTION
527
- } : collection.admin;
528
- return {
529
- ...collection,
530
- admin: baseAdmin,
531
- hooks: {
532
- ...collection.hooks || {},
533
- beforeChange: [
534
- ...collection.hooks?.beforeChange || [],
535
- async ({ data, req })=>{
536
- if (!isRecord(data) || !req.file?.clientUploadContext) {
537
- return data;
538
- }
539
- const context = req.file.clientUploadContext;
540
- const requestMimeType = req.file.mimetype.split(';', 1)[0]?.trim().toLowerCase();
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
- // generateFileData preserves the original size on the document.
543
- context.expectedSize !== data.filesize) {
544
- throw new APIError('ImageKit client upload reference does not match this request.', 400);
545
- }
546
- const uploaded = await verifyClientUpload({
547
- collectionSlug: collection.slug,
548
- context
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
- }
555
- // A provider file can only be claimed once, including through legacy receipts.
556
- // The unique file ID index also rejects concurrent claims.
557
- const existing = await req.payload.db.findOne({
558
- collection: collection.slug,
559
- req,
560
- where: {
561
- 'imagekit.fileId': {
562
- equals: uploaded.fileId
365
+ }
366
+ return file;
367
+ }
368
+ function isPrivateDocument(doc, fallback) {
369
+ const value = doc?.imagekit?.isPrivate;
370
+ return typeof value === 'boolean' ? value : fallback;
371
+ }
372
+ function getFileLocation(doc, filename) {
373
+ const value = doc ?? {};
374
+ return [
375
+ value,
376
+ value.original,
377
+ ...Object.values(value.variants ?? {})
378
+ ].find((file)=>file?.filename === filename) ?? value;
379
+ }
380
+ function applyMetadata(data, file, isPrivate, slug, apiRoute) {
381
+ data.imagekit = {
382
+ fileId: file.fileId,
383
+ isPrivate,
384
+ thumbnailUrl: file.thumbnail,
385
+ url: file.url
386
+ };
387
+ data.thumbnailURL = isPrivate ? getPrivateAdminThumbnailURL(data, slug, apiRoute) : getAdminThumbnailURL(data);
388
+ return data;
389
+ }
390
+ async function fetchOriginal({ file, req, url }) {
391
+ const range = getRangeRequestInfo({
392
+ fileSize: file.size,
393
+ rangeHeader: req.headers.get('Range')
394
+ });
395
+ if (range.type === 'invalid') {
396
+ return new Response(null, {
397
+ headers: range.headers,
398
+ status: range.status
399
+ });
400
+ }
401
+ const response = await fetch(url, {
402
+ cache: 'no-store',
403
+ headers: range.type === 'partial' ? {
404
+ Range: `bytes=${range.rangeStart}-${range.rangeEnd}`
405
+ } : undefined,
406
+ signal: new AbortController().signal
407
+ });
408
+ if (response.status !== range.status || response.headers.get('Content-Length') !== range.headers['Content-Length'] || range.type === 'partial' && response.headers.get('Content-Range') !== range.headers['Content-Range']) {
409
+ void response.body?.cancel().catch(()=>undefined);
410
+ throw new APIError('ImageKit returned mismatched file content metadata.', 502);
411
+ }
412
+ const headers = new Headers(range.headers);
413
+ headers.set('Content-Type', file.mime ?? 'application/octet-stream');
414
+ if (isXmlMimeType(file.mime ?? '')) {
415
+ headers.set('Content-Security-Policy', uploadContentSecurityPolicy);
416
+ }
417
+ return new Response(response.body, {
418
+ headers,
419
+ status: range.status
420
+ });
421
+ }
422
+ function canStoreXml(collection) {
423
+ const upload = typeof collection.upload === 'object' ? collection.upload : undefined;
424
+ return Boolean(upload?.allowRestrictedFileTypes) || !upload?.mimeTypes?.length || upload.mimeTypes.some((mime)=>mime.includes('*') || isXmlMimeType(mime));
425
+ }
426
+ function storageFields({ buildURL, expiresIn, isPrivate, prefix }) {
427
+ return [
428
+ {
429
+ type: 'collapsible',
430
+ admin: {
431
+ condition: (data)=>Boolean(data?.imagekit?.url),
432
+ initCollapsed: true
433
+ },
434
+ fields: [
435
+ {
436
+ name: 'imagekit',
437
+ type: 'group',
438
+ admin: {
439
+ disabled: {
440
+ column: true,
441
+ filter: true
442
+ }
443
+ },
444
+ fields: [
445
+ {
446
+ name: 'fileId',
447
+ type: 'text',
448
+ admin: {
449
+ hidden: true
450
+ },
451
+ unique: true
452
+ },
453
+ {
454
+ name: 'url',
455
+ type: 'text',
456
+ admin: {
457
+ readOnly: true
458
+ },
459
+ label: 'URL'
460
+ },
461
+ {
462
+ name: 'isPrivate',
463
+ type: 'checkbox',
464
+ admin: {
465
+ readOnly: true
466
+ },
467
+ label: 'Private file'
468
+ },
469
+ {
470
+ name: 'signedUrl',
471
+ type: 'text',
472
+ admin: {
473
+ condition: (_data, siblingData)=>isPrivateDocument({
474
+ imagekit: siblingData
475
+ }, isPrivate),
476
+ description: `Temporary signed URL. Expires ${expiresIn} seconds after load. Anyone with this URL can access the file until it expires.`,
477
+ readOnly: true
478
+ },
479
+ hooks: {
480
+ afterRead: [
481
+ ({ data, siblingData })=>{
482
+ if (!data?.filename || !isPrivateDocument({
483
+ imagekit: siblingData
484
+ }, isPrivate)) {
485
+ return undefined;
563
486
  }
487
+ const key = buildStoragePathData({
488
+ collectionPrefix: prefix,
489
+ docPrefix: path.posix.join(data.prefix || prefix, data._objectKey || ''),
490
+ filename: data.filename
491
+ }).storageFilePath;
492
+ return buildURL(key, true);
564
493
  }
565
- });
566
- if (existing) {
567
- throw new APIError('This ImageKit client upload receipt has already been used.', 400);
568
- }
569
- data.filename = uploaded.name;
570
- data.filesize = uploaded.size;
571
- data.mimeType = uploaded.mimeType;
572
- return applyVerifiedClientUploadToData(data, uploaded, buildThumbnailURL, isCollectionPrivate);
573
- }
574
- ],
575
- beforeOperation: [
576
- ...collection.hooks?.beforeOperation || [],
577
- ({ args, operation, req })=>{
578
- if (operation !== 'create' && operation !== 'update' || req.context?.skipCloudStorage || !req.file || req.file.clientUploadContext || !('data' in args) || !isRecord(args.data) || typeof args.data.prefix !== 'string') {
579
- return args;
580
- }
581
- // Payload checks filename uniqueness before cloud storage's beforeChange
582
- // normalizes the prefix. Use the persisted form during that lookup too.
583
- args.data.prefix = buildUploadStoragePathData({
584
- collectionPrefix: getCollectionFolder(rootFolder, collection.slug),
585
- docPrefix: args.data.prefix,
586
- filename: req.file.name
587
- }).sanitizedDocPrefix;
588
- return args;
494
+ ]
495
+ },
496
+ label: 'Signed URL',
497
+ virtual: true
498
+ },
499
+ {
500
+ name: 'thumbnailUrl',
501
+ type: 'text',
502
+ admin: {
503
+ hidden: true
589
504
  }
590
- ]
591
- },
592
- upload: {
593
- // Focal points are opt-in for configured collections. Plugin-level
594
- // and collection-level options can override this default; a
595
- // collection's own `upload.focalPoint` still wins because it is
596
- // spread in afterwards.
597
- focalPoint: false,
598
- ...typeof focalPoint === 'boolean' ? {
599
- focalPoint
600
- } : {},
601
- ...typeof collectionFocalPoint === 'boolean' ? {
602
- focalPoint: collectionFocalPoint
603
- } : {},
604
- ...typeof collection.upload === 'object' ? collection.upload : {},
605
- adminThumbnail: ({ doc })=>buildThumbnailURL(doc),
606
- crop: false
607
- }
608
- };
609
- })
610
- };
611
- const uploadCollections = Object.fromEntries(Object.keys(collections).map((collectionSlug)=>[
612
- collectionSlug,
613
- {
614
- isPrivate: privateSlugs.has(collectionSlug),
615
- prefix: getCollectionFolder(rootFolder, collectionSlug)
616
- }
617
- ]));
618
- initClientUploads({
619
- clientHandler: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler',
620
- collections: uploadCollections,
621
- config: configWithCropDisabled,
622
- enabled: enabled !== false && Boolean(clientUploads),
623
- serverHandler: createImageKitUploadAuthHandler({
624
- access: typeof clientUploads === 'object' ? clientUploads.access : undefined,
625
- collections: uploadCollections,
626
- privateKey,
627
- publicKey,
628
- verifyClientUpload
629
- }),
630
- serverHandlerPath: imageKitUploadAuthPath
631
- });
632
- const output = cloudStoragePlugin({
633
- alwaysInsertFields,
634
- collections: toCloudStorageCollections(collections, adapter, privateSlugs, rootFolder, disablePayloadAccessControl),
635
- enabled
636
- })(configWithCropDisabled);
637
- // Make the provider identity unique at persistence time to reject concurrent receipt replays.
638
- // Keep the legacy object key index for existing uploads.
639
- output.collections = (output.collections || []).map((collection)=>{
640
- if (!configuredSlugs.has(collection.slug)) {
641
- return collection;
642
- }
643
- const originalPrefixField = (config.collections || []).find((original)=>original.slug === collection.slug)?.fields.find((field)=>'name' in field && field.name === 'prefix');
644
- const withUniqueIdentity = (field)=>{
645
- if (field.type === 'text' && 'name' in field && field.name === 'prefix' && (!originalPrefixField || !('defaultValue' in originalPrefixField) || originalPrefixField.defaultValue == null)) {
646
- // Cloud storage's collection-prefix default is applied on reads of old
647
- // documents that have no stored prefix. Its URL hook then appends that
648
- // prefix, but Payload's file access check cannot find it in the DB.
649
- // Upload hooks still persist the collection prefix for new files.
650
- return {
651
- ...field,
652
- defaultValue: ({ req })=>req.file && typeof field.defaultValue === 'string' ? field.defaultValue : ''
653
- };
654
- }
655
- if ('name' in field && field.name === '_objectKey') {
656
- return {
657
- ...field,
658
- unique: true
659
- };
660
- }
661
- if (field.type === 'group' && 'name' in field && field.name === 'imagekit') {
662
- return {
663
- ...field,
664
- fields: field.fields.map((metadataField)=>'name' in metadataField && metadataField.name === 'fileId' ? {
665
- ...metadataField,
666
- unique: true
667
- } : metadataField)
668
- };
669
- }
670
- // The imagekit group sits inside the adapter's presentational "Storage: ImageKit" panel.
671
- if (field.type === 'collapsible') {
672
- return {
673
- ...field,
674
- fields: field.fields.map(withUniqueIdentity)
675
- };
505
+ }
506
+ ],
507
+ interfaceName: 'imagekitFields',
508
+ label: false
676
509
  }
677
- return field;
678
- };
679
- return {
680
- ...collection,
681
- fields: collection.fields.map(withUniqueIdentity)
682
- };
683
- });
684
- return output;
685
- };
686
- export { migrateFolder } from './migrate.js';
687
- // Backward-compatible alias
688
- export const payloadStorageImagekit = imagekitStorage;
689
- export const payloadStorageImageKit = imagekitStorage;
510
+ ],
511
+ label: 'Storage: ImageKit'
512
+ }
513
+ ];
514
+ }
690
515
 
691
516
  //# sourceMappingURL=index.js.map