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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +73 -7
  2. package/dist/clientUploads.d.ts +4 -41
  3. package/dist/clientUploads.js +1 -8
  4. package/dist/clientUploads.js.map +1 -1
  5. package/dist/components/BeforeDashboardServer.d.ts +1 -1
  6. package/dist/components/BeforeDashboardServer.js +6 -1
  7. package/dist/components/BeforeDashboardServer.js.map +1 -1
  8. package/dist/components/ImageKitUploadHandler.d.ts +3 -4
  9. package/dist/components/ImageKitUploadHandler.js +69 -80
  10. package/dist/components/ImageKitUploadHandler.js.map +1 -1
  11. package/dist/endpoints/completeImageKitUpload.d.ts +10 -0
  12. package/dist/endpoints/completeImageKitUpload.js +84 -0
  13. package/dist/endpoints/completeImageKitUpload.js.map +1 -0
  14. package/dist/endpoints/createImageKitUploadInstructions.d.ts +14 -0
  15. package/dist/endpoints/createImageKitUploadInstructions.js +117 -0
  16. package/dist/endpoints/createImageKitUploadInstructions.js.map +1 -0
  17. package/dist/imageKitFileStore.d.ts +129 -0
  18. package/dist/imageKitFileStore.js +134 -0
  19. package/dist/imageKitFileStore.js.map +1 -0
  20. package/dist/index.d.ts +6 -28
  21. package/dist/index.js +465 -640
  22. package/dist/index.js.map +1 -1
  23. package/dist/migrate.js +4 -1
  24. package/dist/migrate.js.map +1 -1
  25. package/package.json +13 -13
  26. package/dist/clientUploadVerification.d.ts +0 -30
  27. package/dist/clientUploadVerification.js +0 -106
  28. package/dist/clientUploadVerification.js.map +0 -1
  29. package/dist/endpoints/createImageKitUploadAuthHandler.d.ts +0 -26
  30. package/dist/endpoints/createImageKitUploadAuthHandler.js +0 -215
  31. package/dist/endpoints/createImageKitUploadAuthHandler.js.map +0 -1
package/README.md CHANGED
@@ -21,8 +21,26 @@ This plugin integrates `@payloadcms/plugin-cloud-storage` with ImageKit and prov
21
21
  pnpm add @humaan/payload-storage-imagekit
