@humaan/payload-storage-imagekit 2.9.1 → 3.0.0-canary.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/dist/index.js CHANGED
@@ -1,691 +1,509 @@
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 { createImageKitUploadInstructions } from './endpoints/createImageKitUploadInstructions.js';
12
9
  import { getAdminThumbnailURL, getPrivateAdminThumbnailURL, isPdfMimeType, isVideoMimeType } from './getAdminThumbnailURL.js';
13
10
  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;
11
+ /** Payload 4 storage capability. Register under config.storage, not config.plugins. */ export const imagekitStorage = (options)=>({
12
+ name: 'imagekit',
13
+ collections: Object.keys(options.collections),
14
+ init: (config)=>initializeStorage(config, options)
15
+ });
16
+ export { migrateFolder } from './migrate.js';
17
+ export const payloadStorageImagekit = imagekitStorage;
18
+ export const payloadStorageImageKit = imagekitStorage;
19
+ function initializeStorage(config, options) {
20
+ for (const key of [
21
+ 'privateKey',
22
+ 'publicKey',
23
+ 'urlEndpoint',
24
+ 'folder'
25
+ ]){
26
+ if (!options[key]?.trim()) {
27
+ throw new Error(`imagekitStorage: \`${key}\` is required.`);
76
28
  }
77
29
  }
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
- });
30
+ const folder = options.folder.trim().replace(/^\/+|\/+$/g, '');
31
+ if (folder.split('/').some((segment)=>!segment || segment === '.' || segment === '..')) {
32
+ throw new Error('imagekitStorage: `folder` must not contain empty or dot segments.');
100
33
  }
