@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
@@ -0,0 +1,117 @@
1
+ import { buildUploadStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { posix } from 'node:path';
4
+ import { Forbidden } from 'payload';
5
+ import { assertClientUploadAccess, assertClientUploadAllowed, createClientUploadReceipt, getOriginalFilename, getSafeFileName, incrementName } from 'payload/internal';
6
+ import { getSanitizedUploadFilename } from 'payload/shared';
7
+ import { createImageKitUploadToken } from '../imageKitUploadToken.js';
8
+ /** ImageKit enforces these constraints before accepting any provider-direct bytes. */ export const getUploadChecks = ({ allowedMimeTypes, allowRestrictedFileTypes, filesize })=>{
9
+ const checks = [
10
+ `"file.size" = ${filesize}`
11
+ ];
12
+ const prefixes = allowedMimeTypes.map((mimeType)=>mimeType.replace('*', '')).filter((prefix)=>!/["\\]/.test(prefix));
13
+ if (prefixes.length && !prefixes.includes('')) {
14
+ checks.push(`(${prefixes.map((prefix)=>`"file.mime" : "${prefix}"`).join(' OR ')})`);
15
+ }
16
+ if (!allowRestrictedFileTypes) {
17
+ checks.push('"file.mime" NOT IN ["application/atom+xml", "application/mathml+xml", "application/rdf+xml", "application/rss+xml", "application/xhtml+xml", "application/xml", "application/xslt+xml", "image/svg+xml", "text/xml"]');
18
+ }
19
+ return checks.join(' AND ');
20
+ };
21
+ export const createImageKitUploadInstructions = ({ access = ({ req })=>Boolean(req.user), isPrivate, prefix, privateKey, publicKey })=>async ({ collectionSlug, docPrefix, filename, filesize, mimeType, overrideAccess, req })=>{
22
+ if (!overrideAccess) {
23
+ await assertClientUploadAccess({
24
+ collectionSlug,
25
+ req
26
+ });
27
+ if (!await access({
28
+ collectionSlug,
29
+ req
30
+ })) {
31
+ throw new Forbidden(req.t);
32
+ }
33
+ }
34
+ const collection = req.payload.collections[collectionSlug].config;
35
+ assertClientUploadAllowed({
36
+ collection,
37
+ filename,
38
+ mimeType
39
+ });
40
+ let safeFilename = await getSafeFileName({
41
+ collectionSlug,
42
+ desiredFilename: getSanitizedUploadFilename(filename).replace(/[^\w.-]/g, '_'),
43
+ req
44
+ });
45
+ // Without a transformer, v4 exposes the retained original as the current file.
46
+ // Reserve both names so repeated uploads cannot violate filename uniqueness.
47
+ while(await getSafeFileName({
48
+ collectionSlug,
49
+ desiredFilename: getOriginalFilename({
50
+ filename: safeFilename
51
+ }),
52
+ req
53
+ }) !== getOriginalFilename({
54
+ filename: safeFilename
55
+ })){
56
+ safeFilename = incrementName(safeFilename);
57
+ }
58
+ const originalFilename = getOriginalFilename({
59
+ filename: safeFilename
60
+ });
61
+ const objectKey = randomUUID();
62
+ const resolved = buildUploadStoragePathData({
63
+ collectionPrefix: prefix,
64
+ docPrefix: docPrefix ?? prefix,
65
+ filename: originalFilename
66
+ });
67
+ // The admin sends an empty docPrefix for new files; the resolved destination retains the collection fallback.
68
+ const filePrefix = posix.dirname(resolved.storageFilePath);
69
+ const storageFilePath = posix.join(filePrefix, objectKey, originalFilename);
70
+ const uploadReference = {
71
+ _objectKey: objectKey,
72
+ prefix: filePrefix,
73
+ signedReceipt: createClientUploadReceipt({
74
+ _objectKey: objectKey,
75
+ collectionSlug,
76
+ filename: safeFilename,
77
+ filePrefix,
78
+ req,
79
+ storageFilePath
80
+ })
81
+ };
82
+ const upload = typeof collection.upload === 'object' ? collection.upload : undefined;
83
+ const uploadPayload = {
84
+ checks: getUploadChecks({
85
+ allowedMimeTypes: upload?.mimeTypes ?? [],
86
+ allowRestrictedFileTypes: upload?.allowRestrictedFileTypes ?? false,
87
+ filesize
88
+ }),
89
+ fileName: originalFilename,
90
+ folder: `/${posix.dirname(storageFilePath)}`,
91
+ isPrivateFile: isPrivate ? 'true' : 'false',
92
+ overwriteFile: 'false',
93
+ tags: `payload-upload-${objectKey}`,
94
+ useUniqueFileName: 'false'
95
+ };
96
+ return {
97
+ name: 'imagekit',
98
+ type: 'dispatch',
99
+ data: {
100
+ token: createImageKitUploadToken({
101
+ privateKey,
102
+ publicKey,
103
+ uploadPayload
104
+ }),
105
+ uploadPayload,
106
+ uploadReference
107
+ },
108
+ file: {
109
+ filename: safeFilename,
110
+ mimeType,
111
+ size: filesize,
112
+ uploadReference
113
+ }
114
+ };
115
+ };
116
+
117
+ //# sourceMappingURL=createImageKitUploadInstructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/createImageKitUploadInstructions.ts"],"sourcesContent":["import type { GenerateUploadInstructions, UploadInstructionsAccess } from 'payload'\n\nimport { buildUploadStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { randomUUID } from 'node:crypto'\nimport { posix } from 'node:path'\nimport { Forbidden } from 'payload'\nimport {\n assertClientUploadAccess,\n assertClientUploadAllowed,\n createClientUploadReceipt,\n getOriginalFilename,\n getSafeFileName,\n incrementName,\n} from 'payload/internal'\nimport { getSanitizedUploadFilename } from 'payload/shared'\n\nimport type { ImageKitUploadPayload } from '../clientUploads.js'\n\nimport { createImageKitUploadToken } from '../imageKitUploadToken.js'\n\n/** ImageKit enforces these constraints before accepting any provider-direct bytes. */\nexport const getUploadChecks = ({\n allowedMimeTypes,\n allowRestrictedFileTypes,\n filesize,\n}: {\n allowedMimeTypes: string[]\n allowRestrictedFileTypes: boolean\n filesize: number\n}): string => {\n const checks = [`\"file.size\" = ${filesize}`]\n const prefixes = allowedMimeTypes\n .map((mimeType) => mimeType.replace('*', ''))\n .filter((prefix) => !/[\"\\\\]/.test(prefix))\n\n if (prefixes.length && !prefixes.includes('')) {\n checks.push(`(${prefixes.map((prefix) => `\"file.mime\" : \"${prefix}\"`).join(' OR ')})`)\n }\n if (!allowRestrictedFileTypes) {\n checks.push(\n '\"file.mime\" NOT IN [\"application/atom+xml\", \"application/mathml+xml\", \"application/rdf+xml\", \"application/rss+xml\", \"application/xhtml+xml\", \"application/xml\", \"application/xslt+xml\", \"image/svg+xml\", \"text/xml\"]',\n )\n }\n return checks.join(' AND ')\n}\n\nexport const createImageKitUploadInstructions =\n ({\n access = ({ req }) => Boolean(req.user),\n isPrivate,\n prefix,\n privateKey,\n publicKey,\n }: {\n access?: UploadInstructionsAccess\n isPrivate: boolean\n prefix: string\n privateKey: string\n publicKey: string\n }): GenerateUploadInstructions =>\n async ({ collectionSlug, docPrefix, filename, filesize, mimeType, overrideAccess, req }) => {\n if (!overrideAccess) {\n await assertClientUploadAccess({ collectionSlug, req })\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n }\n const collection = req.payload.collections[collectionSlug].config\n assertClientUploadAllowed({ collection, filename, mimeType })\n let safeFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: getSanitizedUploadFilename(filename).replace(/[^\\w.-]/g, '_'),\n req,\n })\n // Without a transformer, v4 exposes the retained original as the current file.\n // Reserve both names so repeated uploads cannot violate filename uniqueness.\n while (\n (await getSafeFileName({\n collectionSlug,\n desiredFilename: getOriginalFilename({ filename: safeFilename }),\n req,\n })) !== getOriginalFilename({ filename: safeFilename })\n ) {\n safeFilename = incrementName(safeFilename)\n }\n const originalFilename = getOriginalFilename({ filename: safeFilename })\n const objectKey = randomUUID()\n const resolved = buildUploadStoragePathData({\n collectionPrefix: prefix,\n docPrefix: docPrefix ?? prefix,\n filename: originalFilename,\n })\n // The admin sends an empty docPrefix for new files; the resolved destination retains the collection fallback.\n const filePrefix = posix.dirname(resolved.storageFilePath)\n const storageFilePath = posix.join(filePrefix, objectKey, originalFilename)\n const uploadReference = {\n _objectKey: objectKey,\n prefix: filePrefix,\n signedReceipt: createClientUploadReceipt({\n _objectKey: objectKey,\n collectionSlug,\n filename: safeFilename,\n filePrefix,\n req,\n storageFilePath,\n }),\n }\n const upload = typeof collection.upload === 'object' ? collection.upload : undefined\n const uploadPayload: ImageKitUploadPayload = {\n checks: getUploadChecks({\n allowedMimeTypes: upload?.mimeTypes ?? [],\n allowRestrictedFileTypes: upload?.allowRestrictedFileTypes ?? false,\n filesize,\n }),\n fileName: originalFilename,\n folder: `/${posix.dirname(storageFilePath)}`,\n isPrivateFile: isPrivate ? 'true' : 'false',\n overwriteFile: 'false',\n tags: `payload-upload-${objectKey}`,\n useUniqueFileName: 'false',\n }\n return {\n name: 'imagekit',\n type: 'dispatch',\n data: {\n token: createImageKitUploadToken({ privateKey, publicKey, uploadPayload }),\n uploadPayload,\n uploadReference,\n },\n file: { filename: safeFilename, mimeType, size: filesize, uploadReference },\n }\n }\n"],"names":["buildUploadStoragePathData","randomUUID","posix","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","createClientUploadReceipt","getOriginalFilename","getSafeFileName","incrementName","getSanitizedUploadFilename","createImageKitUploadToken","getUploadChecks","allowedMimeTypes","allowRestrictedFileTypes","filesize","checks","prefixes","map","mimeType","replace","filter","prefix","test","length","includes","push","join","createImageKitUploadInstructions","access","req","Boolean","user","isPrivate","privateKey","publicKey","collectionSlug","docPrefix","filename","overrideAccess","t","collection","payload","collections","config","safeFilename","desiredFilename","originalFilename","objectKey","resolved","collectionPrefix","filePrefix","dirname","storageFilePath","uploadReference","_objectKey","signedReceipt","upload","undefined","uploadPayload","mimeTypes","fileName","folder","isPrivateFile","overwriteFile","tags","useUniqueFileName","name","type","data","token","file","size"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import type ImageKit from '@imagekit/nodejs';
2
+ import type { PayloadRequest } from 'payload';
3
+ /** Resolve new uploads by provider ID, without depending on ImageKit's eventual-consistency listing. */
4
+ export declare const createImageKitFileStore: (client: ImageKit, urlEndpoint: string) => {
5
+ find: ({ key, req }: {
6
+ key: string;
7
+ req: PayloadRequest;
8
+ }) => Promise<{
9
+ name: string;
10
+ fileId: string;
11
+ size: number;
12
+ url: string;
13
+ AITags?: Array<ImageKit.Files.File.AITag> | null;
14
+ audioCodec?: string;
15
+ bitRate?: number;
16
+ createdAt?: string;
17
+ customCoordinates?: string | null;
18
+ customMetadata?: {
19
+ [key: string]: unknown;
20
+ };
21
+ description?: string;
22
+ duration?: number;
23
+ embeddedMetadata?: {
24
+ [key: string]: unknown;
25
+ };
26
+ filePath?: string;
27
+ fileType?: string;
28
+ hasAlpha?: boolean;
29
+ height?: number;
30
+ isPrivateFile?: boolean;
31
+ isPublished?: boolean;
32
+ mime?: string;
33
+ selectedFieldsSchema?: {
34
+ [key: string]: ImageKit.Files.File.SelectedFieldsSchema;
35
+ };
36
+ tags?: Array<string> | null;
37
+ thumbnail?: string;
38
+ type?: "file" | "file-version";
39
+ updatedAt?: string;
40
+ versionInfo?: ImageKit.Files.File.VersionInfo;
41
+ videoCodec?: string;
42
+ width?: number;
43
+ } | undefined>;
44
+ getByID: ({ fileId, key }: {
45
+ fileId: string;
46
+ key: string;
47
+ }) => Promise<{
48
+ name: string;
49
+ fileId: string;
50
+ size: number;
51
+ url: string;
52
+ AITags?: Array<ImageKit.Files.File.AITag> | null;
53
+ audioCodec?: string;
54
+ bitRate?: number;
55
+ createdAt?: string;
56
+ customCoordinates?: string | null;
57
+ customMetadata?: {
58
+ [key: string]: unknown;
59
+ };
60
+ description?: string;
61
+ duration?: number;
62
+ embeddedMetadata?: {
63
+ [key: string]: unknown;
64
+ };
65
+ filePath?: string;
66
+ fileType?: string;
67
+ hasAlpha?: boolean;
68
+ height?: number;
69
+ isPrivateFile?: boolean;
70
+ isPublished?: boolean;
71
+ mime?: string;
72
+ selectedFieldsSchema?: {
73
+ [key: string]: ImageKit.Files.File.SelectedFieldsSchema;
74
+ };
75
+ tags?: Array<string> | null;
76
+ thumbnail?: string;
77
+ type?: "file" | "file-version";
78
+ updatedAt?: string;
79
+ versionInfo?: ImageKit.Files.File.VersionInfo;
80
+ videoCodec?: string;
81
+ width?: number;
82
+ }>;
83
+ remember: ({ fileId, key, req, }: {
84
+ fileId: string;
85
+ key: string;
86
+ req: PayloadRequest;
87
+ }) => Promise<void>;
88
+ remove: ({ key, req }: {
89
+ key: string;
90
+ req: PayloadRequest;
91
+ }) => Promise<void>;
92
+ upload: ({ body, key, req, }: {
93
+ body: Parameters<ImageKit["files"]["upload"]>[0];
94
+ key: string;
95
+ req: PayloadRequest;
96
+ }) => Promise<{
97
+ fileId: string;
98
+ url: string;
99
+ AITags?: Array<ImageKit.Files.FileUploadResponse.AITag> | null;
100
+ audioCodec?: string;
101
+ bitRate?: number;
102
+ customCoordinates?: string | null;
103
+ customMetadata?: {
104
+ [key: string]: unknown;
105
+ };
106
+ description?: string;
107
+ duration?: number;
108
+ embeddedMetadata?: {
109
+ [key: string]: unknown;
110
+ };
111
+ extensionStatus?: ImageKit.Files.FileUploadResponse.ExtensionStatus;
112
+ filePath?: string;
113
+ fileType?: string;
114
+ height?: number;
115
+ isPrivateFile?: boolean;
116
+ isPublished?: boolean;
117
+ metadata?: ImageKit.Files.Metadata;
118
+ name?: string;
119
+ selectedFieldsSchema?: {
120
+ [key: string]: ImageKit.Files.FileUploadResponse.SelectedFieldsSchema;
121
+ };
122
+ size?: number;
123
+ tags?: Array<string> | null;
124
+ thumbnailUrl?: string;
125
+ versionInfo?: ImageKit.Files.FileUploadResponse.VersionInfo;
126
+ videoCodec?: string;
127
+ width?: number;
128
+ }>;
129
+ };
@@ -0,0 +1,134 @@
1
+ import { NotFoundError } from '@imagekit/nodejs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { APIError } from 'payload';
5
+ class FileLocationMismatch extends APIError {
6
+ constructor(){
7
+ super('ImageKit file does not match the authorized destination.', 400);
8
+ }
9
+ }
10
+ /** Resolve new uploads by provider ID, without depending on ImageKit's eventual-consistency listing. */ export const createImageKitFileStore = (client, urlEndpoint)=>{
11
+ const registryKey = (key)=>`imagekit:file:${createHash('sha256').update(JSON.stringify([
12
+ urlEndpoint,
13
+ key
14
+ ])).digest('hex')}`;
15
+ const getByID = async ({ fileId, key })=>{
16
+ const filePath = `/${key.replace(/^\/+/, '')}`;
17
+ const file = await client.files.get(fileId);
18
+ if (file.fileId !== fileId || typeof file.name !== 'string' || typeof file.filePath !== 'string' || typeof file.size !== 'number' || !Number.isSafeInteger(file.size) || file.size < 0 || !file.url) {
19
+ throw new APIError('ImageKit returned incomplete file metadata.', 502);
20
+ }
21
+ if (file.name !== path.posix.basename(filePath) || file.filePath !== filePath) {
22
+ throw new FileLocationMismatch();
23
+ }
24
+ return {
25
+ ...file,
26
+ name: file.name,
27
+ fileId,
28
+ size: file.size,
29
+ url: file.url
30
+ };
31
+ };
32
+ const remember = async ({ fileId, key, req })=>{
33
+ // This is a provider-object index, not an authorization or an upload session.
34
+ // Keep it for the object's lifetime (including retained versions), then remove it on delete.
35
+ await req.payload.kv.set(registryKey(key), fileId);
36
+ };
37
+ const upload = async ({ body, key, req })=>{
38
+ const uploaded = await client.files.upload(body);
39
+ try {
40
+ if (!uploaded.fileId || !uploaded.url) {
41
+ throw new APIError('ImageKit returned incomplete upload metadata.', 502);
42
+ }
43
+ await remember({
44
+ fileId: uploaded.fileId,
45
+ key,
46
+ req
47
+ });
48
+ return {
49
+ ...uploaded,
50
+ fileId: uploaded.fileId,
51
+ url: uploaded.url
52
+ };
53
+ } catch (error) {
54
+ // The provider write already succeeded, but the framework may not yet have registered
55
+ // its rollback action (notably for copies). Compensate by the returned ID, never a listing.
56
+ if (uploaded.fileId) {
57
+ try {
58
+ await client.files.delete(uploaded.fileId);
59
+ await req.payload.kv.delete(registryKey(key));
60
+ } catch (cleanupError) {
61
+ req.payload.logger.error({
62
+ err: cleanupError,
63
+ msg: 'Failed to roll back an unregistered ImageKit upload.'
64
+ });
65
+ }
66
+ }
67
+ throw error;
68
+ }
69
+ };
70
+ const find = async ({ key, req })=>{
71
+ const rememberedID = await req.payload.kv.get(registryKey(key));
72
+ if (rememberedID) {
73
+ try {
74
+ return await getByID({
75
+ fileId: rememberedID,
76
+ key
77
+ });
78
+ } catch (error) {
79
+ if (!(error instanceof NotFoundError) && !(error instanceof FileLocationMismatch)) {
80
+ throw error;
81
+ }
82
+ // External deletes or moves can stale the index. Never delete the mismatched object;
83
+ // forget only its mapping and resolve the requested path normally.
84
+ await req.payload.kv.delete(registryKey(key));
85
+ }
86
+ }
87
+ // Existing installations and uploads started by older admin bundles have no registry entry.
88
+ const filePath = `/${key.replace(/^\/+/, '')}`;
89
+ const name = path.posix.basename(filePath);
90
+ for(let skip = 0;; skip += 100){
91
+ const assets = await client.assets.list({
92
+ type: 'file',
93
+ limit: 100,
94
+ path: `${path.posix.dirname(filePath)}/`,
95
+ skip
96
+ });
97
+ const found = assets.find((asset)=>asset.filePath === filePath && asset.name === name);
98
+ if (found?.fileId) {
99
+ return getByID({
100
+ fileId: found.fileId,
101
+ key
102
+ });
103
+ }
104
+ if (assets.length < 100) {
105
+ return undefined;
106
+ }
107
+ }
108
+ };
109
+ const remove = async ({ key, req })=>{
110
+ const file = await find({
111
+ key,
112
+ req
113
+ });
114
+ if (file) {
115
+ try {
116
+ await client.files.delete(file.fileId);
117
+ } catch (error) {
118
+ if (!(error instanceof NotFoundError)) {
119
+ throw error;
120
+ }
121
+ }
122
+ }
123
+ await req.payload.kv.delete(registryKey(key));
124
+ };
125
+ return {
126
+ find,
127
+ getByID,
128
+ remember,
129
+ remove,
130
+ upload
131
+ };
132
+ };
133
+
134
+ //# sourceMappingURL=imageKitFileStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/imageKitFileStore.ts"],"sourcesContent":["import type ImageKit from '@imagekit/nodejs'\nimport type { PayloadRequest } from 'payload'\n\nimport { NotFoundError } from '@imagekit/nodejs'\nimport { createHash } from 'node:crypto'\nimport path from 'node:path'\nimport { APIError } from 'payload'\n\nclass FileLocationMismatch extends APIError {\n constructor() {\n super('ImageKit file does not match the authorized destination.', 400)\n }\n}\n\n/** Resolve new uploads by provider ID, without depending on ImageKit's eventual-consistency listing. */\nexport const createImageKitFileStore = (client: ImageKit, urlEndpoint: string) => {\n const registryKey = (key: string) =>\n `imagekit:file:${createHash('sha256')\n .update(JSON.stringify([urlEndpoint, key]))\n .digest('hex')}`\n\n const getByID = async ({ fileId, key }: { fileId: string; key: string }) => {\n const filePath = `/${key.replace(/^\\/+/, '')}`\n const file = await client.files.get(fileId)\n if (\n file.fileId !== fileId ||\n typeof file.name !== 'string' ||\n typeof file.filePath !== 'string' ||\n typeof file.size !== 'number' ||\n !Number.isSafeInteger(file.size) ||\n file.size < 0 ||\n !file.url\n ) {\n throw new APIError('ImageKit returned incomplete file metadata.', 502)\n }\n if (file.name !== path.posix.basename(filePath) || file.filePath !== filePath) {\n throw new FileLocationMismatch()\n }\n return { ...file, name: file.name, fileId, size: file.size, url: file.url }\n }\n\n const remember = async ({\n fileId,\n key,\n req,\n }: {\n fileId: string\n key: string\n req: PayloadRequest\n }) => {\n // This is a provider-object index, not an authorization or an upload session.\n // Keep it for the object's lifetime (including retained versions), then remove it on delete.\n await req.payload.kv.set(registryKey(key), fileId)\n }\n\n const upload = async ({\n body,\n key,\n req,\n }: {\n body: Parameters<ImageKit['files']['upload']>[0]\n key: string\n req: PayloadRequest\n }) => {\n const uploaded = await client.files.upload(body)\n try {\n if (!uploaded.fileId || !uploaded.url) {\n throw new APIError('ImageKit returned incomplete upload metadata.', 502)\n }\n await remember({ fileId: uploaded.fileId, key, req })\n return { ...uploaded, fileId: uploaded.fileId, url: uploaded.url }\n } catch (error) {\n // The provider write already succeeded, but the framework may not yet have registered\n // its rollback action (notably for copies). Compensate by the returned ID, never a listing.\n if (uploaded.fileId) {\n try {\n await client.files.delete(uploaded.fileId)\n await req.payload.kv.delete(registryKey(key))\n } catch (cleanupError) {\n req.payload.logger.error({\n err: cleanupError,\n msg: 'Failed to roll back an unregistered ImageKit upload.',\n })\n }\n }\n throw error\n }\n }\n\n const find = async ({ key, req }: { key: string; req: PayloadRequest }) => {\n const rememberedID = await req.payload.kv.get<string>(registryKey(key))\n if (rememberedID) {\n try {\n return await getByID({ fileId: rememberedID, key })\n } catch (error) {\n if (!(error instanceof NotFoundError) && !(error instanceof FileLocationMismatch)) {\n throw error\n }\n // External deletes or moves can stale the index. Never delete the mismatched object;\n // forget only its mapping and resolve the requested path normally.\n await req.payload.kv.delete(registryKey(key))\n }\n }\n // Existing installations and uploads started by older admin bundles have no registry entry.\n const filePath = `/${key.replace(/^\\/+/, '')}`\n const name = path.posix.basename(filePath)\n for (let skip = 0; ; skip += 100) {\n const assets = await client.assets.list({\n type: 'file',\n limit: 100,\n path: `${path.posix.dirname(filePath)}/`,\n skip,\n })\n const found = assets.find((asset) => asset.filePath === filePath && asset.name === name)\n if (found?.fileId) {\n return getByID({ fileId: found.fileId, key })\n }\n if (assets.length < 100) {\n return undefined\n }\n }\n }\n\n const remove = async ({ key, req }: { key: string; req: PayloadRequest }) => {\n const file = await find({ key, req })\n if (file) {\n try {\n await client.files.delete(file.fileId)\n } catch (error) {\n if (!(error instanceof NotFoundError)) {\n throw error\n }\n }\n }\n await req.payload.kv.delete(registryKey(key))\n }\n\n return { find, getByID, remember, remove, upload }\n}\n"],"names":["NotFoundError","createHash","path","APIError","FileLocationMismatch","createImageKitFileStore","client","urlEndpoint","registryKey","key","update","JSON","stringify","digest","getByID","fileId","filePath","replace","file","files","get","name","size","Number","isSafeInteger","url","posix","basename","remember","req","payload","kv","set","upload","body","uploaded","error","delete","cleanupError","logger","err","msg","find","rememberedID","skip","assets","list","type","limit","dirname","found","asset","length","undefined","remove"],"mappings":"AAGA,SAASA,aAAa,QAAQ,mBAAkB;AAChD,SAASC,UAAU,QAAQ,cAAa;AACxC,OAAOC,UAAU,YAAW;AAC5B,SAASC,QAAQ,QAAQ,UAAS;AAElC,MAAMC,6BAA6BD;IACjC,aAAc;QACZ,KAAK,CAAC,4DAA4D;IACpE;AACF;AAEA,sGAAsG,GACtG,OAAO,MAAME,0BAA0B,CAACC,QAAkBC;IACxD,MAAMC,cAAc,CAACC,MACnB,CAAC,cAAc,EAAER,WAAW,UACzBS,MAAM,CAACC,KAAKC,SAAS,CAAC;YAACL;YAAaE;SAAI,GACxCI,MAAM,CAAC,QAAQ;IAEpB,MAAMC,UAAU,OAAO,EAAEC,MAAM,EAAEN,GAAG,EAAmC;QACrE,MAAMO,WAAW,CAAC,CAAC,EAAEP,IAAIQ,OAAO,CAAC,QAAQ,KAAK;QAC9C,MAAMC,OAAO,MAAMZ,OAAOa,KAAK,CAACC,GAAG,CAACL;QACpC,IACEG,KAAKH,MAAM,KAAKA,UAChB,OAAOG,KAAKG,IAAI,KAAK,YACrB,OAAOH,KAAKF,QAAQ,KAAK,YACzB,OAAOE,KAAKI,IAAI,KAAK,YACrB,CAACC,OAAOC,aAAa,CAACN,KAAKI,IAAI,KAC/BJ,KAAKI,IAAI,GAAG,KACZ,CAACJ,KAAKO,GAAG,EACT;YACA,MAAM,IAAItB,SAAS,+CAA+C;QACpE;QACA,IAAIe,KAAKG,IAAI,KAAKnB,KAAKwB,KAAK,CAACC,QAAQ,CAACX,aAAaE,KAAKF,QAAQ,KAAKA,UAAU;YAC7E,MAAM,IAAIZ;QACZ;QACA,OAAO;YAAE,GAAGc,IAAI;YAAEG,MAAMH,KAAKG,IAAI;YAAEN;YAAQO,MAAMJ,KAAKI,IAAI;YAAEG,KAAKP,KAAKO,GAAG;QAAC;IAC5E;IAEA,MAAMG,WAAW,OAAO,EACtBb,MAAM,EACNN,GAAG,EACHoB,GAAG,EAKJ;QACC,8EAA8E;QAC9E,6FAA6F;QAC7F,MAAMA,IAAIC,OAAO,CAACC,EAAE,CAACC,GAAG,CAACxB,YAAYC,MAAMM;IAC7C;IAEA,MAAMkB,SAAS,OAAO,EACpBC,IAAI,EACJzB,GAAG,EACHoB,GAAG,EAKJ;QACC,MAAMM,WAAW,MAAM7B,OAAOa,KAAK,CAACc,MAAM,CAACC;QAC3C,IAAI;YACF,IAAI,CAACC,SAASpB,MAAM,IAAI,CAACoB,SAASV,GAAG,EAAE;gBACrC,MAAM,IAAItB,SAAS,iDAAiD;YACtE;YACA,MAAMyB,SAAS;gBAAEb,QAAQoB,SAASpB,MAAM;gBAAEN;gBAAKoB;YAAI;YACnD,OAAO;gBAAE,GAAGM,QAAQ;gBAAEpB,QAAQoB,SAASpB,MAAM;gBAAEU,KAAKU,SAASV,GAAG;YAAC;QACnE,EAAE,OAAOW,OAAO;YACd,sFAAsF;YACtF,4FAA4F;YAC5F,IAAID,SAASpB,MAAM,EAAE;gBACnB,IAAI;oBACF,MAAMT,OAAOa,KAAK,CAACkB,MAAM,CAACF,SAASpB,MAAM;oBACzC,MAAMc,IAAIC,OAAO,CAACC,EAAE,CAACM,MAAM,CAAC7B,YAAYC;gBAC1C,EAAE,OAAO6B,cAAc;oBACrBT,IAAIC,OAAO,CAACS,MAAM,CAACH,KAAK,CAAC;wBACvBI,KAAKF;wBACLG,KAAK;oBACP;gBACF;YACF;YACA,MAAML;QACR;IACF;IAEA,MAAMM,OAAO,OAAO,EAAEjC,GAAG,EAAEoB,GAAG,EAAwC;QACpE,MAAMc,eAAe,MAAMd,IAAIC,OAAO,CAACC,EAAE,CAACX,GAAG,CAASZ,YAAYC;QAClE,IAAIkC,cAAc;YAChB,IAAI;gBACF,OAAO,MAAM7B,QAAQ;oBAAEC,QAAQ4B;oBAAclC;gBAAI;YACnD,EAAE,OAAO2B,OAAO;gBACd,IAAI,CAAEA,CAAAA,iBAAiBpC,aAAY,KAAM,CAAEoC,CAAAA,iBAAiBhC,oBAAmB,GAAI;oBACjF,MAAMgC;gBACR;gBACA,qFAAqF;gBACrF,mEAAmE;gBACnE,MAAMP,IAAIC,OAAO,CAACC,EAAE,CAACM,MAAM,CAAC7B,YAAYC;YAC1C;QACF;QACA,4FAA4F;QAC5F,MAAMO,WAAW,CAAC,CAAC,EAAEP,IAAIQ,OAAO,CAAC,QAAQ,KAAK;QAC9C,MAAMI,OAAOnB,KAAKwB,KAAK,CAACC,QAAQ,CAACX;QACjC,IAAK,IAAI4B,OAAO,IAAKA,QAAQ,IAAK;YAChC,MAAMC,SAAS,MAAMvC,OAAOuC,MAAM,CAACC,IAAI,CAAC;gBACtCC,MAAM;gBACNC,OAAO;gBACP9C,MAAM,GAAGA,KAAKwB,KAAK,CAACuB,OAAO,CAACjC,UAAU,CAAC,CAAC;gBACxC4B;YACF;YACA,MAAMM,QAAQL,OAAOH,IAAI,CAAC,CAACS,QAAUA,MAAMnC,QAAQ,KAAKA,YAAYmC,MAAM9B,IAAI,KAAKA;YACnF,IAAI6B,OAAOnC,QAAQ;gBACjB,OAAOD,QAAQ;oBAAEC,QAAQmC,MAAMnC,MAAM;oBAAEN;gBAAI;YAC7C;YACA,IAAIoC,OAAOO,MAAM,GAAG,KAAK;gBACvB,OAAOC;YACT;QACF;IACF;IAEA,MAAMC,SAAS,OAAO,EAAE7C,GAAG,EAAEoB,GAAG,EAAwC;QACtE,MAAMX,OAAO,MAAMwB,KAAK;YAAEjC;YAAKoB;QAAI;QACnC,IAAIX,MAAM;YACR,IAAI;gBACF,MAAMZ,OAAOa,KAAK,CAACkB,MAAM,CAACnB,KAAKH,MAAM;YACvC,EAAE,OAAOqB,OAAO;gBACd,IAAI,CAAEA,CAAAA,iBAAiBpC,aAAY,GAAI;oBACrC,MAAMoC;gBACR;YACF;QACF;QACA,MAAMP,IAAIC,OAAO,CAACC,EAAE,CAACM,MAAM,CAAC7B,YAAYC;IAC1C;IAEA,OAAO;QAAEiC;QAAM5B;QAASc;QAAU0B;QAAQrB;IAAO;AACnD,EAAC"}
package/dist/index.d.ts CHANGED
@@ -1,47 +1,25 @@
1
1
  import type { ClientUploadsConfig } from '@payloadcms/plugin-cloud-storage/types';
2
- import type { Config, UploadCollectionSlug } from 'payload';
2
+ import type { StorageAdapter, UploadCollectionSlug } from 'payload';
3
3
  export type ImageKitCollectionOptions = {
4
- /**
5
- * Override Payload's native focal-point picker (`upload.focalPoint`) for this
6
- * collection.
7
- */
8
4
  focalPoint?: boolean;
9
- /**
10
- * Upload files in this collection as ImageKit private files (`isPrivateFile: true`)
11
- * and deliver them only through short-lived signed URLs, behind Payload access control.
12
- */
13
5
  private?: boolean;
14
6
  };
15
- type ImageKitCollectionConfig = ImageKitCollectionOptions | true;
16
- type ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>;
17
7
  export type PayloadStorageImageKitConfig = {
18
- alwaysInsertFields?: boolean;
19
8
  clientUploads?: ClientUploadsConfig;
20
- collections: ImageKitCollectionMap;
9
+ collections: Partial<Record<UploadCollectionSlug, ImageKitCollectionOptions | true>>;
21
10
  disablePayloadAccessControl?: true;
22
11
  enabled?: boolean;
23
- /**
24
- * Broad default for Payload's native focal-point picker (`upload.focalPoint`)
25
- * on every configured collection. Focal points are disabled by default for
26
- * configured collections, so prefer collection-level `focalPoint: true` for
27
- * image collections. A collection's own `upload.focalPoint` still takes
28
- * precedence.
29
- */
30
12
  focalPoint?: boolean;
31
13
  folder: string;
32
14
  privateKey: string;
33
15
  publicKey: string;
34
- /**
35
- * Expiry (in seconds) applied to signed URLs generated for private collections.
36
- * Defaults to 900 (15 minutes). URLs are signed fresh on every request, so a short
37
- * window is safe.
38
- */
39
16
  signedUrlExpiresIn?: number;
40
17
  urlEndpoint: string;
41
18
  };
42
- export declare const imagekitStorage: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
19
+ /** Payload 4 storage capability. Register under config.storage, not config.plugins. */
20
+ export declare const imagekitStorage: (options: PayloadStorageImageKitConfig) => StorageAdapter;
43
21
  export { migrateFolder } from './migrate.js';
44
22
  export type { MigrateFolderOptions, MigrationJobResult, MigrationLocation } from './migrate.js';
45
- export declare const payloadStorageImagekit: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
46
- export declare const payloadStorageImageKit: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
23
+ export declare const payloadStorageImagekit: (options: PayloadStorageImageKitConfig) => StorageAdapter;
24
+ export declare const payloadStorageImageKit: (options: PayloadStorageImageKitConfig) => StorageAdapter;
47
25
  export type PayloadStorageImagekitConfig = PayloadStorageImageKitConfig;