22
22
  ```
23
23
 
24
- Payload `3.90.1` or newer is required. Use matching versions of `payload` and
25
- `@payloadcms/plugin-cloud-storage`.
24
+ This branch targets Payload `4.0.0-beta.1`. Use that exact version of `payload` and
25
+ `@payloadcms/plugin-cloud-storage`. Node.js >=24.15.0 is required.
26
+
27
+ ### Maintainer releases
28
+
29
+ In **Actions → Start release**, set `release_branch` to the branch to release from
30
+ (default: `main`). The version/changelog PR targets that branch. Stable `2.x`
31
+ releases continue from `main`; for Payload v4, select `feat/payload-v4-beta`,
32
+ `release_type: custom`, and `custom_version: 3.0.0-canary.0` (increment the canary
33
+ number for subsequent releases). The v4 feature PR does not need to merge.
34
+
35
+ Non-main releases must be prereleases. `canary` versions publish under the npm
36
+ `canary` tag without changing `latest`, after the release commit passes push CI.
37
+ For a publication dry run or receipt-based recovery, choose the same branch in
38
+ **Publish release (automatic)**. Recovery still requires the original receipt run
39
+ ID and the `publish` confirmation.
40
+
41
+ The shared `humaan/actions@v2` update must be deployed first, and these caller
42
+ workflow changes must be present on the selected branch. The `release` environment
43
+ must permit that branch under its deployment policy.
26
44
 
27
45
  ## Usage
28
46
 
@@ -38,7 +56,7 @@ export default buildConfig({
38
56
  fields: [],
39
57
  },
40
58
  ],
41
- plugins: [
59
+ storage: [
42
60
  imagekitStorage({
43
61
  clientUploads: true,
44
62
  folder: '/payload-uploads',
@@ -67,9 +85,59 @@ export default buildConfig({
67
85
  - `signedUrlExpiresIn` (optional): expiry in seconds for the signed URLs generated for [private](#private-files) collections. Defaults to `900` (15 minutes). Signed URLs are generated fresh on every request, so a short window is safe.
68
86
  - `enabled` (optional): enable / disable plugin
69
87
  - `focalPoint` (optional): broad default for Payload's native focal-point picker (`upload.focalPoint`) on every configured collection. Focal points are disabled by default for configured collections, so prefer the per-collection `collections.<slug>.focalPoint` option for mixed upload collections, e.g. enable it for `media` and leave it off for `documents`. When `true`, editors can place a focal point and `focalX`/`focalY` (0–100) are stored on each doc for focal-aware cropping at render time. A collection's own `upload.focalPoint` still takes precedence.
70
- - `alwaysInsertFields` (optional): pass-through to cloud storage plugin
71
88
  - `disablePayloadAccessControl` (optional): pass-through to cloud storage plugin. It cannot be enabled for private collections or collections that can store SVG/XML, because those files require Payload's file route for signing or Content Security Policy headers. To use this option, every configured collection must have a MIME allow list that excludes XML-family types.
72
89
 
90
+ ## Payload 4 migration / local development
91
+
92
+ These compatibility changes do not bump the package version or publish a release.
93
+ A custom canary can be released separately. Build a local snapshot with
94
+ `pnpm pack --out local-canary.tgz`, then reference that archive from the consuming
95
+ app with a `file:` dependency. Repack and update the dependency after edits.
96
+
97
+ Adapters are now registered in `config.storage`. Direct uploads use the shared
98
+ `POST /api/upload-instructions`, the `imagekit` dispatch handler, and Payload's
99
+ signed `uploadReference`. The old `/imagekit-upload-auth` endpoint and
100
+ `clientUploadContext` completion protocol are removed.
101
+
102
+ Each new source file is stored as `<prefix>/<objectKey>/<name>-original.ext`.
103
+ Payload manages originals, current representations, archived revisions, and
104
+ rollback cleanup through the adapter's copy/delete capabilities. XML-family files
105
+ remain server-mediated. Existing v3 records without `original`/`_objectKey`,
106
+ including records with missing or slash-prefixed prefixes, remain readable.
107
+
108
+ The browser still reauthorizes mislabeled raster images using content-detected
109
+ filename/MIME hints; the server independently validates provider metadata and bytes.
110
+
111
+ After a provider-direct upload, the browser calls the adapter's completion endpoint
112
+ (normally `POST /api/imagekit-upload-complete`) with the collection slug, ImageKit's
113
+ returned `fileId`, and the original `signedReceipt`. The server checks access and
114
+ the user-bound receipt, then verifies the provider's exact path, name, upload tag,
115
+ privacy and MIME policy before recording the ID. Document admission retrieves the
116
+ file by that ID and rechecks provider state and file contents. It does not depend
117
+ on ImageKit's asset listing becoming visible immediately after upload.
118
+
119
+ Verified path-to-ID mappings use Payload's existing KV store (`payload-kv` with the
120
+ default database adapter), remain available while the provider object exists, and
121
+ are removed when the adapter deletes that object. Server uploads and managed copies
122
+ also record their returned IDs, so immediate rollback/deletion does not depend on
123
+ listing visibility. A failed registry write triggers cleanup by the returned ID.
124
+ If you configure a custom KV
125
+ adapter, it must persist and share entries across the instances handling completion
126
+ and document-save requests; a process-local cache is not sufficient for serverless
127
+ or multi-instance deployments. No new credentials, custom collections, storage-path
128
+ changes or content migration are required. Existing files and older/in-flight
129
+ clients without a mapping retain the path-listing fallback. Custom direct-upload
130
+ clients should call completion before saving their original Payload reference.
131
+
132
+ Run `pnpm exec vitest run` for mock-provider REST/MongoDB, client transport, and
133
+ folder-migration coverage. Live provider tests require explicitly configured
134
+ `IMAGEKIT_INTEGRATION_*` credentials and are otherwise skipped. The integration
135
+ suite was migrated from the removed v3 endpoint/receipt protocol to the v4 REST
136
+ boundary; the obsolete context-only verification helper is no longer shipped.
137
+
138
+ The folder migration utility covers current/legacy records; migrating every
139
+ archived v4 representation between accounts needs a separate history-aware plan.
140
+
73
141
  ## Exports
74
142
 
75
143
  - `imagekitStorage` (primary)
@@ -89,9 +157,7 @@ export default buildConfig({
89
157
  Use the browser-safe `image` export in app image components to build ImageKit URLs from Payload upload data without importing the server plugin entry.
90
158
 
91
159
  ```ts
92
- import {
93
- withImageKitTransformation,
94
- } from '@humaan/payload-storage-imagekit/image'
160
+ import { withImageKitTransformation } from '@humaan/payload-storage-imagekit/image'
95
161
 
