@humaan/payload-storage-imagekit 2.9.0 → 3.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,40 @@ 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
+ Because v4 receipts carry storage paths rather than provider IDs, direct admission
111
+ uses bounded retries when ImageKit's asset listing has not yet caught up.
112
+
113
+ Run `pnpm exec vitest run` for mock-provider REST/MongoDB, client transport, and
114
+ folder-migration coverage. Live provider tests require explicitly configured
115
+ `IMAGEKIT_INTEGRATION_*` credentials and are otherwise skipped. The integration
116
+ suite was migrated from the removed v3 endpoint/receipt protocol to the v4 REST
117
+ boundary; the obsolete context-only verification helper is no longer shipped.
118
+
119
+ The folder migration utility covers current/legacy records; migrating every
120
+ archived v4 representation between accounts needs a separate history-aware plan.
121
+
73
122
  ## Exports
74
123
 
75
124
  - `imagekitStorage` (primary)
@@ -89,9 +138,7 @@ export default buildConfig({
89
138
  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
139
 
91
140
  ```ts
92
- import {
93
- withImageKitTransformation,
94
- } from '@humaan/payload-storage-imagekit/image'
141
+ import { withImageKitTransformation } from '@humaan/payload-storage-imagekit/image'
95
142
 
96
143
  const src = withImageKitTransformation(media.imagekit.url, {
97
144
  focalX: media.focalX,
@@ -318,6 +365,12 @@ paths are flattened into the destination collection folder; collisions are repor
318
365
  history and account-specific custom metadata schemas are not transferred. Run one migration per
319
366
  destination at a time; concurrent runs may report a conflict recoverable on a subsequent run.
320
367
 
368
+ On MongoDB, migration updates use the database adapter directly so the old `_objectKey` can be
369
+ removed in the same write as the new storage reference. Writing `null` would collide with the
370
+ legacy sparse unique index. These metadata updates bypass Payload validation and change hooks,
371
+ including cache revalidation; refresh application caches after the migration. Already uploaded
372
+ files from an interrupted 2.9.0 migration can be recovered by rerunning with this fix.
373
+
321
374
  Run a migration script:
322
375
 
323
376
  ```bash
@@ -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, 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,34 @@ 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.');
34
+ }
35
+ const instructions = await response.json();
36
+ if (instructions.type !== 'dispatch' || instructions.name !== 'imagekit') {
37
+ throw new Error('Expected ImageKit upload instructions.');
93
38
  }
94
- const completion = await completionResponse.json();
95
- return completion.clientUploadContext;
39
+ authorization = instructions.data;
40
+ uploadReference = instructions.file.uploadReference;
41
+ updateFilename(instructions.file.filename);
42
+ }
43
+ const formData = new FormData();
44
+ formData.set('file', file);
45
+ formData.set('token', authorization.token);
46
+ for (const [key, value] of Object.entries(authorization.uploadPayload)){
47
+ formData.set(key, value);
48
+ }
49
+ const response = await fetch('https://upload.imagekit.io/api/v2/files/upload', {
50
+ body: formData,
51
+ method: 'POST'
52
+ });
53
+ if (!response.ok) {
54
+ const error = await response.json().catch(()=>null);
55
+ throw new Error(error?.message || 'Failed to upload file to ImageKit.');
96
56
  }
97
- throw new Error('Could not find an available filename. Rename the file and try again.');
57
+ // Payload retains the signed uploadReference supplied by /upload-instructions.
58
+ // Provider identity is resolved and verified on the server, never trusted from the browser.
59
+ return uploadReference;
98
60
  }
99
61
  });
100
62
 