101
- return req.payload.db.findOne({
102
- collection,
103
- req,
104
- where: {
105
- and: constraints
106
- }
34
+ const client = new ImageKit({
35
+ privateKey: options.privateKey
107
36
  });
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
37
+ const collections = options.collections;
38
+ if (!Object.keys(collections).length) {
39
+ throw new Error('imagekitStorage: `collections` must not be empty.');
40
+ }
41
+ const configured = new Set(Object.keys(collections));
42
+ const apiRoute = config.routes?.api ?? '/api';
43
+ const expiresIn = options.signedUrlExpiresIn ?? 900;
44
+ const prefixFor = (slug)=>path.posix.join(folder, slug);
45
+ const collectionIsPrivate = (slug)=>{
46
+ const value = collections[slug];
47
+ return value !== true && value?.private === true;
115
48
  };
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
49
+ const sourceConfig = {
50
+ ...config,
51
+ collections: (config.collections ?? []).map((collection)=>{
52
+ if (!configured.has(collection.slug)) {
53
+ return collection;
54
+ }
55
+ const isPrivate = collectionIsPrivate(collection.slug);
56
+ if (options.disablePayloadAccessControl && (isPrivate || canStoreXml(collection))) {
57
+ throw new Error('imagekitStorage: private and XML-capable collections require Payload access control.');
58
+ }
59
+ const setting = collections[collection.slug];
60
+ const focalPoint = setting !== true ? setting?.focalPoint : undefined;
61
+ const description = collection.admin?.description;
62
+ return {
63
+ ...collection,
64
+ admin: isPrivate ? {
65
+ ...collection.admin,
66
+ 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.'
67
+ } : collection.admin,
68
+ hooks: {
69
+ ...collection.hooks,
70
+ beforeChange: [
71
+ ...collection.hooks?.beforeChange ?? [],
72
+ async ({ data, operation, originalDoc, req })=>{
73
+ if (operation === 'validate') {
74
+ return data;
75
+ }
76
+ const verified = req.context.imagekitVerifiedUpload;
77
+ if (!verified) {
78
+ return data;
79
+ }
80
+ const existing = await req.payload.db.findOne({
81
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Generated consumer configs narrow CollectionSlug, unlike the standalone package build.
82
+ collection: collection.slug,
83
+ req,
84
+ where: {
85
+ 'imagekit.fileId': {
86
+ equals: verified.fileId
87
+ }
88
+ }
89
+ });
90
+ if (existing && existing.id !== originalDoc?.id) {
91
+ throw new APIError('This ImageKit upload reference has already been used.', 400);
92
+ }
93
+ return applyMetadata(data, verified, isPrivate, collection.slug, apiRoute);
94
+ }
95
+ ]
96
+ },
97
+ upload: {
98
+ focalPoint: focalPoint ?? options.focalPoint ?? false,
99
+ ...typeof collection.upload === 'object' ? collection.upload : {},
100
+ adminThumbnail: ({ doc })=>isPrivateDocument(doc, isPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc),
101
+ crop: false
142
102
  }
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
- });
103
+ };
104
+ })
167
105
  };
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 ? [
106
+ const adapter = ({ collection, prefix = prefixFor(collection.slug) })=>{
107
+ const isPrivate = collectionIsPrivate(collection.slug);
108
+ const buildURL = (key, signed, transformation)=>client.helper.buildSrc({
109
+ expiresIn: signed ? expiresIn : undefined,
110
+ signed,
111
+ src: `/${key.replace(/^\/+/, '')}`,
112
+ transformation: transformation ? [
178
113
  {
179
- raw: tr
114
+ raw: transformation
180
115
  }
181
116
  ] : undefined,
182
- urlEndpoint
117
+ urlEndpoint: options.urlEndpoint
183
118
  });
184
- const buildThumbnailURL = (doc)=>isDocumentPrivate(doc, isPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc);
119
+ const deleteFile = async ({ storageFilePath })=>{
120
+ const file = await findFile(client, storageFilePath);
121
+ if (file) {
122
+ await client.files.delete(file.fileId);
123
+ }
124
+ };
185
125
  return {
186
126
  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'
127
+ copyFile: async ({ from, to })=>{
128
+ if (await findFile(client, to)) {
129
+ throw new Error(`ImageKit destination already exists: ${to}`);
284
130
  }
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;
131
+ const source = await findFile(client, from);
132
+ if (!source) {
133
+ throw new Error(`ImageKit source not found: ${from}`);
299
134
  }
300
- await client.files.delete(fileId);
135
+ // Upload by URL copies the exact original without buffering it in the app process.
136
+ // overwriteFile:false protects a destination created concurrently with the lookup.
137
+ await client.files.upload({
138
+ customMetadata: source.customMetadata ?? undefined,
139
+ file: buildURL(from, Boolean(source.isPrivateFile), 'orig-true'),
140
+ fileName: path.posix.basename(to),
141
+ folder: `/${path.posix.dirname(to)}`,
142
+ isPrivateFile: source.isPrivateFile ?? false,
143
+ overwriteFile: false,
144
+ tags: source.tags ?? undefined,
145
+ useUniqueFileName: false
146
+ });
301
147
  },
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.
148
+ deleteFile,
149
+ fields: storageFields({
150
+ buildURL,
151
+ expiresIn,
152
+ isPrivate,
153
+ prefix
154
+ }),
155
+ generateURL: ({ filename, prefix: docPrefix })=>buildURL(buildStoragePathData({
156
+ collectionPrefix: prefix,
157
+ docPrefix,
158
+ filename
159
+ }).storageFilePath, false),
160
+ handleDelete: deleteFile,
161
+ handleUpload: async ({ data, file, storageFilePath })=>{
312
162
  const uploadSource = file.tempFilePath ? await openAsBlob(file.tempFilePath, {
313
163
  type: file.mimeType
314
164
  }) : 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
165
  const uploaded = await client.files.upload({
321
- file: uploadFile,
322
- fileName: uploadFilename,
323
- folder: uploadFolder,
324
- useUniqueFileName: false,
325
- ...isPrivate ? {
326
- isPrivateFile: true
327
- } : {}
166
+ file: await toFile(uploadSource, file.filename, {
167
+ type: file.mimeType
168
+ }),
169
+ fileName: path.posix.basename(storageFilePath),
170
+ folder: `/${path.posix.dirname(storageFilePath)}`,
171
+ isPrivateFile: isPrivate,
172
+ overwriteFile: false,
173
+ useUniqueFileName: false
328
174
  });
329
- data.imagekit = {
175
+ if (!uploaded.fileId || !uploaded.url) {
176
+ throw new Error('ImageKit returned incomplete upload metadata.');
177
+ }
178
+ return applyMetadata(data, {
179
+ name: path.posix.basename(storageFilePath),
330
180
  fileId: uploaded.fileId,
331
- isPrivate,
332
- thumbnailUrl: uploaded.thumbnailUrl,
181
+ isPrivateFile: isPrivate,
182
+ mime: file.mimeType,
183
+ size: file.filesize,
184
+ thumbnail: uploaded.thumbnailUrl,
333
185
  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;
186
+ }, isPrivate, collection.slug, apiRoute);
343
187
  },
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);
188
+ staticHandler: async (req, { doc, params })=>{
189
+ if (params.uploadReference) {
190
+ try {
191
+ const reference = params.uploadReference;
192
+ const receipt = verifyClientUploadReceipt({
193
+ collectionSlug: collection.slug,
194
+ req,
195
+ signedReceipt: reference.signedReceipt
196
+ });
197
+ const expectedPath = buildStoragePathData({
198
+ collectionPrefix: prefix,
199
+ docPrefix: path.posix.join(receipt.filePrefix, receipt._objectKey ?? ''),
200
+ filename: params.filename
201
+ }).storageFilePath;
202
+ if (!receipt._objectKey || expectedPath !== receipt.storageFilePath || !expectedPath.startsWith(`${prefix.replace(/^\/+/, '')}/`)) {
203
+ throw new APIError('Invalid ImageKit upload reference.', 400);
204
+ }
205
+ const uploaded = await findUploadedFile(client, expectedPath);
206
+ if (!uploaded || !uploaded.mime || Boolean(uploaded.isPrivateFile) !== isPrivate) {
207
+ throw new APIError('ImageKit upload did not match its authorization.', 400);
208
+ }
209
+ assertClientUploadAllowed({
210
+ collection,
211
+ filename: uploaded.name,
212
+ mimeType: uploaded.mime
213
+ });
214
+ req.context.imagekitVerifiedUpload = uploaded;
215
+ return await fetchOriginal({
216
+ file: uploaded,
217
+ req,
218
+ url: buildURL(expectedPath, isPrivate, 'orig-true')
219
+ });
220
+ } catch (error) {
221
+ if (error instanceof APIError) {
222
+ return Response.json({
223
+ message: error.message
224
+ }, {
225
+ status: error.status
226
+ });
227
+ }
228
+ throw error;
353
229
  }
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
230
  }
378
- const doc = args.doc || await findUploadDocumentByFilename({
379
- collection: args.params.collection,
380
- filename: args.params.filename,
381
- prefix: args.params.prefix,
382
- req
231
+ const document = doc ?? await req.payload.db.findOne({
232
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Generated consumer configs narrow CollectionSlug, unlike the standalone package build.
233
+ collection: collection.slug,
234
+ req,
235
+ where: {
236
+ filename: {
237
+ equals: params.filename
238
+ }
239
+ }
383
240
  });
384
- const storageFilePath = await getStorageFilePath({
385
- collection,
241
+ const source = getFileLocation(document, params.filename);
242
+ const key = buildStoragePathData({
386
243
  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);
244
+ docPrefix: path.posix.join(params.prefix ?? source.prefix ?? prefix, source._objectKey ?? ''),
245
+ filename: params.filename
246
+ }).storageFilePath;
247
+ const privateFile = isPrivateDocument(document, isPrivate);
248
+ if (params.operation === 'transform' || isXmlMimeType(source.mimeType ?? '')) {
249
+ const file = await findFile(client, key);
250
+ if (!file?.mime) {
251
+ throw new APIError('ImageKit source not found.', 404);
400
252
  }
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
409
- });
410
- return fetchUploadedFile({
411
- fileSize: filesize,
412
- fileURL,
413
- mimeType,
414
- range: req.headers.get('Range')
253
+ return fetchOriginal({
254
+ file,
255
+ req,
256
+ url: buildURL(key, privateFile, 'orig-true')
415
257
  });
416
258
  }
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);
259
+ const tr = new URL(req.url ?? 'http://localhost').searchParams.get('tr');
260
+ let src = key;
261
+ if (tr && isVideoMimeType(source.mimeType)) {
262
+ src = getVideoPosterSrc(src);
263
+ } else if (tr && isPdfMimeType(source.mimeType)) {
264
+ src = getImageKitThumbnailSrc(src);
429
265
  }
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);
266
+ // Existing v3 files may live under an earlier folder or delivery endpoint.
267
+ const storedURL = document?.imagekit?.url;
268
+ if (!privateFile && !tr && !(document && 'original' in document && document.original) && storedURL) {
269
+ return Response.redirect(storedURL, 302);
270
+ }
271
+ return Response.redirect(buildURL(src, privateFile, tr), 302);
272
+ },
273
+ supportsTempFiles: true,
274
+ uploadInstructions: {
275
+ adminHandler: {
276
+ path: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler'
277
+ },
278
+ enabled: options.enabled !== false && Boolean(options.clientUploads),
279
+ generate: createImageKitUploadInstructions({
280
+ access: typeof options.clientUploads === 'object' ? options.clientUploads.access : undefined,
281
+ isPrivate,
282
+ prefix,
283
+ privateKey: options.privateKey,
284
+ publicKey: options.publicKey
285
+ }),
286
+ requiresUploadReceipt: true,
287
+ useInAdmin: true
440
288
  }
441
289
  };
442
290
  };
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.');
291
+ const output = cloudStoragePlugin({
292
+ collections: Object.fromEntries(Object.keys(collections).map((slug)=>[
293
+ slug,
294
+ {
295
+ adapter,
296
+ disableLocalStorage: true,
297
+ disablePayloadAccessControl: options.disablePayloadAccessControl,
298
+ prefix: prefixFor(slug)
299
+ }
300
+ ])),
301
+ enabled: options.enabled
302
+ })(sourceConfig);
303
+ // Do not invent a prefix on reads of legacy documents. A stored prefix is retained;
304
+ // new uploads get their normalized destination from the file-operation pipeline.
305
+ output.collections = output.collections?.map((collection)=>!configured.has(collection.slug) ? collection : {
306
+ ...collection,
307
+ fields: collection.fields.map((field)=>field.type === 'text' && field.name === 'prefix' ? {
308
+ ...field,
309
+ defaultValue: ({ req })=>req.file && typeof field.defaultValue === 'string' ? field.defaultValue : ''
310
+ } : field)
311
+ });
312
+ return output;
313
+ }
314
+ async function findFile(client, storageFilePath) {
315
+ const filePath = `/${storageFilePath.replace(/^\/+/, '')}`;
316
+ const name = path.posix.basename(filePath);
317
+ for(let skip = 0;; skip += 100){
318
+ const assets = await client.assets.list({
319
+ type: 'file',
320
+ limit: 100,
321
+ path: `${path.posix.dirname(filePath)}/`,
322
+ skip
323
+ });
324
+ const found = assets.find((asset)=>'fileId' in asset && asset.filePath === filePath && asset.name === name);
325
+ if (found && 'fileId' in found && found.fileId) {
326
+ const file = await client.files.get(found.fileId);
327
+ if (file.fileId !== found.fileId || file.name !== name || file.filePath !== filePath || typeof file.size !== 'number' || !Number.isSafeInteger(file.size) || file.size < 0 || !file.url) {
328
+ throw new Error('ImageKit returned incomplete file metadata.');
329
+ }
330
+ return {
331
+ ...file,
332
+ name: file.name,
333
+ fileId: file.fileId,
334
+ size: file.size,
335
+ url: file.url
336
+ };
470
337
  }
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.');
338
+ if (assets.length < 100) {
339
+ return undefined;
475
340
  }
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
- }
341
+ }
342
+ }
343
+ async function findUploadedFile(client, key) {
344
+ // ImageKit asset listings can lag a successful upload. Only admission retries;
345
+ // ordinary missing reads/deletes and collision lookups must stay immediate.
346
+ let file = await findFile(client, key);
347
+ for (const delay of [
348
+ 250,
349
+ 500,
350
+ 1000,
351
+ 2000
352
+ ]){
353
+ if (file) {
354
+ break;
481
355
  }
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
356
+ await new Promise((resolve)=>setTimeout(resolve, delay));
357
+ file = await findFile(client, key);
358
+ }
359
+ return file;
360
+ }
361
+ function isPrivateDocument(doc, fallback) {
362
+ const value = doc?.imagekit?.isPrivate;
363
+ return typeof value === 'boolean' ? value : fallback;
364
+ }
365
+ function getFileLocation(doc, filename) {
366
+ const value = doc ?? {};
367
+ return [
368
+ value,
369
+ value.original,
370
+ ...Object.values(value.variants ?? {})
371
+ ].find((file)=>file?.filename === filename) ?? value;
372
+ }
373
+ function applyMetadata(data, file, isPrivate, slug, apiRoute) {
374
+ data.imagekit = {
375
+ fileId: file.fileId,
376
+ isPrivate,
377
+ thumbnailUrl: file.thumbnail,
378
+ url: file.url
379
+ };
380
+ data.thumbnailURL = isPrivate ? getPrivateAdminThumbnailURL(data, slug, apiRoute) : getAdminThumbnailURL(data);
381
+ return data;
382
+ }
383
+ async function fetchOriginal({ file, req, url }) {
384
+ const range = getRangeRequestInfo({
385
+ fileSize: file.size,
386
+ rangeHeader: req.headers.get('Range')
387
+ });
388
+ if (range.type === 'invalid') {
389
+ return new Response(null, {
390
+ headers: range.headers,
391
+ status: range.status
487
392
  });
488
- const collectionSecurity = Object.fromEntries((config.collections || []).filter((collection)=>configuredSlugs.has(collection.slug)).map((collection)=>[
489
- collection.slug,
393
+ }
394
+ const response = await fetch(url, {
395
+ cache: 'no-store',
396
+ headers: range.type === 'partial' ? {
397
+ Range: `bytes=${range.rangeStart}-${range.rangeEnd}`
398
+ } : undefined,
399
+ signal: new AbortController().signal
400
+ });
401
+ 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']) {
402
+ void response.body?.cancel().catch(()=>undefined);
403
+ throw new APIError('ImageKit returned mismatched file content metadata.', 502);
404
+ }
405
+ const headers = new Headers(range.headers);
406
+ headers.set('Content-Type', file.mime ?? 'application/octet-stream');
407
+ if (isXmlMimeType(file.mime ?? '')) {
408
+ headers.set('Content-Security-Policy', uploadContentSecurityPolicy);
409
+ }
410
+ return new Response(response.body, {
411
+ headers,
412
+ status: range.status
413
+ });
414
+ }
415
+ function canStoreXml(collection) {
416
+ const upload = typeof collection.upload === 'object' ? collection.upload : undefined;
417
+ return Boolean(upload?.allowRestrictedFileTypes) || !upload?.mimeTypes?.length || upload.mimeTypes.some((mime)=>mime.includes('*') || isXmlMimeType(mime));
418
+ }
419
+ function storageFields({ buildURL, expiresIn, isPrivate, prefix }) {
420
+ return [
421
+ {
422
+ type: 'collapsible',
423
+ admin: {
424
+ condition: (data)=>Boolean(data?.imagekit?.url),
425
+ initCollapsed: true
426
+ },
427
+ fields: [
490
428
  {
491
- collection,
492
- isPrivate: privateSlugs.has(collection.slug),
493
- prefix: getCollectionFolder(rootFolder, collection.slug)
494
- }
495
- ]));
496
- const verifyClientUpload = createImageKitClientUploadVerifier({
497
- client: imageKitClient,
498
- collections: collectionSecurity
499
- });
500
- const adapter = imagekitAdapter({
501
- apiRoute,
502
- client: imageKitClient,
503
- clientUploads,
504
- privateSlugs,
505
- rootFolder,
506
- signedUrlExpiresIn,
507
- urlEndpoint,
508
- verifyClientUpload
509
- });
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
429
+ name: 'imagekit',
430
+ type: 'group',
431
+ admin: {
432
+ disabled: {
433
+ column: true,
434
+ filter: true
435
+ }
436
+ },
437
+ fields: [
438
+ {
439
+ name: 'fileId',
440
+ type: 'text',
441
+ admin: {
442
+ hidden: true
443
+ },
444
+ unique: true
445
+ },
446
+ {
447
+ name: 'url',
448
+ type: 'text',
449
+ admin: {
450
+ readOnly: true
451
+ },
452
+ label: 'URL'
453
+ },
454
+ {
455
+ name: 'isPrivate',
456
+ type: 'checkbox',
457
+ admin: {
458
+ readOnly: true
459
+ },
460
+ label: 'Private file'
461
+ },
462
+ {
463
+ name: 'signedUrl',
464
+ type: 'text',
465
+ admin: {
466
+ condition: (_data, siblingData)=>isPrivateDocument({
467
+ imagekit: siblingData
468
+ }, isPrivate),
469
+ description: `Temporary signed URL. Expires ${expiresIn} seconds after load. Anyone with this URL can access the file until it expires.`,
470
+ readOnly: true
471
+ },
472
+ hooks: {
473
+ afterRead: [
474
+ ({ data, siblingData })=>{
475
+ if (!data?.filename || !isPrivateDocument({
476
+ imagekit: siblingData
477
+ }, isPrivate)) {
478
+ return undefined;
563
479
  }
480
+ const key = buildStoragePathData({
481
+ collectionPrefix: prefix,
482
+ docPrefix: path.posix.join(data.prefix || prefix, data._objectKey || ''),
483
+ filename: data.filename
484
+ }).storageFilePath;
485
+ return buildURL(key, true);
564
486
  }
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;
487
+ ]
488
+ },
489
+ label: 'Signed URL',
490
+ virtual: true
491
+ },
492
+ {
493
+ name: 'thumbnailUrl',
494
+ type: 'text',
495
+ admin: {
496
+ hidden: true
589
497
  }
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
- };
498
+ }
499
+ ],
500
+ interfaceName: 'imagekitFields',
501
+ label: false
676
502
  }
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;
503
+ ],
504
+ label: 'Storage: ImageKit'
505
+ }
506
+ ];
507
+ }
690
508
 
691
509
  //# sourceMappingURL=index.js.map