@humaan/payload-storage-imagekit 2.6.2 → 2.8.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.
@@ -1,86 +1,101 @@
1
1
  'use client';
2
- import { useConfig, useUploadHandlers } from '@payloadcms/ui';
2
+ import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client';
3
3
  import { formatAdminURL } from 'payload/shared';
4
- import { useEffect } from 'react';
5
- const uploadAPIURL = 'https://upload.imagekit.io/api/v1/files/upload';
4
+ import { getClientImageIdentity } from '../clientImageIdentity.js';
5
+ const uploadAPIURL = 'https://upload.imagekit.io/api/v2/files/upload';
6
6
  const readErrorMessage = async (response, fallback)=>{
7
7
  try {
8
8
  const result = await response.json();
9
9
  if (typeof result.message === 'string' && result.message.trim().length > 0) {
10
10
  return result.message;
11
11
  }
12
+ const messages = result.errors?.map((error)=>error.message).filter((message)=>Boolean(message));
13
+ if (messages?.length) {
14
+ return messages.join(', ');
15
+ }
12
16
  } catch {
13
17
  // Ignore non-JSON responses and fall back to the default message.
14
18
  }
15
19
  return fallback;
16
20
  };
17
- export const ImageKitUploadHandler = ({ authPath, collectionSlug, folder, isPrivate, publicKey })=>{
18
- const { config } = useConfig();
19
- const { setUploadHandler } = useUploadHandlers();
20
- useEffect(()=>{
21
+ export const ImageKitUploadHandler = createClientUploadHandler({
22
+ handler: async ({ apiRoute, collectionSlug, file, serverHandlerPath, serverURL, updateFilename })=>{
21
23
  const authURL = formatAdminURL({
22
- apiRoute: config.routes.api,
23
- path: authPath
24
+ apiRoute,
25
+ path: serverHandlerPath,
26
+ serverURL
24
27
  });
25
- setUploadHandler({
28
+ const identity = await getClientImageIdentity(file);
29
+ let authBody = {
26
30
  collectionSlug,
27
- handler: async ({ file, updateFilename })=>{
28
- const authRequestURL = new URL(authURL, window.location.origin);
29
- authRequestURL.searchParams.set('filename', file.name);
30
- const authResponse = await fetch(authRequestURL, {
31
- credentials: 'include'
32
- });
33
- if (!authResponse.ok) {
34
- throw new Error(await readErrorMessage(authResponse, 'Failed to authenticate direct ImageKit upload.'));
35
- }
36
- const auth = await authResponse.json();
37
- const safeFilename = typeof auth.fileName === 'string' && auth.fileName.length > 0 ? auth.fileName : file.name;
38
- const formData = new FormData();
39
- formData.set('expire', String(auth.expire));
40
- formData.set('file', file);
41
- formData.set('fileName', safeFilename);
42
- formData.set('folder', folder);
43
- formData.set('publicKey', publicKey);
44
- formData.set('signature', auth.signature);
45
- formData.set('token', auth.token);
46
- formData.set('useUniqueFileName', 'false');
47
- if (isPrivate) {
48
- // ImageKit's upload signature only covers `token` + `expire`, so adding
49
- // this flag does not invalidate the signed auth parameters.
50
- formData.set('isPrivateFile', 'true');
51
- }
52
- const uploadResponse = await fetch(uploadAPIURL, {
53
- body: formData,
54
- method: 'POST'
55
- });
56
- if (!uploadResponse.ok) {
57
- throw new Error(await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.'));
31
+ filename: identity.filename,
32
+ filesize: file.size,
33
+ mimeType: identity.mimeType
34
+ };
35
+ for(let attempt = 0; attempt < 10; attempt++){
36
+ const authResponse = await fetch(authURL, {
37
+ body: JSON.stringify(authBody),
38
+ credentials: 'include',
39
+ headers: {
40
+ 'Content-Type': 'application/json'
41
+ },
42
+ method: 'POST'
43
+ });
44
+ if (!authResponse.ok) {
45
+ throw new Error(await readErrorMessage(authResponse, 'Failed to authorize direct ImageKit upload.'));
46
+ }
47
+ const authorization = await authResponse.json();
48
+ if (authorization.filename !== file.name) {
49
+ updateFilename(authorization.filename);
50
+ }
51
+ const formData = new FormData();
52
+ formData.set('file', file);
53
+ formData.set('token', authorization.token);
54
+ for (const [key, value] of Object.entries(authorization.uploadPayload)){
55
+ formData.set(key, value);
56
+ }
57
+ const uploadResponse = await fetch(uploadAPIURL, {
58
+ body: formData,
59
+ method: 'POST'
60
+ });
61
+ if (!uploadResponse.ok) {
62
+ const message = await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.');
63
+ const isFilenameConflict = (uploadResponse.status === 400 || uploadResponse.status === 409) && message.includes('A file with the same name already exists at the exact location');
64
+ if (isFilenameConflict) {
65
+ authBody = {
66
+ action: 'retry',
67
+ collectionSlug,
68
+ signedReceipt: authorization.clientUploadContext.signedReceipt
69
+ };
70
+ continue;
58
71
  }
59
- const uploaded = await uploadResponse.json();
60
- const uploadedFilename = typeof uploaded.name === 'string' && uploaded.name.length > 0 ? uploaded.name : safeFilename;
61
- updateFilename(uploadedFilename);
62
- return {
63
- name: uploadedFilename,
72
+ throw new Error(message);
73
+ }
74
+ const uploaded = await uploadResponse.json();
75
+ if (typeof uploaded.fileId !== 'string' || !uploaded.fileId.trim()) {
76
+ throw new Error('ImageKit did not return an uploaded file ID.');
77
+ }
78
+ const completionResponse = await fetch(authURL, {
79
+ body: JSON.stringify({
80
+ action: 'complete',
81
+ collectionSlug,
64
82
  fileId: uploaded.fileId,
65
- filePath: uploaded.filePath,
66
- fileType: uploaded.fileType,
67
- mimeType: file.type,
68
- size: typeof uploaded.size === 'number' ? uploaded.size : file.size,
69
- thumbnailUrl: uploaded.thumbnailUrl,
70
- url: uploaded.url
71
- };
83
+ signedReceipt: authorization.clientUploadContext.signedReceipt
84
+ }),
85
+ credentials: 'include',
86
+ headers: {
87
+ 'Content-Type': 'application/json'
88
+ },
89
+ method: 'POST'
90
+ });
91
+ if (!completionResponse.ok) {
92
+ throw new Error(await readErrorMessage(completionResponse, 'Failed to verify ImageKit upload.'));
72
93
  }
73
- });
74
- }, [
75
- authPath,
76
- collectionSlug,
77
- config.routes.api,
78
- folder,
79
- isPrivate,
80
- publicKey,
81
- setUploadHandler
82
- ]);
83
- return null;
84
- };
94
+ const completion = await completionResponse.json();
95
+ return completion.clientUploadContext;
96
+ }
97
+ throw new Error('Could not find an available filename. Rename the file and try again.');
98
+ }
99
+ });
85
100
 
86
101
  //# sourceMappingURL=ImageKitUploadHandler.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useUploadHandlers } from '@payloadcms/ui'\nimport { formatAdminURL } from 'payload/shared'\nimport { useEffect } from 'react'\n\nimport type { ImageKitClientUploadContext, ImageKitUploadHandlerProps } from '../clientUploads.js'\n\ntype ImageKitAuthResponse = {\n expire: number\n fileName?: string\n signature: string\n token: string\n}\n\ntype ImageKitUploadResponse = {\n fileType?: string\n message?: string\n name?: string\n size?: number\n} & ImageKitClientUploadContext\n\nconst uploadAPIURL = 'https://upload.imagekit.io/api/v1/files/upload'\n\nconst readErrorMessage = async (response: Response, fallback: string): Promise<string> => {\n try {\n const result = (await response.json()) as { message?: string }\n\n if (typeof result.message === 'string' && result.message.trim().length > 0) {\n return result.message\n }\n } catch {\n // Ignore non-JSON responses and fall back to the default message.\n }\n\n return fallback\n}\n\nexport const ImageKitUploadHandler = ({\n authPath,\n collectionSlug,\n folder,\n isPrivate,\n publicKey,\n}: ImageKitUploadHandlerProps) => {\n const { config } = useConfig()\n const { setUploadHandler } = useUploadHandlers()\n\n useEffect(() => {\n const authURL = formatAdminURL({\n apiRoute: config.routes.api,\n path: authPath,\n })\n\n setUploadHandler({\n collectionSlug,\n handler: async ({ file, updateFilename }) => {\n const authRequestURL = new URL(authURL, window.location.origin)\n\n authRequestURL.searchParams.set('filename', file.name)\n\n const authResponse = await fetch(authRequestURL, {\n credentials: 'include',\n })\n\n if (!authResponse.ok) {\n throw new Error(\n await readErrorMessage(authResponse, 'Failed to authenticate direct ImageKit upload.'),\n )\n }\n\n const auth = (await authResponse.json()) as ImageKitAuthResponse\n const safeFilename =\n typeof auth.fileName === 'string' && auth.fileName.length > 0 ? auth.fileName : file.name\n const formData = new FormData()\n\n formData.set('expire', String(auth.expire))\n formData.set('file', file)\n formData.set('fileName', safeFilename)\n formData.set('folder', folder)\n formData.set('publicKey', publicKey)\n formData.set('signature', auth.signature)\n formData.set('token', auth.token)\n formData.set('useUniqueFileName', 'false')\n\n if (isPrivate) {\n // ImageKit's upload signature only covers `token` + `expire`, so adding\n // this flag does not invalidate the signed auth parameters.\n formData.set('isPrivateFile', 'true')\n }\n\n const uploadResponse = await fetch(uploadAPIURL, {\n body: formData,\n method: 'POST',\n })\n\n if (!uploadResponse.ok) {\n throw new Error(\n await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.'),\n )\n }\n\n const uploaded = (await uploadResponse.json()) as ImageKitUploadResponse\n const uploadedFilename =\n typeof uploaded.name === 'string' && uploaded.name.length > 0\n ? uploaded.name\n : safeFilename\n\n updateFilename(uploadedFilename)\n\n return {\n name: uploadedFilename,\n fileId: uploaded.fileId,\n filePath: uploaded.filePath,\n fileType: uploaded.fileType,\n mimeType: file.type,\n size: typeof uploaded.size === 'number' ? uploaded.size : file.size,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n },\n })\n }, [authPath, collectionSlug, config.routes.api, folder, isPrivate, publicKey, setUploadHandler])\n\n return null\n}\n"],"names":["useConfig","useUploadHandlers","formatAdminURL","useEffect","uploadAPIURL","readErrorMessage","response","fallback","result","json","message","trim","length","ImageKitUploadHandler","authPath","collectionSlug","folder","isPrivate","publicKey","config","setUploadHandler","authURL","apiRoute","routes","api","path","handler","file","updateFilename","authRequestURL","URL","window","location","origin","searchParams","set","name","authResponse","fetch","credentials","ok","Error","auth","safeFilename","fileName","formData","FormData","String","expire","signature","token","uploadResponse","body","method","uploaded","uploadedFilename","fileId","filePath","fileType","mimeType","type","size","thumbnailUrl","url"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type { ImageKitUploadAuthorization, ImageKitUploadCompletion } from '../clientUploads.js'\n\nimport { getClientImageIdentity } from '../clientImageIdentity.js'\n\nconst uploadAPIURL = 'https://upload.imagekit.io/api/v2/files/upload'\n\nconst readErrorMessage = async (response: Response, fallback: string): Promise<string> => {\n try {\n const result = (await response.json()) as {\n errors?: Array<{ message?: string }>\n message?: string\n }\n\n if (typeof result.message === 'string' && result.message.trim().length > 0) {\n return result.message\n }\n\n const messages = result.errors\n ?.map((error) => error.message)\n .filter((message): message is string => Boolean(message))\n\n if (messages?.length) {\n return messages.join(', ')\n }\n } catch {\n // Ignore non-JSON responses and fall back to the default message.\n }\n\n return fallback\n}\n\nexport const ImageKitUploadHandler = createClientUploadHandler({\n handler: async ({\n apiRoute,\n collectionSlug,\n file,\n serverHandlerPath,\n serverURL,\n updateFilename,\n }) => {\n const authURL = formatAdminURL({\n apiRoute,\n path: serverHandlerPath,\n serverURL,\n })\n const identity = await getClientImageIdentity(file)\n let authBody: Record<string, unknown> = {\n collectionSlug,\n filename: identity.filename,\n filesize: file.size,\n mimeType: identity.mimeType,\n }\n\n for (let attempt = 0; attempt < 10; attempt++) {\n const authResponse = await fetch(authURL, {\n body: JSON.stringify(authBody),\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (!authResponse.ok) {\n throw new Error(\n await readErrorMessage(authResponse, 'Failed to authorize direct ImageKit upload.'),\n )\n }\n\n const authorization = (await authResponse.json()) as ImageKitUploadAuthorization\n\n if (authorization.filename !== file.name) {\n updateFilename(authorization.filename)\n }\n\n const formData = new FormData()\n\n formData.set('file', file)\n formData.set('token', authorization.token)\n for (const [key, value] of Object.entries(authorization.uploadPayload)) {\n formData.set(key, value)\n }\n\n const uploadResponse = await fetch(uploadAPIURL, {\n body: formData,\n method: 'POST',\n })\n\n if (!uploadResponse.ok) {\n const message = await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.')\n const isFilenameConflict =\n (uploadResponse.status === 400 || uploadResponse.status === 409) &&\n message.includes('A file with the same name already exists at the exact location')\n if (isFilenameConflict) {\n authBody = {\n action: 'retry',\n collectionSlug,\n signedReceipt: authorization.clientUploadContext.signedReceipt,\n }\n continue\n }\n throw new Error(message)\n }\n\n const uploaded = (await uploadResponse.json()) as { fileId?: unknown }\n if (typeof uploaded.fileId !== 'string' || !uploaded.fileId.trim()) {\n throw new Error('ImageKit did not return an uploaded file ID.')\n }\n\n const completionResponse = await fetch(authURL, {\n body: JSON.stringify({\n action: 'complete',\n collectionSlug,\n fileId: uploaded.fileId,\n signedReceipt: authorization.clientUploadContext.signedReceipt,\n }),\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n })\n if (!completionResponse.ok) {\n throw new Error(\n await readErrorMessage(completionResponse, 'Failed to verify ImageKit upload.'),\n )\n }\n const completion = (await completionResponse.json()) as ImageKitUploadCompletion\n return completion.clientUploadContext\n }\n throw new Error('Could not find an available filename. Rename the file and try again.')\n },\n})\n"],"names":["createClientUploadHandler","formatAdminURL","getClientImageIdentity","uploadAPIURL","readErrorMessage","response","fallback","result","json","message","trim","length","messages","errors","map","error","filter","Boolean","join","ImageKitUploadHandler","handler","apiRoute","collectionSlug","file","serverHandlerPath","serverURL","updateFilename","authURL","path","identity","authBody","filename","filesize","size","mimeType","attempt","authResponse","fetch","body","JSON","stringify","credentials","headers","method","ok","Error","authorization","name","formData","FormData","set","token","key","value","Object","entries","uploadPayload","uploadResponse","isFilenameConflict","status","includes","action","signedReceipt","clientUploadContext","uploaded","fileId","completionResponse","completion"],"mappings":"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"}
@@ -1,8 +1,16 @@
1
- import type ImageKit from '@imagekit/nodejs';
1
+ import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types';
2
2
  import type { PayloadHandler, UploadCollectionSlug } from 'payload';
3
+ import type { createImageKitClientUploadVerifier } from '../clientUploadVerification.js';
4
+ type ImageKitUploadCollection = {
5
+ isPrivate: boolean;
6
+ prefix: string;
7
+ };
3
8
  type CreateImageKitUploadAuthHandlerArgs = {
4
- client: ImageKit;
5
- collections: Partial<Record<UploadCollectionSlug, unknown>>;
9
+ access?: ClientUploadsAccess;
10
+ collections: Partial<Record<UploadCollectionSlug, ImageKitUploadCollection>>;
11
+ privateKey: string;
12
+ publicKey: string;
13
+ verifyClientUpload: ReturnType<typeof createImageKitClientUploadVerifier>;
6
14
  };
7
- export declare const createImageKitUploadAuthHandler: ({ client, collections, }: CreateImageKitUploadAuthHandlerArgs) => PayloadHandler;
15
+ export declare const createImageKitUploadAuthHandler: ({ access, collections, privateKey, publicKey, verifyClientUpload, }: CreateImageKitUploadAuthHandlerArgs) => PayloadHandler;
8
16
  export {};
@@ -1,42 +1,175 @@
1
- import { getSafeFileName } from 'payload/internal';
2
- export const createImageKitUploadAuthHandler = ({ client, collections })=>{
1
+ import { buildUploadStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { posix } from 'node:path';
4
+ import { APIError, Forbidden } from 'payload';
5
+ import { assertClientUploadAccess, assertClientUploadAllowed, assertClientUploadFileSize, createClientUploadReceipt, getSafeFileName, verifyClientUploadReceipt } from 'payload/internal';
6
+ import { getSanitizedUploadFilename, validateMimeType } from 'payload/shared';
7
+ import { isImageKitReceiptContext } from '../clientUploads.js';
8
+ import { createImageKitUploadToken } from '../imageKitUploadToken.js';
9
+ const defaultAccess = ({ req })=>Boolean(req.user);
10
+ const bytesToMB = (bytes)=>(bytes / 1024 / 1024).toFixed(2);
11
+ const getMimeTypeEssence = (mimeType)=>(mimeType.split(';', 1)[0] || '').trim().toLowerCase();
12
+ const getImageKitSafeFilename = (filename)=>getSanitizedUploadFilename(filename).replace(/[^\w.-]/g, '_');
13
+ // Continue Payload's numeric suffix convention when the conflict exists only in ImageKit.
14
+ const nextFilename = (filename)=>{
15
+ const { name, ext } = posix.parse(filename);
16
+ const suffix = /^(.*)-(\d+)$/.exec(name);
17
+ return `${suffix ? `${suffix[1]}-${Number(suffix[2]) + 1}` : `${name}-1`}${ext}`;
18
+ };
19
+ export const createImageKitUploadAuthHandler = ({ access = defaultAccess, collections, privateKey, publicKey, verifyClientUpload })=>{
3
20
  return async (req)=>{
4
- const collectionSlug = req.routeParams?.collection;
5
- if (typeof collectionSlug !== 'string' || !(collectionSlug in collections)) {
6
- return Response.json({
7
- message: 'Collection not found.'
8
- }, {
9
- status: 404
10
- });
21
+ if (!req.json) {
22
+ throw new APIError('Content-Type expected to be application/json', 400);
11
23
  }
12
- if (!req.user) {
13
- return Response.json({
14
- message: 'Unauthorized.'
15
- }, {
16
- status: 401
17
- });
24
+ const body = await req.json();
25
+ const { action, collectionSlug, fileId, signedReceipt } = body;
26
+ let { filename, filesize, mimeType } = body;
27
+ if (typeof collectionSlug !== 'string' || !Object.hasOwn(collections, collectionSlug)) {
28
+ throw new APIError('Invalid upload payload.', 400);
29
+ }
30
+ await assertClientUploadAccess({
31
+ collectionSlug,
32
+ req
33
+ });
34
+ const imageKitCollection = collections[collectionSlug];
35
+ if (!imageKitCollection) {
36
+ throw new APIError(`Collection ${collectionSlug} was not found in ImageKit options.`, 400);
18
37
  }
19
- const authenticationParameters = client.helper.getAuthenticationParameters();
20
- const requestURL = new URL(req.url || 'http://localhost');
21
- const requestedFilename = requestURL.searchParams.get('filename')?.trim();
22
- const collection = req.payload.collections[collectionSlug];
23
- if (!requestedFilename || typeof collection?.config.upload !== 'object' || typeof collection.config.upload.staticDir !== 'string') {
24
- return Response.json(authenticationParameters, {
38
+ if (!await access({
39
+ collectionSlug,
40
+ req
41
+ })) {
42
+ throw new Forbidden(req.t);
43
+ }
44
+ if (action === 'complete') {
45
+ if (typeof fileId !== 'string' || !fileId.trim() || typeof signedReceipt !== 'string') {
46
+ throw new APIError('Invalid ImageKit upload completion payload.', 400);
47
+ }
48
+ const receipt = verifyClientUploadReceipt({
49
+ collectionSlug,
50
+ req,
51
+ signedReceipt
52
+ });
53
+ if (!isImageKitReceiptContext(receipt.context) || receipt.filename !== receipt.context.filename || receipt.context.fileId !== undefined && receipt.context.fileId !== fileId) {
54
+ throw new APIError('Invalid ImageKit client upload context.', 400);
55
+ }
56
+ // The browser supplies a lookup ID, not proof. Check provider state against the
57
+ // original signed authorization before including that ID in a new signed receipt.
58
+ const context = {
59
+ ...receipt.context,
60
+ fileId
61
+ };
62
+ await verifyClientUpload({
63
+ collectionSlug,
64
+ context
65
+ });
66
+ delete context.uploadTag;
67
+ const response = {
68
+ clientUploadContext: {
69
+ _objectKey: context._objectKey,
70
+ prefix: context.prefix,
71
+ signedReceipt: createClientUploadReceipt({
72
+ collectionSlug,
73
+ context,
74
+ filename: context.filename,
75
+ req
76
+ })
77
+ }
78
+ };
79
+ return Response.json(response, {
25
80
  headers: {
26
81
  'Cache-Control': 'no-store'
27
82
  }
28
83
  });
29
84
  }
30
- const safeFilename = await getSafeFileName({
85
+ if (action === 'retry') {
86
+ if (typeof signedReceipt !== 'string') {
87
+ throw new APIError('Invalid ImageKit upload retry payload.', 400);
88
+ }
89
+ const receipt = verifyClientUploadReceipt({
90
+ collectionSlug,
91
+ req,
92
+ signedReceipt
93
+ });
94
+ if (!isImageKitReceiptContext(receipt.context) || receipt.filename !== receipt.context.filename || receipt.context.fileId !== undefined) {
95
+ throw new APIError('Invalid ImageKit upload retry payload.', 400);
96
+ }
97
+ filename = nextFilename(receipt.context.filename);
98
+ filesize = receipt.context.expectedSize;
99
+ mimeType = receipt.context.expectedMimeType;
100
+ }
101
+ if (action !== undefined && action !== 'retry' || typeof filename !== 'string' || typeof mimeType !== 'string') {
102
+ throw new APIError('Invalid upload payload.', 400);
103
+ }
104
+ assertClientUploadAllowed({
105
+ collection: req.payload.collections[collectionSlug]?.config,
106
+ filename,
107
+ mimeType
108
+ });
109
+ assertClientUploadFileSize(filesize);
110
+ const requestedSize = filesize;
111
+ const expectedMimeType = getMimeTypeEssence(mimeType);
112
+ if (!expectedMimeType) {
113
+ throw new APIError('A valid MIME type is required for ImageKit client uploads.', 400);
114
+ }
115
+ const collectionConfig = req.payload.collections[collectionSlug]?.config;
116
+ const allowedMimeTypes = collectionConfig && typeof collectionConfig.upload === 'object' && Array.isArray(collectionConfig.upload.mimeTypes) ? collectionConfig.upload.mimeTypes : [];
117
+ if (allowedMimeTypes.length > 0 && !validateMimeType(expectedMimeType, allowedMimeTypes)) {
118
+ throw new APIError(`File type ${expectedMimeType} is not allowed.`, 400);
119
+ }
120
+ const filesizeLimit = req.payload.config.upload.limits?.fileSize;
121
+ if (typeof filesizeLimit === 'number' && Number.isFinite(filesizeLimit) && requestedSize > filesizeLimit) {
122
+ throw new APIError(`Exceeded file size limit. Limit: ${bytesToMB(filesizeLimit)}MB, got: ${bytesToMB(requestedSize)}MB`, 400);
123
+ }
124
+ const sanitizedFilename = await getSafeFileName({
31
125
  collectionSlug,
32
- desiredFilename: requestedFilename,
33
- req,
34
- staticPath: collection.config.upload.staticDir
126
+ desiredFilename: getImageKitSafeFilename(filename),
127
+ req
128
+ });
129
+ const resolved = buildUploadStoragePathData({
130
+ collectionPrefix: imageKitCollection.prefix,
131
+ docPrefix: imageKitCollection.prefix,
132
+ filename: sanitizedFilename
35
133
  });
36
- return Response.json({
37
- ...authenticationParameters,
38
- fileName: safeFilename
39
- }, {
134
+ const folder = `/${resolved.storageFilePath.slice(0, -(sanitizedFilename.length + 1))}`;
135
+ const uploadTag = `payload-upload-${randomUUID()}`;
136
+ const uploadPayload = {
137
+ checks: `"file.size" = ${requestedSize} AND "file.mime" = "${expectedMimeType}"`,
138
+ fileName: sanitizedFilename,
139
+ folder,
140
+ isPrivateFile: imageKitCollection.isPrivate ? 'true' : 'false',
141
+ overwriteFile: 'false',
142
+ tags: uploadTag,
143
+ useUniqueFileName: 'false'
144
+ };
145
+ const receiptContext = {
146
+ collectionSlug,
147
+ expectedMimeType,
148
+ expectedSize: requestedSize,
149
+ filename: sanitizedFilename,
150
+ isPrivate: imageKitCollection.isPrivate,
151
+ prefix: resolved.sanitizedDocPrefix,
152
+ uploadTag
153
+ };
154
+ const response = {
155
+ clientUploadContext: {
156
+ prefix: resolved.sanitizedDocPrefix,
157
+ signedReceipt: createClientUploadReceipt({
158
+ collectionSlug,
159
+ context: receiptContext,
160
+ filename: sanitizedFilename,
161
+ req
162
+ })
163
+ },
164
+ filename: sanitizedFilename,
165
+ token: createImageKitUploadToken({
166
+ privateKey,
167
+ publicKey,
168
+ uploadPayload
169
+ }),
170
+ uploadPayload
171
+ };
172
+ return Response.json(response, {
40
173
  headers: {
41
174
  'Cache-Control': 'no-store'
42
175
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/createImageKitUploadAuthHandler.ts"],"sourcesContent":["import type ImageKit from '@imagekit/nodejs'\nimport type { PayloadHandler, UploadCollectionSlug } from 'payload'\n\nimport { getSafeFileName } from 'payload/internal'\n\ntype CreateImageKitUploadAuthHandlerArgs = {\n client: ImageKit\n collections: Partial<Record<UploadCollectionSlug, unknown>>\n}\n\nexport const createImageKitUploadAuthHandler = ({\n client,\n collections,\n}: CreateImageKitUploadAuthHandlerArgs): PayloadHandler => {\n return async (req) => {\n const collectionSlug = req.routeParams?.collection\n\n if (typeof collectionSlug !== 'string' || !(collectionSlug in collections)) {\n return Response.json({ message: 'Collection not found.' }, { status: 404 })\n }\n\n if (!req.user) {\n return Response.json({ message: 'Unauthorized.' }, { status: 401 })\n }\n\n const authenticationParameters = client.helper.getAuthenticationParameters()\n const requestURL = new URL(req.url || 'http://localhost')\n const requestedFilename = requestURL.searchParams.get('filename')?.trim()\n const collection = req.payload.collections[collectionSlug]\n\n if (\n !requestedFilename ||\n typeof collection?.config.upload !== 'object' ||\n typeof collection.config.upload.staticDir !== 'string'\n ) {\n return Response.json(authenticationParameters, {\n headers: {\n 'Cache-Control': 'no-store',\n },\n })\n }\n\n const safeFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: requestedFilename,\n req,\n staticPath: collection.config.upload.staticDir,\n })\n\n return Response.json(\n { ...authenticationParameters, fileName: safeFilename },\n {\n headers: {\n 'Cache-Control': 'no-store',\n },\n },\n )\n }\n}\n"],"names":["getSafeFileName","createImageKitUploadAuthHandler","client","collections","req","collectionSlug","routeParams","collection","Response","json","message","status","user","authenticationParameters","helper","getAuthenticationParameters","requestURL","URL","url","requestedFilename","searchParams","get","trim","payload","config","upload","staticDir","headers","safeFilename","desiredFilename","staticPath","fileName"],"mappings":"AAGA,SAASA,eAAe,QAAQ,mBAAkB;AAOlD,OAAO,MAAMC,kCAAkC,CAAC,EAC9CC,MAAM,EACNC,WAAW,EACyB;IACpC,OAAO,OAAOC;QACZ,MAAMC,iBAAiBD,IAAIE,WAAW,EAAEC;QAExC,IAAI,OAAOF,mBAAmB,YAAY,CAAEA,CAAAA,kBAAkBF,WAAU,GAAI;YAC1E,OAAOK,SAASC,IAAI,CAAC;gBAAEC,SAAS;YAAwB,GAAG;gBAAEC,QAAQ;YAAI;QAC3E;QAEA,IAAI,CAACP,IAAIQ,IAAI,EAAE;YACb,OAAOJ,SAASC,IAAI,CAAC;gBAAEC,SAAS;YAAgB,GAAG;gBAAEC,QAAQ;YAAI;QACnE;QAEA,MAAME,2BAA2BX,OAAOY,MAAM,CAACC,2BAA2B;QAC1E,MAAMC,aAAa,IAAIC,IAAIb,IAAIc,GAAG,IAAI;QACtC,MAAMC,oBAAoBH,WAAWI,YAAY,CAACC,GAAG,CAAC,aAAaC;QACnE,MAAMf,aAAaH,IAAImB,OAAO,CAACpB,WAAW,CAACE,eAAe;QAE1D,IACE,CAACc,qBACD,OAAOZ,YAAYiB,OAAOC,WAAW,YACrC,OAAOlB,WAAWiB,MAAM,CAACC,MAAM,CAACC,SAAS,KAAK,UAC9C;YACA,OAAOlB,SAASC,IAAI,CAACI,0BAA0B;gBAC7Cc,SAAS;oBACP,iBAAiB;gBACnB;YACF;QACF;QAEA,MAAMC,eAAe,MAAM5B,gBAAgB;YACzCK;YACAwB,iBAAiBV;YACjBf;YACA0B,YAAYvB,WAAWiB,MAAM,CAACC,MAAM,CAACC,SAAS;QAChD;QAEA,OAAOlB,SAASC,IAAI,CAClB;YAAE,GAAGI,wBAAwB;YAAEkB,UAAUH;QAAa,GACtD;YACED,SAAS;gBACP,iBAAiB;YACnB;QACF;IAEJ;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/endpoints/createImageKitUploadAuthHandler.ts"],"sourcesContent":["import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types'\nimport type { PayloadHandler, UploadCollectionSlug } from 'payload'\n\nimport { buildUploadStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { randomUUID } from 'node:crypto'\nimport { posix } from 'node:path'\nimport { APIError, Forbidden } from 'payload'\nimport {\n assertClientUploadAccess,\n assertClientUploadAllowed,\n assertClientUploadFileSize,\n createClientUploadReceipt,\n getSafeFileName,\n verifyClientUploadReceipt,\n} from 'payload/internal'\nimport { getSanitizedUploadFilename, validateMimeType } from 'payload/shared'\n\nimport type {\n ImageKitReceiptContext,\n ImageKitUploadAuthorization,\n ImageKitUploadCompletion,\n ImageKitUploadPayload,\n} from '../clientUploads.js'\nimport type { createImageKitClientUploadVerifier } from '../clientUploadVerification.js'\n\nimport { isImageKitReceiptContext } from '../clientUploads.js'\nimport { createImageKitUploadToken } from '../imageKitUploadToken.js'\n\ntype ImageKitUploadCollection = {\n isPrivate: boolean\n prefix: string\n}\n\ntype CreateImageKitUploadAuthHandlerArgs = {\n access?: ClientUploadsAccess\n collections: Partial<Record<UploadCollectionSlug, ImageKitUploadCollection>>\n privateKey: string\n publicKey: string\n verifyClientUpload: ReturnType<typeof createImageKitClientUploadVerifier>\n}\n\nconst defaultAccess: ClientUploadsAccess = ({ req }) => Boolean(req.user)\n\nconst bytesToMB = (bytes: number): string => (bytes / 1024 / 1024).toFixed(2)\n\nconst getMimeTypeEssence = (mimeType: string): string =>\n (mimeType.split(';', 1)[0] || '').trim().toLowerCase()\n\nconst getImageKitSafeFilename = (filename: string): string =>\n getSanitizedUploadFilename(filename).replace(/[^\\w.-]/g, '_')\n\n// Continue Payload's numeric suffix convention when the conflict exists only in ImageKit.\nconst nextFilename = (filename: string): string => {\n const { name, ext } = posix.parse(filename)\n const suffix = /^(.*)-(\\d+)$/.exec(name)\n return `${suffix ? `${suffix[1]}-${Number(suffix[2]) + 1}` : `${name}-1`}${ext}`\n}\n\nexport const createImageKitUploadAuthHandler = ({\n access = defaultAccess,\n collections,\n privateKey,\n publicKey,\n verifyClientUpload,\n}: CreateImageKitUploadAuthHandlerArgs): PayloadHandler => {\n return async (req) => {\n if (!req.json) {\n throw new APIError('Content-Type expected to be application/json', 400)\n }\n\n const body = (await req.json()) as {\n action?: unknown\n collectionSlug?: unknown\n fileId?: unknown\n filename?: unknown\n filesize?: unknown\n mimeType?: unknown\n signedReceipt?: unknown\n }\n const { action, collectionSlug, fileId, signedReceipt } = body\n let { filename, filesize, mimeType } = body\n\n if (typeof collectionSlug !== 'string' || !Object.hasOwn(collections, collectionSlug)) {\n throw new APIError('Invalid upload payload.', 400)\n }\n\n await assertClientUploadAccess({ collectionSlug, req })\n\n const imageKitCollection = collections[collectionSlug]\n if (!imageKitCollection) {\n throw new APIError(`Collection ${collectionSlug} was not found in ImageKit options.`, 400)\n }\n\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n\n if (action === 'complete') {\n if (typeof fileId !== 'string' || !fileId.trim() || typeof signedReceipt !== 'string') {\n throw new APIError('Invalid ImageKit upload completion payload.', 400)\n }\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n if (\n !isImageKitReceiptContext(receipt.context) ||\n receipt.filename !== receipt.context.filename ||\n (receipt.context.fileId !== undefined && receipt.context.fileId !== fileId)\n ) {\n throw new APIError('Invalid ImageKit client upload context.', 400)\n }\n\n // The browser supplies a lookup ID, not proof. Check provider state against the\n // original signed authorization before including that ID in a new signed receipt.\n const context: ImageKitReceiptContext = { ...receipt.context, fileId }\n await verifyClientUpload({ collectionSlug, context })\n delete context.uploadTag\n const response: ImageKitUploadCompletion = {\n clientUploadContext: {\n _objectKey: context._objectKey,\n prefix: context.prefix,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context,\n filename: context.filename,\n req,\n }),\n },\n }\n return Response.json(response, { headers: { 'Cache-Control': 'no-store' } })\n }\n\n if (action === 'retry') {\n if (typeof signedReceipt !== 'string') {\n throw new APIError('Invalid ImageKit upload retry payload.', 400)\n }\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n if (\n !isImageKitReceiptContext(receipt.context) ||\n receipt.filename !== receipt.context.filename ||\n receipt.context.fileId !== undefined\n ) {\n throw new APIError('Invalid ImageKit upload retry payload.', 400)\n }\n filename = nextFilename(receipt.context.filename)\n filesize = receipt.context.expectedSize\n mimeType = receipt.context.expectedMimeType\n }\n\n if (\n (action !== undefined && action !== 'retry') ||\n typeof filename !== 'string' ||\n typeof mimeType !== 'string'\n ) {\n throw new APIError('Invalid upload payload.', 400)\n }\n\n assertClientUploadAllowed({\n collection: req.payload.collections[collectionSlug]?.config,\n filename,\n mimeType,\n })\n assertClientUploadFileSize(filesize)\n\n const requestedSize = filesize as number\n const expectedMimeType = getMimeTypeEssence(mimeType)\n if (!expectedMimeType) {\n throw new APIError('A valid MIME type is required for ImageKit client uploads.', 400)\n }\n\n const collectionConfig = req.payload.collections[collectionSlug]?.config\n const allowedMimeTypes =\n collectionConfig &&\n typeof collectionConfig.upload === 'object' &&\n Array.isArray(collectionConfig.upload.mimeTypes)\n ? collectionConfig.upload.mimeTypes\n : []\n\n if (allowedMimeTypes.length > 0 && !validateMimeType(expectedMimeType, allowedMimeTypes)) {\n throw new APIError(`File type ${expectedMimeType} is not allowed.`, 400)\n }\n\n const filesizeLimit = req.payload.config.upload.limits?.fileSize\n\n if (\n typeof filesizeLimit === 'number' &&\n Number.isFinite(filesizeLimit) &&\n requestedSize > filesizeLimit\n ) {\n throw new APIError(\n `Exceeded file size limit. Limit: ${bytesToMB(filesizeLimit)}MB, got: ${bytesToMB(requestedSize)}MB`,\n 400,\n )\n }\n\n const sanitizedFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: getImageKitSafeFilename(filename),\n req,\n })\n const resolved = buildUploadStoragePathData({\n collectionPrefix: imageKitCollection.prefix,\n docPrefix: imageKitCollection.prefix,\n filename: sanitizedFilename,\n })\n const folder = `/${resolved.storageFilePath.slice(0, -(sanitizedFilename.length + 1))}`\n const uploadTag = `payload-upload-${randomUUID()}`\n const uploadPayload: ImageKitUploadPayload = {\n checks: `\"file.size\" = ${requestedSize} AND \"file.mime\" = \"${expectedMimeType}\"`,\n fileName: sanitizedFilename,\n folder,\n isPrivateFile: imageKitCollection.isPrivate ? 'true' : 'false',\n overwriteFile: 'false',\n tags: uploadTag,\n useUniqueFileName: 'false',\n }\n const receiptContext: ImageKitReceiptContext = {\n collectionSlug,\n expectedMimeType,\n expectedSize: requestedSize,\n filename: sanitizedFilename,\n isPrivate: imageKitCollection.isPrivate,\n prefix: resolved.sanitizedDocPrefix,\n uploadTag,\n }\n const response: ImageKitUploadAuthorization = {\n clientUploadContext: {\n prefix: resolved.sanitizedDocPrefix,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context: receiptContext,\n filename: sanitizedFilename,\n req,\n }),\n },\n filename: sanitizedFilename,\n token: createImageKitUploadToken({ privateKey, publicKey, uploadPayload }),\n uploadPayload,\n }\n\n return Response.json(response, {\n headers: {\n 'Cache-Control': 'no-store',\n },\n })\n }\n}\n"],"names":["buildUploadStoragePathData","randomUUID","posix","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","assertClientUploadFileSize","createClientUploadReceipt","getSafeFileName","verifyClientUploadReceipt","getSanitizedUploadFilename","validateMimeType","isImageKitReceiptContext","createImageKitUploadToken","defaultAccess","req","Boolean","user","bytesToMB","bytes","toFixed","getMimeTypeEssence","mimeType","split","trim","toLowerCase","getImageKitSafeFilename","filename","replace","nextFilename","name","ext","parse","suffix","exec","Number","createImageKitUploadAuthHandler","access","collections","privateKey","publicKey","verifyClientUpload","json","body","action","collectionSlug","fileId","signedReceipt","filesize","Object","hasOwn","imageKitCollection","t","receipt","context","undefined","uploadTag","response","clientUploadContext","_objectKey","prefix","Response","headers","expectedSize","expectedMimeType","collection","payload","config","requestedSize","collectionConfig","allowedMimeTypes","upload","Array","isArray","mimeTypes","length","filesizeLimit","limits","fileSize","isFinite","sanitizedFilename","desiredFilename","resolved","collectionPrefix","docPrefix","folder","storageFilePath","slice","uploadPayload","checks","fileName","isPrivateFile","isPrivate","overwriteFile","tags","useUniqueFileName","receiptContext","sanitizedDocPrefix","token"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { ImageKitUploadPayload } from './clientUploads.js';
2
+ /**
3
+ * Creates the ImageKit Upload API V2 JWT. ImageKit authenticates the token with
4
+ * HS256 and compares every non-standard claim with the multipart upload payload.
5
+ * The `file` and `token` fields are the only upload fields intentionally absent.
6
+ */
7
+ export declare const createImageKitUploadToken: ({ now, privateKey, publicKey, uploadPayload, }: {
8
+ now?: number;
9
+ privateKey: string;
10
+ publicKey: string;
11
+ uploadPayload: ImageKitUploadPayload;
12
+ }) => string;
@@ -0,0 +1,25 @@
1
+ import { createHmac } from 'node:crypto';
2
+ const IMAGEKIT_UPLOAD_TOKEN_LIFETIME_SECONDS = 10 * 60;
3
+ const encode = (value)=>Buffer.from(JSON.stringify(value)).toString('base64url');
4
+ /**
5
+ * Creates the ImageKit Upload API V2 JWT. ImageKit authenticates the token with
6
+ * HS256 and compares every non-standard claim with the multipart upload payload.
7
+ * The `file` and `token` fields are the only upload fields intentionally absent.
8
+ */ export const createImageKitUploadToken = ({ now = Date.now(), privateKey, publicKey, uploadPayload })=>{
9
+ const issuedAt = Math.floor(now / 1000);
10
+ const header = encode({
11
+ alg: 'HS256',
12
+ kid: publicKey,
13
+ typ: 'JWT'
14
+ });
15
+ const payload = encode({
16
+ ...uploadPayload,
17
+ exp: issuedAt + IMAGEKIT_UPLOAD_TOKEN_LIFETIME_SECONDS,
18
+ iat: issuedAt
19
+ });
20
+ const unsignedToken = `${header}.${payload}`;
21
+ const signature = createHmac('sha256', privateKey).update(unsignedToken).digest('base64url');
22
+ return `${unsignedToken}.${signature}`;
23
+ };
24
+
25
+ //# sourceMappingURL=imageKitUploadToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/imageKitUploadToken.ts"],"sourcesContent":["import { createHmac } from 'node:crypto'\n\nimport type { ImageKitUploadPayload } from './clientUploads.js'\n\nconst IMAGEKIT_UPLOAD_TOKEN_LIFETIME_SECONDS = 10 * 60\n\nconst encode = (value: unknown): string =>\n Buffer.from(JSON.stringify(value)).toString('base64url')\n\n/**\n * Creates the ImageKit Upload API V2 JWT. ImageKit authenticates the token with\n * HS256 and compares every non-standard claim with the multipart upload payload.\n * The `file` and `token` fields are the only upload fields intentionally absent.\n */\nexport const createImageKitUploadToken = ({\n now = Date.now(),\n privateKey,\n publicKey,\n uploadPayload,\n}: {\n now?: number\n privateKey: string\n publicKey: string\n uploadPayload: ImageKitUploadPayload\n}): string => {\n const issuedAt = Math.floor(now / 1000)\n const header = encode({ alg: 'HS256', kid: publicKey, typ: 'JWT' })\n const payload = encode({\n ...uploadPayload,\n exp: issuedAt + IMAGEKIT_UPLOAD_TOKEN_LIFETIME_SECONDS,\n iat: issuedAt,\n })\n const unsignedToken = `${header}.${payload}`\n const signature = createHmac('sha256', privateKey).update(unsignedToken).digest('base64url')\n\n return `${unsignedToken}.${signature}`\n}\n"],"names":["createHmac","IMAGEKIT_UPLOAD_TOKEN_LIFETIME_SECONDS","encode","value","Buffer","from","JSON","stringify","toString","createImageKitUploadToken","now","Date","privateKey","publicKey","uploadPayload","issuedAt","Math","floor","header","alg","kid","typ","payload","exp","iat","unsignedToken","signature","update","digest"],"mappings":"AAAA,SAASA,UAAU,QAAQ,cAAa;AAIxC,MAAMC,yCAAyC,KAAK;AAEpD,MAAMC,SAAS,CAACC,QACdC,OAAOC,IAAI,CAACC,KAAKC,SAAS,CAACJ,QAAQK,QAAQ,CAAC;AAE9C;;;;CAIC,GACD,OAAO,MAAMC,4BAA4B,CAAC,EACxCC,MAAMC,KAAKD,GAAG,EAAE,EAChBE,UAAU,EACVC,SAAS,EACTC,aAAa,EAMd;IACC,MAAMC,WAAWC,KAAKC,KAAK,CAACP,MAAM;IAClC,MAAMQ,SAAShB,OAAO;QAAEiB,KAAK;QAASC,KAAKP;QAAWQ,KAAK;IAAM;IACjE,MAAMC,UAAUpB,OAAO;QACrB,GAAGY,aAAa;QAChBS,KAAKR,WAAWd;QAChBuB,KAAKT;IACP;IACA,MAAMU,gBAAgB,GAAGP,OAAO,CAAC,EAAEI,SAAS;IAC5C,MAAMI,YAAY1B,WAAW,UAAUY,YAAYe,MAAM,CAACF,eAAeG,MAAM,CAAC;IAEhF,OAAO,GAAGH,cAAc,CAAC,EAAEC,WAAW;AACxC,EAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ClientUploadsConfig } from '@payloadcms/plugin-cloud-storage/types';
1
2
  import type { Config, UploadCollectionSlug } from 'payload';
2
3
  export type ImageKitCollectionOptions = {
3
4
  /**
@@ -15,7 +16,7 @@ type ImageKitCollectionConfig = ImageKitCollectionOptions | true;
15
16
  type ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>;
16
17
  export type PayloadStorageImageKitConfig = {
17
18
  alwaysInsertFields?: boolean;
18
- clientUploads?: boolean;
19
+ clientUploads?: ClientUploadsConfig;
19
20
  collections: ImageKitCollectionMap;
20
21
  disablePayloadAccessControl?: true;
21
22
  enabled?: boolean;