@@ -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 file,\n serverURL,\n updateFilename,\n }) => {\n let authorization = data as {\n token: string\n uploadPayload: Record<string, string>\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 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 // Payload retains the signed uploadReference supplied by /upload-instructions.\n // Provider identity is resolved and verified on the server, never trusted from the browser.\n return uploadReference\n },\n})\n"],"names":["createClientUploadHandler","formatAdminURL","getClientImageIdentity","ImageKitUploadHandler","name","handler","apiRoute","collectionSlug","data","docPrefix","file","serverURL","updateFilename","authorization","uploadReference","identity","filename","mimeType","type","response","fetch","path","body","JSON","stringify","filesize","size","credentials","headers","method","ok","Error","instructions","json","formData","FormData","set","token","key","value","Object","entries","uploadPayload","error","catch","message"],"mappings":"AAAA;AAIA,SAASA,yBAAyB,QAAQ,0CAAyC;AACnF,SAASC,cAAc,QAAQ,iBAAgB;AAE/C,SAASC,sBAAsB,QAAQ,4BAA2B;AAElE,OAAO,MAAMC,wBAAwBH,0BAA0B;IAC7DI,MAAM;IACNC,SAAS,OAAO,EACdC,QAAQ,EACRC,cAAc,EACdC,IAAI,EACJC,SAAS,EACTC,IAAI,EACJC,SAAS,EACTC,cAAc,EACf;QACC,IAAIC,gBAAgBL;QAIpB,IAAIM;QACJ,MAAMC,WAAW,MAAMb,uBAAuBQ;QAC9C,2EAA2E;QAC3E,8EAA8E;QAC9E,IAAIK,SAASC,QAAQ,KAAKN,KAAKN,IAAI,IAAIW,SAASE,QAAQ,KAAKP,KAAKQ,IAAI,EAAE;YACtE,MAAMC,WAAW,MAAMC,MACrBnB,eAAe;gBAAEK;gBAAUe,MAAM;gBAAwBV;YAAU,IACnE;gBACEW,MAAMC,KAAKC,SAAS,CAAC;oBACnBjB;oBACAE;oBACAO,UAAUD,SAASC,QAAQ;oBAC3BS,UAAUf,KAAKgB,IAAI;oBACnBT,UAAUF,SAASE,QAAQ;gBAC7B;gBACAU,aAAa;gBACbC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,QAAQ;YACV;YAEF,IAAI,CAACV,SAASW,EAAE,EAAE;gBAChB,MAAM,IAAIC,MAAM;YAClB;YACA,MAAMC,eAAgB,MAAMb,SAASc,IAAI;YACzC,IAAID,aAAad,IAAI,KAAK,cAAcc,aAAa5B,IAAI,KAAK,YAAY;gBACxE,MAAM,IAAI2B,MAAM;YAClB;YACAlB,gBAAgBmB,aAAaxB,IAAI;YACjCM,kBAAkBkB,aAAatB,IAAI,CAACI,eAAe;YACnDF,eAAeoB,aAAatB,IAAI,CAACM,QAAQ;QAC3C;QACA,MAAMkB,WAAW,IAAIC;QACrBD,SAASE,GAAG,CAAC,QAAQ1B;QACrBwB,SAASE,GAAG,CAAC,SAASvB,cAAcwB,KAAK;QACzC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAAC5B,cAAc6B,aAAa,EAAG;YACtER,SAASE,GAAG,CAACE,KAAKC;QACpB;QACA,MAAMpB,WAAW,MAAMC,MAAM,kDAAkD;YAC7EE,MAAMY;YACNL,QAAQ;QACV;QACA,IAAI,CAACV,SAASW,EAAE,EAAE;YAChB,MAAMa,QAAS,MAAMxB,SAASc,IAAI,GAAGW,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIb,MAAMY,OAAOE,WAAW;QACpC;QACA,+EAA+E;QAC/E,4FAA4F;QAC5F,OAAO/B;IACT;AACF,GAAE"}
@@ -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;
@@ -0,0 +1,116 @@
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
+ },
107
+ file: {
108
+ filename: safeFilename,
109
+ mimeType,
110
+ size: filesize,
111
+ uploadReference
112
+ }
113
+ };
114
+ };
115
+
116
+ //# 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 },\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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"}