96
162
  const src = withImageKitTransformation(media.imagekit.url, {
97
163
  focalX: media.focalX,
@@ -1,20 +1,7 @@
1
- import type { ClientUploadContext } from '@payloadcms/plugin-cloud-storage/types';
2
- export declare const imageKitUploadAuthPath = "/imagekit-upload-auth";
3
- /**
4
- * @deprecated Provider metadata is verified server-side and is no longer accepted as upload
5
- * proof. This backwards-compatible shape remains exported for existing type consumers.
6
- */
7
- export type ImageKitClientUploadContext = {
8
- fileId?: string;
9
- filePath?: string;
10
- fileType?: string;
11
- mimeType?: string;
12
- name?: string;
13
- size?: number;
14
- thumbnailUrl?: string;
15
- url?: string;
16
- } & Partial<ClientUploadContext>;
17
- /** @deprecated The upload handler is configured internally by Payload 3.90.1. */
1
+ import type { UploadReference } from '@payloadcms/plugin-cloud-storage/types';
2
+ /** @deprecated Use Payload 4's UploadReference and UploadInstructions types. */
3
+ export type ImageKitClientUploadContext = Partial<UploadReference>;
4
+ /** @deprecated The upload handler is configured by the storage capability. */
18
5
  export type ImageKitUploadHandlerProps = {
19
6
  authPath: `/${string}`;
20
7
  collectionSlug: string;
@@ -22,20 +9,6 @@ export type ImageKitUploadHandlerProps = {
22
9
  isPrivate?: boolean;
23
10
  publicKey: string;
24
11
  };
25
- export type ImageKitReceiptContext = {
26
- /** Legacy uploads stored beneath a per-upload folder. */
27
- _objectKey?: string;
28
- collectionSlug: string;
29
- expectedMimeType: string;
30
- expectedSize: number;
31
- /** Present only after the server verifies the completed upload. */
32
- fileId?: string;
33
- filename: string;
34
- isPrivate: boolean;
35
- prefix: string;
36
- /** Binds a pending authorization to the newly uploaded provider object. */
37
- uploadTag?: string;
38
- };
39
12
  export type ImageKitUploadPayload = {
40
13
  checks: string;
41
14
  fileName: string;
@@ -45,13 +18,3 @@ export type ImageKitUploadPayload = {
45
18
  tags: string;
46
19
  useUniqueFileName: 'false';
47
20
  };
48
- export type ImageKitUploadAuthorization = {
49
- clientUploadContext: ClientUploadContext;
50
- filename: string;
51
- token: string;
52
- uploadPayload: ImageKitUploadPayload;
53
- };
54
- export type ImageKitUploadCompletion = {
55
- clientUploadContext: ClientUploadContext;
56
- };
57
- export declare const isImageKitReceiptContext: (value: unknown) => value is ImageKitReceiptContext;
@@ -1,10 +1,3 @@
1
- export const imageKitUploadAuthPath = '/imagekit-upload-auth';
2
- export const isImageKitReceiptContext = (value)=>{
3
- if (!value || typeof value !== 'object') {
4
- return false;
5
- }
6
- const context = value;
7
- return (context._objectKey === undefined || typeof context._objectKey === 'string' && context._objectKey.length > 0) && (context.uploadTag === undefined || typeof context.uploadTag === 'string' && context.uploadTag.length > 0) && Boolean(context._objectKey || context.uploadTag || context.fileId) && typeof context.collectionSlug === 'string' && context.collectionSlug.length > 0 && typeof context.expectedMimeType === 'string' && context.expectedMimeType.length > 0 && typeof context.expectedSize === 'number' && Number.isSafeInteger(context.expectedSize) && context.expectedSize >= 0 && (context.fileId === undefined || typeof context.fileId === 'string' && context.fileId.length > 0) && typeof context.filename === 'string' && context.filename.length > 0 && typeof context.isPrivate === 'boolean' && typeof context.prefix === 'string';
8
- };
1
+ export { };
9
2
 
10
3
  //# sourceMappingURL=clientUploads.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/clientUploads.ts"],"sourcesContent":["import type { ClientUploadContext } from '@payloadcms/plugin-cloud-storage/types'\n\nexport const imageKitUploadAuthPath = '/imagekit-upload-auth'\n\n/**\n * @deprecated Provider metadata is verified server-side and is no longer accepted as upload\n * proof. This backwards-compatible shape remains exported for existing type consumers.\n */\nexport type ImageKitClientUploadContext = {\n fileId?: string\n filePath?: string\n fileType?: string\n mimeType?: string\n name?: string\n size?: number\n thumbnailUrl?: string\n url?: string\n} & Partial<ClientUploadContext>\n\n/** @deprecated The upload handler is configured internally by Payload 3.90.1. */\nexport type ImageKitUploadHandlerProps = {\n authPath: `/${string}`\n collectionSlug: string\n folder: string\n isPrivate?: boolean\n publicKey: string\n}\n\nexport type ImageKitReceiptContext = {\n /** Legacy uploads stored beneath a per-upload folder. */\n _objectKey?: string\n collectionSlug: string\n expectedMimeType: string\n expectedSize: number\n /** Present only after the server verifies the completed upload. */\n fileId?: string\n filename: string\n isPrivate: boolean\n prefix: string\n /** Binds a pending authorization to the newly uploaded provider object. */\n uploadTag?: string\n}\n\nexport type ImageKitUploadPayload = {\n checks: string\n fileName: string\n folder: string\n isPrivateFile: 'false' | 'true'\n overwriteFile: 'false'\n tags: string\n useUniqueFileName: 'false'\n}\n\nexport type ImageKitUploadAuthorization = {\n clientUploadContext: ClientUploadContext\n filename: string\n token: string\n uploadPayload: ImageKitUploadPayload\n}\n\nexport type ImageKitUploadCompletion = {\n clientUploadContext: ClientUploadContext\n}\n\nexport const isImageKitReceiptContext = (value: unknown): value is ImageKitReceiptContext => {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n const context = value as Partial<ImageKitReceiptContext>\n\n return (\n (context._objectKey === undefined ||\n (typeof context._objectKey === 'string' && context._objectKey.length > 0)) &&\n (context.uploadTag === undefined ||\n (typeof context.uploadTag === 'string' && context.uploadTag.length > 0)) &&\n Boolean(context._objectKey || context.uploadTag || context.fileId) &&\n typeof context.collectionSlug === 'string' &&\n context.collectionSlug.length > 0 &&\n typeof context.expectedMimeType === 'string' &&\n context.expectedMimeType.length > 0 &&\n typeof context.expectedSize === 'number' &&\n Number.isSafeInteger(context.expectedSize) &&\n context.expectedSize >= 0 &&\n (context.fileId === undefined ||\n (typeof context.fileId === 'string' && context.fileId.length > 0)) &&\n typeof context.filename === 'string' &&\n context.filename.length > 0 &&\n typeof context.isPrivate === 'boolean' &&\n typeof context.prefix === 'string'\n )\n}\n"],"names":["imageKitUploadAuthPath","isImageKitReceiptContext","value","context","_objectKey","undefined","length","uploadTag","Boolean","fileId","collectionSlug","expectedMimeType","expectedSize","Number","isSafeInteger","filename","isPrivate","prefix"],"mappings":"AAEA,OAAO,MAAMA,yBAAyB,wBAAuB;AA8D7D,OAAO,MAAMC,2BAA2B,CAACC;IACvC,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU;QACvC,OAAO;IACT;IAEA,MAAMC,UAAUD;IAEhB,OACE,AAACC,CAAAA,QAAQC,UAAU,KAAKC,aACrB,OAAOF,QAAQC,UAAU,KAAK,YAAYD,QAAQC,UAAU,CAACE,MAAM,GAAG,CAAC,KACzEH,CAAAA,QAAQI,SAAS,KAAKF,aACpB,OAAOF,QAAQI,SAAS,KAAK,YAAYJ,QAAQI,SAAS,CAACD,MAAM,GAAG,CAAC,KACxEE,QAAQL,QAAQC,UAAU,IAAID,QAAQI,SAAS,IAAIJ,QAAQM,MAAM,KACjE,OAAON,QAAQO,cAAc,KAAK,YAClCP,QAAQO,cAAc,CAACJ,MAAM,GAAG,KAChC,OAAOH,QAAQQ,gBAAgB,KAAK,YACpCR,QAAQQ,gBAAgB,CAACL,MAAM,GAAG,KAClC,OAAOH,QAAQS,YAAY,KAAK,YAChCC,OAAOC,aAAa,CAACX,QAAQS,YAAY,KACzCT,QAAQS,YAAY,IAAI,KACvBT,CAAAA,QAAQM,MAAM,KAAKJ,aACjB,OAAOF,QAAQM,MAAM,KAAK,YAAYN,QAAQM,MAAM,CAACH,MAAM,GAAG,CAAC,KAClE,OAAOH,QAAQY,QAAQ,KAAK,YAC5BZ,QAAQY,QAAQ,CAACT,MAAM,GAAG,KAC1B,OAAOH,QAAQa,SAAS,KAAK,aAC7B,OAAOb,QAAQc,MAAM,KAAK;AAE9B,EAAC"}
1
+ {"version":3,"sources":["../src/clientUploads.ts"],"sourcesContent":["import type { UploadReference } from '@payloadcms/plugin-cloud-storage/types'\n\n/** @deprecated Use Payload 4's UploadReference and UploadInstructions types. */\nexport type ImageKitClientUploadContext = Partial<UploadReference>\n\n/** @deprecated The upload handler is configured by the storage capability. */\nexport type ImageKitUploadHandlerProps = {\n authPath: `/${string}`\n collectionSlug: string\n folder: string\n isPrivate?: boolean\n publicKey: string\n}\n\nexport type ImageKitUploadPayload = {\n checks: string\n fileName: string\n folder: string\n isPrivateFile: 'false' | 'true'\n overwriteFile: 'false'\n tags: string\n useUniqueFileName: 'false'\n}\n"],"names":[],"mappings":"AAcA,WAQC"}
@@ -1,2 +1,2 @@
1
1
  import type { ServerComponentProps } from 'payload';
2
- export declare const BeforeDashboardServer: (props: ServerComponentProps) => Promise<import("react").JSX.Element>;
2
+ export declare const BeforeDashboardServer: (props: ServerComponentProps) => Promise<import("react").JSX.Element | null>;
@@ -2,8 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import styles from './BeforeDashboardServer.module.css';
3
3
  export const BeforeDashboardServer = async (props)=>{
4
4
  const { payload } = props;
5
+ const collection = payload.config.collections.find((collection)=>String(collection.slug) === 'plugin-collection');
6
+ if (!collection) {
7
+ return null;
8
+ }
5
9
  const { docs } = await payload.find({
6
- collection: 'plugin-collection'
10
+ collection: collection.slug,
11
+ overrideAccess: true
7
12
  });
8
13
  return /*#__PURE__*/ _jsxs("div", {
9
14
  className: styles.wrapper,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/BeforeDashboardServer.tsx"],"sourcesContent":["import type { ServerComponentProps } from 'payload'\n\nimport styles from './BeforeDashboardServer.module.css'\n\nexport const BeforeDashboardServer = async (props: ServerComponentProps) => {\n const { payload } = props\n\n const { docs } = await payload.find({ collection: 'plugin-collection' })\n\n return (\n <div className={styles.wrapper}>\n <h1>Added by the plugin: Before Dashboard Server</h1>\n Docs from Local API:\n {docs.map((doc) => (\n <div key={doc.id}>{doc.id}</div>\n ))}\n </div>\n )\n}\n"],"names":["styles","BeforeDashboardServer","props","payload","docs","find","collection","div","className","wrapper","h1","map","doc","id"],"mappings":";AAEA,OAAOA,YAAY,qCAAoC;AAEvD,OAAO,MAAMC,wBAAwB,OAAOC;IAC1C,MAAM,EAAEC,OAAO,EAAE,GAAGD;IAEpB,MAAM,EAAEE,IAAI,EAAE,GAAG,MAAMD,QAAQE,IAAI,CAAC;QAAEC,YAAY;IAAoB;IAEtE,qBACE,MAACC;QAAIC,WAAWR,OAAOS,OAAO;;0BAC5B,KAACC;0BAAG;;YAAiD;YAEpDN,KAAKO,GAAG,CAAC,CAACC,oBACT,KAACL;8BAAkBK,IAAIC,EAAE;mBAAfD,IAAIC,EAAE;;;AAIxB,EAAC"}
1
+ {"version":3,"sources":["../../src/components/BeforeDashboardServer.tsx"],"sourcesContent":["import type { ServerComponentProps } from 'payload'\n\nimport styles from './BeforeDashboardServer.module.css'\n\nexport const BeforeDashboardServer = async (props: ServerComponentProps) => {\n const { payload } = props\n\n const collection = payload.config.collections.find(\n (collection) => String(collection.slug) === 'plugin-collection',\n )\n if (!collection) {\n return null\n }\n const { docs } = await payload.find({ collection: collection.slug, overrideAccess: true })\n\n return (\n <div className={styles.wrapper}>\n <h1>Added by the plugin: Before Dashboard Server</h1>\n Docs from Local API:\n {docs.map((doc) => (\n <div key={doc.id}>{doc.id}</div>\n ))}\n </div>\n )\n}\n"],"names":["styles","BeforeDashboardServer","props","payload","collection","config","collections","find","String","slug","docs","overrideAccess","div","className","wrapper","h1","map","doc","id"],"mappings":";AAEA,OAAOA,YAAY,qCAAoC;AAEvD,OAAO,MAAMC,wBAAwB,OAAOC;IAC1C,MAAM,EAAEC,OAAO,EAAE,GAAGD;IAEpB,MAAME,aAAaD,QAAQE,MAAM,CAACC,WAAW,CAACC,IAAI,CAChD,CAACH,aAAeI,OAAOJ,WAAWK,IAAI,MAAM;IAE9C,IAAI,CAACL,YAAY;QACf,OAAO;IACT;IACA,MAAM,EAAEM,IAAI,EAAE,GAAG,MAAMP,QAAQI,IAAI,CAAC;QAAEH,YAAYA,WAAWK,IAAI;QAAEE,gBAAgB;IAAK;IAExF,qBACE,MAACC;QAAIC,WAAWb,OAAOc,OAAO;;0BAC5B,KAACC;0BAAG;;YAAiD;YAEpDL,KAAKM,GAAG,CAAC,CAACC,oBACT,KAACL;8BAAkBK,IAAIC,EAAE;mBAAfD,IAAIC,EAAE;;;AAIxB,EAAC"}
@@ -1,8 +1,7 @@
1
- export declare const ImageKitUploadHandler: ({ children, collectionSlug, enabled, extra, prefix, serverHandlerPath, }: {
1
+ export declare const ImageKitUploadHandler: ({ children, collectionSlug, endpointPath, prefix, props, }: {
2
2
  children: import("react").ReactNode;
3
3
  collectionSlug: import("payload").UploadCollectionSlug;
4
- enabled?: boolean;
5
- extra: Record<string, unknown>;
4
+ endpointPath?: `/${string}`;
6
5
  prefix?: string;
7
- serverHandlerPath: `/${string}`;
6
+ props: Record<string, unknown>;
8
7
  }) => import("react").JSX.Element;
@@ -2,85 +2,26 @@
2
2
  import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client';
3
3
  import { formatAdminURL } from 'payload/shared';
4
4
  import { getClientImageIdentity } from '../clientImageIdentity.js';
5
- const uploadAPIURL = 'https://upload.imagekit.io/api/v2/files/upload';
6
- const readErrorMessage = async (response, fallback)=>{
7
- try {
8
- const result = await response.json();
9
- if (typeof result.message === 'string' && result.message.trim().length > 0) {
10
- return result.message;
11
- }
12
- const messages = result.errors?.map((error)=>error.message).filter((message)=>Boolean(message));
13
- if (messages?.length) {
14
- return messages.join(', ');
15
- }
16
- } catch {
17
- // Ignore non-JSON responses and fall back to the default message.
18
- }
19
- return fallback;
20
- };
21
5
  export const ImageKitUploadHandler = createClientUploadHandler({
22
- handler: async ({ apiRoute, collectionSlug, file, serverHandlerPath, serverURL, updateFilename })=>{
23
- const authURL = formatAdminURL({
24
- apiRoute,
25
- path: serverHandlerPath,
26
- serverURL
27
- });
6
+ name: 'imagekit',
7
+ handler: async ({ apiRoute, collectionSlug, data, docPrefix, endpointPath, file, serverURL, updateFilename })=>{
8
+ let authorization = data;
9
+ let uploadReference;
28
10
  const identity = await getClientImageIdentity(file);
29
- let authBody = {
30
- collectionSlug,
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;
71
- }
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, {
11
+ // Payload's initial instructions use the browser's MIME hint. Re-authorize
12
+ // mislabeled image bytes through the same shared endpoint, not a private API.
13
+ if (identity.filename !== file.name || identity.mimeType !== file.type) {
14
+ const response = await fetch(formatAdminURL({
15
+ apiRoute,
16
+ path: '/upload-instructions',
17
+ serverURL
18
+ }), {
79
19
  body: JSON.stringify({
80
- action: 'complete',
81
20
  collectionSlug,
82
- fileId: uploaded.fileId,
83
- signedReceipt: authorization.clientUploadContext.signedReceipt
21
+ docPrefix,
22
+ filename: identity.filename,
23
+ filesize: file.size,
24
+ mimeType: identity.mimeType
84
25
  }),
85
26
  credentials: 'include',
86
27
  headers: {
@@ -88,13 +29,61 @@ export const ImageKitUploadHandler = createClientUploadHandler({
88
29
  },
89
30
  method: 'POST'
90
31
  });
91
- if (!completionResponse.ok) {
92
- throw new Error(await readErrorMessage(completionResponse, 'Failed to verify ImageKit upload.'));
32
+ if (!response.ok) {
33
+ throw new Error('Failed to authorize detected image content.');
93
34
  }
94
- const completion = await completionResponse.json();
95
- return completion.clientUploadContext;
35
+ const instructions = await response.json();
36
+ if (instructions.type !== 'dispatch' || instructions.name !== 'imagekit') {
37
+ throw new Error('Expected ImageKit upload instructions.');
38
+ }
39
+ authorization = instructions.data;
40
+ uploadReference = instructions.file.uploadReference;
41
+ updateFilename(instructions.file.filename);
42
+ }
43
+ if (!endpointPath || typeof authorization.uploadReference?.signedReceipt !== 'string') {
44
+ throw new Error('ImageKit upload configuration changed. Refresh the page and try again.');
45
+ }
46
+ const formData = new FormData();
47
+ formData.set('file', file);
48
+ formData.set('token', authorization.token);
49
+ for (const [key, value] of Object.entries(authorization.uploadPayload)){
50
+ formData.set(key, value);
51
+ }
52
+ const response = await fetch('https://upload.imagekit.io/api/v2/files/upload', {
53
+ body: formData,
54
+ method: 'POST'
55
+ });
56
+ if (!response.ok) {
57
+ const error = await response.json().catch(()=>null);
58
+ throw new Error(error?.message || 'Failed to upload file to ImageKit.');
59
+ }
60
+ const uploaded = await response.json().catch(()=>null);
61
+ if (typeof uploaded?.fileId !== 'string' || !uploaded.fileId) {
62
+ throw new Error('ImageKit returned incomplete upload metadata.');
63
+ }
64
+ const completed = await fetch(formatAdminURL({
65
+ apiRoute,
66
+ path: endpointPath,
67
+ serverURL
68
+ }), {
69
+ body: JSON.stringify({
70
+ collectionSlug,
71
+ fileId: uploaded.fileId,
72
+ signedReceipt: authorization.uploadReference.signedReceipt
73
+ }),
74
+ credentials: 'include',
75
+ headers: {
76
+ 'Content-Type': 'application/json'
77
+ },
78
+ method: 'POST'
79
+ });
80
+ if (!completed.ok) {
81
+ const error = await completed.json().catch(()=>null);
82
+ throw new Error(error?.errors?.[0]?.message || 'Failed to verify the ImageKit upload.');
96
83
  }
97
- throw new Error('Could not find an available filename. Rename the file and try again.');
84
+ // The completion callback remembers a server-verified ID, not a browser-supplied proof.
85
+ // Payload keeps its original signed reference and owns the original/history lifecycle.
86
+ return uploadReference;
98
87
  }
99
88
  });
100
89
 
@@ -1 +1 @@
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
+ {"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport type { UploadInstructions } from 'payload'\n\nimport { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'\nimport { formatAdminURL } from 'payload/shared'\n\nimport { getClientImageIdentity } from '../clientImageIdentity.js'\n\nexport const ImageKitUploadHandler = createClientUploadHandler({\n name: 'imagekit',\n handler: async ({\n apiRoute,\n collectionSlug,\n data,\n docPrefix,\n endpointPath,\n file,\n serverURL,\n updateFilename,\n }) => {\n let authorization = data as {\n token: string\n uploadPayload: Record<string, string>\n uploadReference: Record<string, unknown>\n }\n let uploadReference: Record<string, unknown> | undefined\n const identity = await getClientImageIdentity(file)\n // Payload's initial instructions use the browser's MIME hint. Re-authorize\n // mislabeled image bytes through the same shared endpoint, not a private API.\n if (identity.filename !== file.name || identity.mimeType !== file.type) {\n const response = await fetch(\n formatAdminURL({ apiRoute, path: '/upload-instructions', serverURL }),\n {\n body: JSON.stringify({\n collectionSlug,\n docPrefix,\n filename: identity.filename,\n filesize: file.size,\n mimeType: identity.mimeType,\n }),\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n },\n )\n if (!response.ok) {\n throw new Error('Failed to authorize detected image content.')\n }\n const instructions = (await response.json()) as UploadInstructions\n if (instructions.type !== 'dispatch' || instructions.name !== 'imagekit') {\n throw new Error('Expected ImageKit upload instructions.')\n }\n authorization = instructions.data as typeof authorization\n uploadReference = instructions.file.uploadReference\n updateFilename(instructions.file.filename)\n }\n if (!endpointPath || typeof authorization.uploadReference?.signedReceipt !== 'string') {\n throw new Error('ImageKit upload configuration changed. Refresh the page and try again.')\n }\n const formData = new FormData()\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 const response = await fetch('https://upload.imagekit.io/api/v2/files/upload', {\n body: formData,\n method: 'POST',\n })\n if (!response.ok) {\n const error = (await response.json().catch(() => null)) as { message?: string } | null\n throw new Error(error?.message || 'Failed to upload file to ImageKit.')\n }\n const uploaded = (await response.json().catch(() => null)) as { fileId?: unknown } | null\n if (typeof uploaded?.fileId !== 'string' || !uploaded.fileId) {\n throw new Error('ImageKit returned incomplete upload metadata.')\n }\n const completed = await fetch(formatAdminURL({ apiRoute, path: endpointPath, serverURL }), {\n body: JSON.stringify({\n collectionSlug,\n fileId: uploaded.fileId,\n signedReceipt: authorization.uploadReference.signedReceipt,\n }),\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n })\n if (!completed.ok) {\n const error = (await completed.json().catch(() => null)) as {\n errors?: { message?: string }[]\n } | null\n throw new Error(error?.errors?.[0]?.message || 'Failed to verify the ImageKit upload.')\n }\n // The completion callback remembers a server-verified ID, not a browser-supplied proof.\n // Payload keeps its original signed reference and owns the original/history lifecycle.\n return uploadReference\n },\n})\n"],"names":["createClientUploadHandler","formatAdminURL","getClientImageIdentity","ImageKitUploadHandler","name","handler","apiRoute","collectionSlug","data","docPrefix","endpointPath","file","serverURL","updateFilename","authorization","uploadReference","identity","filename","mimeType","type","response","fetch","path","body","JSON","stringify","filesize","size","credentials","headers","method","ok","Error","instructions","json","signedReceipt","formData","FormData","set","token","key","value","Object","entries","uploadPayload","error","catch","message","uploaded","fileId","completed","errors"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { PayloadHandler, UploadInstructionsAccess } from 'payload';
2
+ import type { createImageKitFileStore } from '../imageKitFileStore.js';
3
+ export declare const completeImageKitUpload: ({ access, collections, files, }: {
4
+ access?: UploadInstructionsAccess;
5
+ collections: Record<string, {
6
+ isPrivate: boolean;
7
+ prefix: string;
8
+ }>;
9
+ files: ReturnType<typeof createImageKitFileStore>;
10
+ }) => PayloadHandler;
@@ -0,0 +1,84 @@
1
+ import { NotFoundError } from '@imagekit/nodejs';
2
+ import { buildStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities';
3
+ import path from 'node:path';
4
+ import { APIError, Forbidden } from 'payload';
5
+ import { assertClientUploadAccess, assertClientUploadAllowed, getOriginalFilename, verifyClientUploadReceipt } from 'payload/internal';
6
+ import { validateMimeType } from 'payload/shared';
7
+ export const completeImageKitUpload = ({ access = ({ req })=>Boolean(req.user), collections, files })=>async (req)=>{
8
+ if (!req.json) {
9
+ throw new APIError('Expected an ImageKit upload completion payload.', 400);
10
+ }
11
+ const body = await req.json().catch(()=>{
12
+ throw new APIError('Invalid ImageKit upload completion.', 400);
13
+ });
14
+ if (!body || typeof body.collectionSlug !== 'string' || !Object.hasOwn(collections, body.collectionSlug) || typeof body.fileId !== 'string' || !body.fileId.trim()) {
15
+ throw new APIError('Invalid ImageKit upload completion.', 400);
16
+ }
17
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Consumer-generated configs narrow UploadCollectionSlug.
18
+ const collectionSlug = body.collectionSlug;
19
+ await assertClientUploadAccess({
20
+ collectionSlug,
21
+ req
22
+ });
23
+ if (!await access({
24
+ collectionSlug,
25
+ req
26
+ })) {
27
+ throw new Forbidden(req.t);
28
+ }
29
+ const receipt = verifyClientUploadReceipt({
30
+ collectionSlug,
31
+ req,
32
+ signedReceipt: body.signedReceipt
33
+ });
34
+ const { isPrivate, prefix } = collections[collectionSlug];
35
+ const expectedPath = buildStoragePathData({
36
+ collectionPrefix: prefix,
37
+ docPrefix: path.posix.join(receipt.filePrefix, receipt._objectKey ?? ''),
38
+ filename: getOriginalFilename({
39
+ filename: receipt.filename
40
+ })
41
+ }).storageFilePath;
42
+ if (!receipt._objectKey || receipt.allowOverwrite || expectedPath !== receipt.storageFilePath || !expectedPath.startsWith(`${prefix}/`)) {
43
+ throw new APIError('Invalid ImageKit upload reference.', 400);
44
+ }
45
+ let file;
46
+ try {
47
+ file = await files.getByID({
48
+ fileId: body.fileId,
49
+ key: expectedPath
50
+ });
51
+ } catch (error) {
52
+ if (error instanceof NotFoundError) {
53
+ throw new APIError('ImageKit upload was not found.', 400);
54
+ }
55
+ throw error;
56
+ }
57
+ if (!file.mime || Boolean(file.isPrivateFile) !== isPrivate || !file.tags?.includes(`payload-upload-${receipt._objectKey}`)) {
58
+ throw new APIError('ImageKit upload did not match its authorization.', 400);
59
+ }
60
+ const collection = req.payload.collections[collectionSlug].config;
61
+ assertClientUploadAllowed({
62
+ collection,
63
+ filename: file.name,
64
+ mimeType: file.mime
65
+ });
66
+ if (typeof collection.upload === 'object' && !validateMimeType(file.mime, collection.upload.mimeTypes ?? [])) {
67
+ throw new APIError('ImageKit file type is not allowed for this collection.', 400);
68
+ }
69
+ // The browser's ID is only a lookup hint. Persist it only after checking authoritative state
70
+ // against the user-bound receipt. Save-time admission rechecks both the receipt and provider.
71
+ await files.remember({
72
+ fileId: file.fileId,
73
+ key: expectedPath,
74
+ req
75
+ });
76
+ return new Response(null, {
77
+ headers: {
78
+ 'Cache-Control': 'no-store'
79
+ },
80
+ status: 204
81
+ });
82
+ };
83
+
84
+ //# sourceMappingURL=completeImageKitUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/completeImageKitUpload.ts"],"sourcesContent":["import type { PayloadHandler, UploadCollectionSlug, UploadInstructionsAccess } from 'payload'\n\nimport { NotFoundError } from '@imagekit/nodejs'\nimport { buildStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities'\nimport path from 'node:path'\nimport { APIError, Forbidden } from 'payload'\nimport {\n assertClientUploadAccess,\n assertClientUploadAllowed,\n getOriginalFilename,\n verifyClientUploadReceipt,\n} from 'payload/internal'\nimport { validateMimeType } from 'payload/shared'\n\nimport type { createImageKitFileStore } from '../imageKitFileStore.js'\n\nexport const completeImageKitUpload =\n ({\n access = ({ req }) => Boolean(req.user),\n collections,\n files,\n }: {\n access?: UploadInstructionsAccess\n collections: Record<string, { isPrivate: boolean; prefix: string }>\n files: ReturnType<typeof createImageKitFileStore>\n }): PayloadHandler =>\n async (req) => {\n if (!req.json) {\n throw new APIError('Expected an ImageKit upload completion payload.', 400)\n }\n const body = (await req.json().catch(() => {\n throw new APIError('Invalid ImageKit upload completion.', 400)\n })) as {\n collectionSlug?: unknown\n fileId?: unknown\n signedReceipt?: unknown\n }\n if (\n !body ||\n typeof body.collectionSlug !== 'string' ||\n !Object.hasOwn(collections, body.collectionSlug) ||\n typeof body.fileId !== 'string' ||\n !body.fileId.trim()\n ) {\n throw new APIError('Invalid ImageKit upload completion.', 400)\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Consumer-generated configs narrow UploadCollectionSlug.\n const collectionSlug = body.collectionSlug as UploadCollectionSlug\n await assertClientUploadAccess({ collectionSlug, req })\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n const receipt = verifyClientUploadReceipt({\n collectionSlug,\n req,\n signedReceipt: body.signedReceipt,\n })\n const { isPrivate, prefix } = collections[collectionSlug]\n const expectedPath = buildStoragePathData({\n collectionPrefix: prefix,\n docPrefix: path.posix.join(receipt.filePrefix, receipt._objectKey ?? ''),\n filename: getOriginalFilename({ filename: receipt.filename }),\n }).storageFilePath\n if (\n !receipt._objectKey ||\n receipt.allowOverwrite ||\n expectedPath !== receipt.storageFilePath ||\n !expectedPath.startsWith(`${prefix}/`)\n ) {\n throw new APIError('Invalid ImageKit upload reference.', 400)\n }\n let file: Awaited<ReturnType<typeof files.getByID>>\n try {\n file = await files.getByID({ fileId: body.fileId, key: expectedPath })\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new APIError('ImageKit upload was not found.', 400)\n }\n throw error\n }\n if (\n !file.mime ||\n Boolean(file.isPrivateFile) !== isPrivate ||\n !file.tags?.includes(`payload-upload-${receipt._objectKey}`)\n ) {\n throw new APIError('ImageKit upload did not match its authorization.', 400)\n }\n const collection = req.payload.collections[collectionSlug].config\n assertClientUploadAllowed({\n collection,\n filename: file.name,\n mimeType: file.mime,\n })\n if (\n typeof collection.upload === 'object' &&\n !validateMimeType(file.mime, collection.upload.mimeTypes ?? [])\n ) {\n throw new APIError('ImageKit file type is not allowed for this collection.', 400)\n }\n // The browser's ID is only a lookup hint. Persist it only after checking authoritative state\n // against the user-bound receipt. Save-time admission rechecks both the receipt and provider.\n await files.remember({ fileId: file.fileId, key: expectedPath, req })\n return new Response(null, { headers: { 'Cache-Control': 'no-store' }, status: 204 })\n }\n"],"names":["NotFoundError","buildStoragePathData","path","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","getOriginalFilename","verifyClientUploadReceipt","validateMimeType","completeImageKitUpload","access","req","Boolean","user","collections","files","json","body","catch","collectionSlug","Object","hasOwn","fileId","trim","t","receipt","signedReceipt","isPrivate","prefix","expectedPath","collectionPrefix","docPrefix","posix","join","filePrefix","_objectKey","filename","storageFilePath","allowOverwrite","startsWith","file","getByID","key","error","mime","isPrivateFile","tags","includes","collection","payload","config","name","mimeType","upload","mimeTypes","remember","Response","headers","status"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { GenerateUploadInstructions, UploadInstructionsAccess } from 'payload';
2
+ /** ImageKit enforces these constraints before accepting any provider-direct bytes. */
3
+ export declare const getUploadChecks: ({ allowedMimeTypes, allowRestrictedFileTypes, filesize, }: {
4
+ allowedMimeTypes: string[];
5
+ allowRestrictedFileTypes: boolean;
6
+ filesize: number;
7
+ }) => string;
8
+ export declare const createImageKitUploadInstructions: ({ access, isPrivate, prefix, privateKey, publicKey, }: {
9
+ access?: UploadInstructionsAccess;
10
+ isPrivate: boolean;
11
+ prefix: string;
12
+ privateKey: string;
13
+ publicKey: string;
14
+ }) => GenerateUploadInstructions;