@payloadcms/plugin-cloud-storage 3.89.0 → 3.90.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.
- package/README.md +2 -0
- package/dist/exports/client.d.ts +1 -1
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +1 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/utilities.d.ts +3 -2
- package/dist/exports/utilities.d.ts.map +1 -1
- package/dist/exports/utilities.js +3 -2
- package/dist/exports/utilities.js.map +1 -1
- package/dist/fields/getFields.d.ts.map +1 -1
- package/dist/fields/getFields.js +28 -1
- package/dist/fields/getFields.js.map +1 -1
- package/dist/hooks/afterChange.d.ts +3 -1
- package/dist/hooks/afterChange.d.ts.map +1 -1
- package/dist/hooks/afterChange.js +56 -9
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/afterChange.spec.js +150 -0
- package/dist/hooks/afterChange.spec.js.map +1 -0
- package/dist/hooks/afterDelete.d.ts +3 -1
- package/dist/hooks/afterDelete.d.ts.map +1 -1
- package/dist/hooks/afterDelete.js +16 -2
- package/dist/hooks/afterDelete.js.map +1 -1
- package/dist/hooks/afterRead.d.ts.map +1 -1
- package/dist/hooks/afterRead.js +13 -2
- package/dist/hooks/afterRead.js.map +1 -1
- package/dist/hooks/beforeChange.d.ts.map +1 -1
- package/dist/hooks/beforeChange.js +10 -1
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/hooks/normalizeUploadPrefix.d.ts +11 -0
- package/dist/hooks/normalizeUploadPrefix.d.ts.map +1 -0
- package/dist/hooks/normalizeUploadPrefix.js +42 -0
- package/dist/hooks/normalizeUploadPrefix.js.map +1 -0
- package/dist/hooks/normalizeUploadPrefix.spec.js +154 -0
- package/dist/hooks/normalizeUploadPrefix.spec.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +15 -6
- package/dist/plugin.js.map +1 -1
- package/dist/plugin.spec.js +166 -0
- package/dist/plugin.spec.js.map +1 -0
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts +15 -0
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts.map +1 -0
- package/dist/utilities/assertReceiptMatchesFileKey.js +31 -0
- package/dist/utilities/assertReceiptMatchesFileKey.js.map +1 -0
- package/dist/utilities/buildPrefixWithObjectKey.d.ts +8 -0
- package/dist/utilities/buildPrefixWithObjectKey.d.ts.map +1 -0
- package/dist/utilities/buildPrefixWithObjectKey.js +13 -0
- package/dist/utilities/buildPrefixWithObjectKey.js.map +1 -0
- package/dist/utilities/buildStoragePathData.d.ts +55 -0
- package/dist/utilities/buildStoragePathData.d.ts.map +1 -0
- package/dist/utilities/buildStoragePathData.js +69 -0
- package/dist/utilities/buildStoragePathData.js.map +1 -0
- package/dist/utilities/{getFileKey.spec.js → buildStoragePathData.spec.js} +116 -36
- package/dist/utilities/buildStoragePathData.spec.js.map +1 -0
- package/dist/utilities/getFilePrefix.d.ts +27 -11
- package/dist/utilities/getFilePrefix.d.ts.map +1 -1
- package/dist/utilities/getFilePrefix.js +75 -31
- package/dist/utilities/getFilePrefix.js.map +1 -1
- package/dist/utilities/getFilePrefix.spec.js +221 -35
- package/dist/utilities/getFilePrefix.spec.js.map +1 -1
- package/dist/utilities/resolveSignedURLKey.d.ts +11 -0
- package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
- package/dist/utilities/resolveSignedURLKey.js +43 -8
- package/dist/utilities/resolveSignedURLKey.js.map +1 -1
- package/dist/utilities/resolveSignedURLKey.spec.js +56 -0
- package/dist/utilities/resolveSignedURLKey.spec.js.map +1 -0
- package/dist/utilities/sanitizePrefix.d.ts +2 -2
- package/dist/utilities/sanitizePrefix.js +4 -4
- package/dist/utilities/sanitizePrefix.js.map +1 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts +20 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts.map +1 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js +24 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js.map +1 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js +108 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js.map +1 -0
- package/package.json +4 -4
- package/dist/utilities/getFileKey.d.ts +0 -22
- package/dist/utilities/getFileKey.d.ts.map +0 -1
- package/dist/utilities/getFileKey.js +0 -23
- package/dist/utilities/getFileKey.js.map +0 -1
- package/dist/utilities/getFileKey.spec.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveSignedURLKey.d.ts","sourceRoot":"","sources":["../../src/utilities/resolveSignedURLKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAM7C,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,cAAc,CAAA;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"resolveSignedURLKey.d.ts","sourceRoot":"","sources":["../../src/utilities/resolveSignedURLKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAM7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAItD,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,cAAc,CAAA;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,gBAAqB,EACrB,cAAc,EACd,SAAS,EACT,QAAQ,EACR,GAAG,EACH,oBAA4B,GAC7B,EAAE,IAAI;;;;;;;GAiDN"}
|
|
@@ -1,29 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { createClientUploadReceipt, getSafeFileName } from 'payload/internal';
|
|
3
|
+
import { getSanitizedUploadFilename } from 'payload/shared';
|
|
4
|
+
import { buildUploadPrefix, buildUploadStoragePathData } from './buildStoragePathData.js';
|
|
3
5
|
/**
|
|
4
6
|
* Resolves the storage key for a clientUploads signed-URL request, deduping
|
|
5
7
|
* the filename via {@link getSafeFileName} so a duplicate upload does not
|
|
6
8
|
* overwrite an existing blob.
|
|
7
9
|
*
|
|
10
|
+
* A per-upload segment is persisted in `_objectKey` (not `prefix`), so the stored `prefix`
|
|
11
|
+
* and `filename` stay clean.
|
|
12
|
+
*
|
|
8
13
|
* The resolved `sanitizedFilename` is returned so the browser-side handler
|
|
9
14
|
* can update the form via `updateFilename`.
|
|
15
|
+
*
|
|
16
|
+
* The semantic prefix is resolved with {@link buildUploadPrefix} and the final key with
|
|
17
|
+
* {@link buildUploadStoragePathData}, so the receipt only ever signs a key beneath the configured
|
|
18
|
+
* collection prefix.
|
|
10
19
|
*/ export async function resolveSignedURLKey({ collectionPrefix = '', collectionSlug, docPrefix, filename, req, useCompositePrefixes = false }) {
|
|
20
|
+
// Sanitize with the same helper generateFileData uses for the DB filename so the storage key
|
|
21
|
+
// and doc.filename stay in sync (#16694).
|
|
11
22
|
const sanitizedFilename = await getSafeFileName({
|
|
12
23
|
collectionSlug,
|
|
13
|
-
desiredFilename: filename,
|
|
14
|
-
prefix: docPrefix,
|
|
24
|
+
desiredFilename: getSanitizedUploadFilename(filename),
|
|
15
25
|
req
|
|
16
26
|
});
|
|
17
|
-
const
|
|
27
|
+
const rawBaseDocPrefix = useCompositePrefixes ? docPrefix : docPrefix || collectionPrefix;
|
|
28
|
+
const { sanitizedDocPrefix } = buildUploadPrefix({
|
|
18
29
|
collectionPrefix,
|
|
19
|
-
docPrefix,
|
|
30
|
+
docPrefix: rawBaseDocPrefix,
|
|
31
|
+
useCompositePrefixes
|
|
32
|
+
});
|
|
33
|
+
const _objectKey = randomUUID();
|
|
34
|
+
// Persisted in `_objectKey`, keeping `prefix` semantic.
|
|
35
|
+
const keyedDocPrefix = sanitizedDocPrefix ? `${sanitizedDocPrefix}/${_objectKey}` : _objectKey;
|
|
36
|
+
const { storageFilePath } = buildUploadStoragePathData({
|
|
37
|
+
collectionPrefix,
|
|
38
|
+
docPrefix: keyedDocPrefix,
|
|
20
39
|
filename: sanitizedFilename,
|
|
21
40
|
useCompositePrefixes
|
|
22
41
|
});
|
|
42
|
+
const context = {
|
|
43
|
+
_objectKey,
|
|
44
|
+
prefix: sanitizedDocPrefix
|
|
45
|
+
};
|
|
46
|
+
const clientUploadContext = {
|
|
47
|
+
...context,
|
|
48
|
+
signedReceipt: createClientUploadReceipt({
|
|
49
|
+
collectionSlug,
|
|
50
|
+
context,
|
|
51
|
+
filename: sanitizedFilename,
|
|
52
|
+
req
|
|
53
|
+
})
|
|
54
|
+
};
|
|
23
55
|
return {
|
|
24
|
-
|
|
56
|
+
_objectKey,
|
|
57
|
+
clientUploadContext,
|
|
58
|
+
fileKey: storageFilePath,
|
|
25
59
|
sanitizedDocPrefix,
|
|
26
|
-
sanitizedFilename
|
|
60
|
+
sanitizedFilename,
|
|
61
|
+
storageFilePath
|
|
27
62
|
};
|
|
28
63
|
}
|
|
29
64
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/resolveSignedURLKey.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { getSafeFileName } from 'payload/internal'\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/resolveSignedURLKey.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { randomUUID } from 'crypto'\nimport { createClientUploadReceipt, getSafeFileName } from 'payload/internal'\nimport { getSanitizedUploadFilename } from 'payload/shared'\n\nimport type { ClientUploadContext } from '../types.js'\n\nimport { buildUploadPrefix, buildUploadStoragePathData } from './buildStoragePathData.js'\n\ntype Args = {\n collectionPrefix?: string\n collectionSlug: string\n docPrefix?: string\n filename: string\n req: PayloadRequest\n useCompositePrefixes?: boolean\n}\n\n/**\n * Resolves the storage key for a clientUploads signed-URL request, deduping\n * the filename via {@link getSafeFileName} so a duplicate upload does not\n * overwrite an existing blob.\n *\n * A per-upload segment is persisted in `_objectKey` (not `prefix`), so the stored `prefix`\n * and `filename` stay clean.\n *\n * The resolved `sanitizedFilename` is returned so the browser-side handler\n * can update the form via `updateFilename`.\n *\n * The semantic prefix is resolved with {@link buildUploadPrefix} and the final key with\n * {@link buildUploadStoragePathData}, so the receipt only ever signs a key beneath the configured\n * collection prefix.\n */\nexport async function resolveSignedURLKey({\n collectionPrefix = '',\n collectionSlug,\n docPrefix,\n filename,\n req,\n useCompositePrefixes = false,\n}: Args) {\n // Sanitize with the same helper generateFileData uses for the DB filename so the storage key\n // and doc.filename stay in sync (#16694).\n const sanitizedFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: getSanitizedUploadFilename(filename),\n req,\n })\n\n const rawBaseDocPrefix = useCompositePrefixes ? docPrefix : docPrefix || collectionPrefix\n const { sanitizedDocPrefix } = buildUploadPrefix({\n collectionPrefix,\n docPrefix: rawBaseDocPrefix,\n useCompositePrefixes,\n })\n\n const _objectKey = randomUUID()\n // Persisted in `_objectKey`, keeping `prefix` semantic.\n const keyedDocPrefix = sanitizedDocPrefix ? `${sanitizedDocPrefix}/${_objectKey}` : _objectKey\n\n const { storageFilePath } = buildUploadStoragePathData({\n collectionPrefix,\n docPrefix: keyedDocPrefix,\n filename: sanitizedFilename,\n useCompositePrefixes,\n })\n\n const context = {\n _objectKey,\n prefix: sanitizedDocPrefix,\n }\n const clientUploadContext: ClientUploadContext = {\n ...context,\n signedReceipt: createClientUploadReceipt({\n collectionSlug,\n context,\n filename: sanitizedFilename,\n req,\n }),\n }\n\n return {\n _objectKey,\n clientUploadContext,\n fileKey: storageFilePath,\n sanitizedDocPrefix,\n sanitizedFilename,\n storageFilePath,\n }\n}\n"],"names":["randomUUID","createClientUploadReceipt","getSafeFileName","getSanitizedUploadFilename","buildUploadPrefix","buildUploadStoragePathData","resolveSignedURLKey","collectionPrefix","collectionSlug","docPrefix","filename","req","useCompositePrefixes","sanitizedFilename","desiredFilename","rawBaseDocPrefix","sanitizedDocPrefix","_objectKey","keyedDocPrefix","storageFilePath","context","prefix","clientUploadContext","signedReceipt","fileKey"],"mappings":"AAEA,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,yBAAyB,EAAEC,eAAe,QAAQ,mBAAkB;AAC7E,SAASC,0BAA0B,QAAQ,iBAAgB;AAI3D,SAASC,iBAAiB,EAAEC,0BAA0B,QAAQ,4BAA2B;AAWzF;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,oBAAoB,EACxCC,mBAAmB,EAAE,EACrBC,cAAc,EACdC,SAAS,EACTC,QAAQ,EACRC,GAAG,EACHC,uBAAuB,KAAK,EACvB;IACL,6FAA6F;IAC7F,0CAA0C;IAC1C,MAAMC,oBAAoB,MAAMX,gBAAgB;QAC9CM;QACAM,iBAAiBX,2BAA2BO;QAC5CC;IACF;IAEA,MAAMI,mBAAmBH,uBAAuBH,YAAYA,aAAaF;IACzE,MAAM,EAAES,kBAAkB,EAAE,GAAGZ,kBAAkB;QAC/CG;QACAE,WAAWM;QACXH;IACF;IAEA,MAAMK,aAAajB;IACnB,wDAAwD;IACxD,MAAMkB,iBAAiBF,qBAAqB,GAAGA,mBAAmB,CAAC,EAAEC,YAAY,GAAGA;IAEpF,MAAM,EAAEE,eAAe,EAAE,GAAGd,2BAA2B;QACrDE;QACAE,WAAWS;QACXR,UAAUG;QACVD;IACF;IAEA,MAAMQ,UAAU;QACdH;QACAI,QAAQL;IACV;IACA,MAAMM,sBAA2C;QAC/C,GAAGF,OAAO;QACVG,eAAetB,0BAA0B;YACvCO;YACAY;YACAV,UAAUG;YACVF;QACF;IACF;IAEA,OAAO;QACLM;QACAK;QACAE,SAASL;QACTH;QACAH;QACAM;IACF;AACF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createClientUploadReceipt, getSafeFileName } from 'payload/internal';
|
|
3
|
+
import { resolveSignedURLKey } from './resolveSignedURLKey.js';
|
|
4
|
+
vi.mock('payload/internal', ()=>({
|
|
5
|
+
createClientUploadReceipt: vi.fn(()=>'receipt'),
|
|
6
|
+
getSafeFileName: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
describe('resolveSignedURLKey', ()=>{
|
|
9
|
+
beforeEach(()=>{
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it('should deduplicate within the resolved document prefix', async ()=>{
|
|
13
|
+
vi.mocked(getSafeFileName).mockImplementation(async ({ desiredFilename })=>{
|
|
14
|
+
expect(desiredFilename).toBe('photo.jpg');
|
|
15
|
+
return 'photo-1.jpg';
|
|
16
|
+
});
|
|
17
|
+
const result = await resolveSignedURLKey({
|
|
18
|
+
collectionPrefix: 'media',
|
|
19
|
+
collectionSlug: 'media',
|
|
20
|
+
docPrefix: 'media-archive',
|
|
21
|
+
filename: 'photo.jpg',
|
|
22
|
+
req: {}
|
|
23
|
+
});
|
|
24
|
+
expect(result.sanitizedDocPrefix).toBe('media/media-archive');
|
|
25
|
+
expect(result.fileKey).toMatch(/^media\/media-archive\/[0-9a-f-]+\/photo-1\.jpg$/);
|
|
26
|
+
expect(result.sanitizedFilename).toBe('photo-1.jpg');
|
|
27
|
+
expect(result._objectKey).toMatch(/^[0-9a-f-]+$/);
|
|
28
|
+
expect(result.clientUploadContext).toMatchObject({
|
|
29
|
+
_objectKey: result._objectKey,
|
|
30
|
+
prefix: 'media/media-archive',
|
|
31
|
+
signedReceipt: 'receipt'
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('should mint the receipt for a prefix contained by the collection prefix', async ()=>{
|
|
35
|
+
vi.mocked(getSafeFileName).mockResolvedValue('photo-1.jpg');
|
|
36
|
+
const { _objectKey, sanitizedDocPrefix } = await resolveSignedURLKey({
|
|
37
|
+
collectionPrefix: 'media',
|
|
38
|
+
collectionSlug: 'media',
|
|
39
|
+
docPrefix: 'media-archive',
|
|
40
|
+
filename: 'photo.jpg',
|
|
41
|
+
req: {}
|
|
42
|
+
});
|
|
43
|
+
expect(sanitizedDocPrefix).toBe('media/media-archive');
|
|
44
|
+
expect(createClientUploadReceipt).toHaveBeenCalledWith({
|
|
45
|
+
collectionSlug: 'media',
|
|
46
|
+
context: {
|
|
47
|
+
_objectKey,
|
|
48
|
+
prefix: sanitizedDocPrefix
|
|
49
|
+
},
|
|
50
|
+
filename: 'photo-1.jpg',
|
|
51
|
+
req: {}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=resolveSignedURLKey.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/resolveSignedURLKey.spec.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport { createClientUploadReceipt, getSafeFileName } from 'payload/internal'\n\nimport { resolveSignedURLKey } from './resolveSignedURLKey.js'\n\nvi.mock('payload/internal', () => ({\n createClientUploadReceipt: vi.fn(() => 'receipt'),\n getSafeFileName: vi.fn(),\n}))\n\ndescribe('resolveSignedURLKey', () => {\n beforeEach(() => {\n vi.clearAllMocks()\n })\n\n it('should deduplicate within the resolved document prefix', async () => {\n vi.mocked(getSafeFileName).mockImplementation(async ({ desiredFilename }) => {\n expect(desiredFilename).toBe('photo.jpg')\n return 'photo-1.jpg'\n })\n\n const result = await resolveSignedURLKey({\n collectionPrefix: 'media',\n collectionSlug: 'media',\n docPrefix: 'media-archive',\n filename: 'photo.jpg',\n req: {} as PayloadRequest,\n })\n\n expect(result.sanitizedDocPrefix).toBe('media/media-archive')\n expect(result.fileKey).toMatch(/^media\\/media-archive\\/[0-9a-f-]+\\/photo-1\\.jpg$/)\n expect(result.sanitizedFilename).toBe('photo-1.jpg')\n expect(result._objectKey).toMatch(/^[0-9a-f-]+$/)\n expect(result.clientUploadContext).toMatchObject({\n _objectKey: result._objectKey,\n prefix: 'media/media-archive',\n signedReceipt: 'receipt',\n })\n })\n\n it('should mint the receipt for a prefix contained by the collection prefix', async () => {\n vi.mocked(getSafeFileName).mockResolvedValue('photo-1.jpg')\n\n const { _objectKey, sanitizedDocPrefix } = await resolveSignedURLKey({\n collectionPrefix: 'media',\n collectionSlug: 'media',\n docPrefix: 'media-archive',\n filename: 'photo.jpg',\n req: {} as PayloadRequest,\n })\n\n expect(sanitizedDocPrefix).toBe('media/media-archive')\n expect(createClientUploadReceipt).toHaveBeenCalledWith({\n collectionSlug: 'media',\n context: { _objectKey, prefix: sanitizedDocPrefix },\n filename: 'photo-1.jpg',\n req: {},\n })\n })\n})\n"],"names":["beforeEach","describe","expect","it","vi","createClientUploadReceipt","getSafeFileName","resolveSignedURLKey","mock","fn","clearAllMocks","mocked","mockImplementation","desiredFilename","toBe","result","collectionPrefix","collectionSlug","docPrefix","filename","req","sanitizedDocPrefix","fileKey","toMatch","sanitizedFilename","_objectKey","clientUploadContext","toMatchObject","prefix","signedReceipt","mockResolvedValue","toHaveBeenCalledWith","context"],"mappings":"AAEA,SAASA,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAE7D,SAASC,yBAAyB,EAAEC,eAAe,QAAQ,mBAAkB;AAE7E,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9DH,GAAGI,IAAI,CAAC,oBAAoB,IAAO,CAAA;QACjCH,2BAA2BD,GAAGK,EAAE,CAAC,IAAM;QACvCH,iBAAiBF,GAAGK,EAAE;IACxB,CAAA;AAEAR,SAAS,uBAAuB;IAC9BD,WAAW;QACTI,GAAGM,aAAa;IAClB;IAEAP,GAAG,0DAA0D;QAC3DC,GAAGO,MAAM,CAACL,iBAAiBM,kBAAkB,CAAC,OAAO,EAAEC,eAAe,EAAE;YACtEX,OAAOW,iBAAiBC,IAAI,CAAC;YAC7B,OAAO;QACT;QAEA,MAAMC,SAAS,MAAMR,oBAAoB;YACvCS,kBAAkB;YAClBC,gBAAgB;YAChBC,WAAW;YACXC,UAAU;YACVC,KAAK,CAAC;QACR;QAEAlB,OAAOa,OAAOM,kBAAkB,EAAEP,IAAI,CAAC;QACvCZ,OAAOa,OAAOO,OAAO,EAAEC,OAAO,CAAC;QAC/BrB,OAAOa,OAAOS,iBAAiB,EAAEV,IAAI,CAAC;QACtCZ,OAAOa,OAAOU,UAAU,EAAEF,OAAO,CAAC;QAClCrB,OAAOa,OAAOW,mBAAmB,EAAEC,aAAa,CAAC;YAC/CF,YAAYV,OAAOU,UAAU;YAC7BG,QAAQ;YACRC,eAAe;QACjB;IACF;IAEA1B,GAAG,2EAA2E;QAC5EC,GAAGO,MAAM,CAACL,iBAAiBwB,iBAAiB,CAAC;QAE7C,MAAM,EAAEL,UAAU,EAAEJ,kBAAkB,EAAE,GAAG,MAAMd,oBAAoB;YACnES,kBAAkB;YAClBC,gBAAgB;YAChBC,WAAW;YACXC,UAAU;YACVC,KAAK,CAAC;QACR;QAEAlB,OAAOmB,oBAAoBP,IAAI,CAAC;QAChCZ,OAAOG,2BAA2B0B,oBAAoB,CAAC;YACrDd,gBAAgB;YAChBe,SAAS;gBAAEP;gBAAYG,QAAQP;YAAmB;YAClDF,UAAU;YACVC,KAAK,CAAC;QACR;IACF;AACF"}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Decodes URI components once (so query-style `%2F` becomes `/`), rejects
|
|
5
5
|
* values that still contain percent-encodings after decoding (e.g. `%252f`),
|
|
6
|
-
* then normalizes slashes, drops `.` / `..` segments,
|
|
7
|
-
* and
|
|
6
|
+
* then removes control characters, normalizes slashes, drops `.` / `..` segments,
|
|
7
|
+
* and strips leading slashes.
|
|
8
8
|
*/
|
|
9
9
|
export declare function sanitizePrefix(prefix: string): string;
|
|
10
10
|
//# sourceMappingURL=sanitizePrefix.d.ts.map
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Decodes URI components once (so query-style `%2F` becomes `/`), rejects
|
|
5
5
|
* values that still contain percent-encodings after decoding (e.g. `%252f`),
|
|
6
|
-
* then normalizes slashes, drops `.` / `..` segments,
|
|
7
|
-
* and
|
|
6
|
+
* then removes control characters, normalizes slashes, drops `.` / `..` segments,
|
|
7
|
+
* and strips leading slashes.
|
|
8
8
|
*/ export function sanitizePrefix(prefix) {
|
|
9
9
|
let decodedPrefix;
|
|
10
10
|
try {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
if (/%[0-9a-f]{2}/i.test(decodedPrefix)) {
|
|
17
17
|
return '';
|
|
18
18
|
}
|
|
19
|
-
return decodedPrefix
|
|
20
|
-
.replace(/[\x00-\x1f\x80-\x9f]/g, '');
|
|
19
|
+
return decodedPrefix// eslint-disable-next-line no-control-regex
|
|
20
|
+
.replace(/[\x00-\x1f\x80-\x9f]/g, '').replace(/\\/g, '/').split('/').filter((segment)=>segment && segment !== '..' && segment !== '.').join('/').replace(/^\/+/, '');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
//# sourceMappingURL=sanitizePrefix.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/sanitizePrefix.ts"],"sourcesContent":["/**\n * Normalizes a storage prefix for use in object keys and URLs.\n *\n * Decodes URI components once (so query-style `%2F` becomes `/`), rejects\n * values that still contain percent-encodings after decoding (e.g. `%252f`),\n * then normalizes slashes, drops `.` / `..` segments
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/sanitizePrefix.ts"],"sourcesContent":["/**\n * Normalizes a storage prefix for use in object keys and URLs.\n *\n * Decodes URI components once (so query-style `%2F` becomes `/`), rejects\n * values that still contain percent-encodings after decoding (e.g. `%252f`),\n * then removes control characters, normalizes slashes, drops `.` / `..` segments,\n * and strips leading slashes.\n */\nexport function sanitizePrefix(prefix: string): string {\n let decodedPrefix: string\n\n try {\n decodedPrefix = decodeURIComponent(prefix)\n } catch {\n return ''\n }\n\n // Reject multi-encoded values (e.g. `%252f`) by allowing only a single decode pass.\n if (/%[0-9a-f]{2}/i.test(decodedPrefix)) {\n return ''\n }\n\n return (\n decodedPrefix\n // eslint-disable-next-line no-control-regex\n .replace(/[\\x00-\\x1f\\x80-\\x9f]/g, '')\n .replace(/\\\\/g, '/')\n .split('/')\n .filter((segment) => segment && segment !== '..' && segment !== '.')\n .join('/')\n .replace(/^\\/+/, '')\n )\n}\n"],"names":["sanitizePrefix","prefix","decodedPrefix","decodeURIComponent","test","replace","split","filter","segment","join"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,SAASA,eAAeC,MAAc;IAC3C,IAAIC;IAEJ,IAAI;QACFA,gBAAgBC,mBAAmBF;IACrC,EAAE,OAAM;QACN,OAAO;IACT;IAEA,oFAAoF;IACpF,IAAI,gBAAgBG,IAAI,CAACF,gBAAgB;QACvC,OAAO;IACT;IAEA,OACEA,aACE,4CAA4C;KAC3CG,OAAO,CAAC,yBAAyB,IACjCA,OAAO,CAAC,OAAO,KACfC,KAAK,CAAC,KACNC,MAAM,CAAC,CAACC,UAAYA,WAAWA,YAAY,QAAQA,YAAY,KAC/DC,IAAI,CAAC,KACLJ,OAAO,CAAC,QAAQ;AAEvB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { ClientUploadReceipt } from 'payload/internal';
|
|
3
|
+
type Args = {
|
|
4
|
+
collectionPrefix?: string;
|
|
5
|
+
collectionSlug: string;
|
|
6
|
+
expectedFileKey: string;
|
|
7
|
+
req: PayloadRequest;
|
|
8
|
+
signedReceipt: string;
|
|
9
|
+
useCompositePrefixes?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Authenticates a client-upload receipt and asserts it authorizes writing to
|
|
13
|
+
* `expectedFileKey`, returning the verified receipt. Combines
|
|
14
|
+
* {@link verifyClientUploadReceipt} (authentication + collection binding) with
|
|
15
|
+
* {@link assertReceiptMatchesFileKey} (storage-key binding) so an adapter binds
|
|
16
|
+
* the receipt to its request in a single call.
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifyClientUploadReceiptForFileKey({ collectionPrefix, collectionSlug, expectedFileKey, req, signedReceipt, useCompositePrefixes, }: Args): ClientUploadReceipt;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=verifyClientUploadReceiptForFileKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyClientUploadReceiptForFileKey.d.ts","sourceRoot":"","sources":["../../src/utilities/verifyClientUploadReceiptForFileKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAM3D,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,GAAG,EAAE,cAAc,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,EAClD,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,GAAG,EACH,aAAa,EACb,oBAAoB,GACrB,EAAE,IAAI,GAAG,mBAAmB,CAM5B"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { verifyClientUploadReceipt } from 'payload/internal';
|
|
2
|
+
import { assertReceiptMatchesFileKey } from './assertReceiptMatchesFileKey.js';
|
|
3
|
+
/**
|
|
4
|
+
* Authenticates a client-upload receipt and asserts it authorizes writing to
|
|
5
|
+
* `expectedFileKey`, returning the verified receipt. Combines
|
|
6
|
+
* {@link verifyClientUploadReceipt} (authentication + collection binding) with
|
|
7
|
+
* {@link assertReceiptMatchesFileKey} (storage-key binding) so an adapter binds
|
|
8
|
+
* the receipt to its request in a single call.
|
|
9
|
+
*/ export function verifyClientUploadReceiptForFileKey({ collectionPrefix, collectionSlug, expectedFileKey, req, signedReceipt, useCompositePrefixes }) {
|
|
10
|
+
const receipt = verifyClientUploadReceipt({
|
|
11
|
+
collectionSlug,
|
|
12
|
+
req,
|
|
13
|
+
signedReceipt
|
|
14
|
+
});
|
|
15
|
+
assertReceiptMatchesFileKey({
|
|
16
|
+
collectionPrefix,
|
|
17
|
+
expectedFileKey,
|
|
18
|
+
receipt,
|
|
19
|
+
useCompositePrefixes
|
|
20
|
+
});
|
|
21
|
+
return receipt;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=verifyClientUploadReceiptForFileKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/verifyClientUploadReceiptForFileKey.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport type { ClientUploadReceipt } from 'payload/internal'\n\nimport { verifyClientUploadReceipt } from 'payload/internal'\n\nimport { assertReceiptMatchesFileKey } from './assertReceiptMatchesFileKey.js'\n\ntype Args = {\n collectionPrefix?: string\n collectionSlug: string\n expectedFileKey: string\n req: PayloadRequest\n signedReceipt: string\n useCompositePrefixes?: boolean\n}\n\n/**\n * Authenticates a client-upload receipt and asserts it authorizes writing to\n * `expectedFileKey`, returning the verified receipt. Combines\n * {@link verifyClientUploadReceipt} (authentication + collection binding) with\n * {@link assertReceiptMatchesFileKey} (storage-key binding) so an adapter binds\n * the receipt to its request in a single call.\n */\nexport function verifyClientUploadReceiptForFileKey({\n collectionPrefix,\n collectionSlug,\n expectedFileKey,\n req,\n signedReceipt,\n useCompositePrefixes,\n}: Args): ClientUploadReceipt {\n const receipt = verifyClientUploadReceipt({ collectionSlug, req, signedReceipt })\n\n assertReceiptMatchesFileKey({ collectionPrefix, expectedFileKey, receipt, useCompositePrefixes })\n\n return receipt\n}\n"],"names":["verifyClientUploadReceipt","assertReceiptMatchesFileKey","verifyClientUploadReceiptForFileKey","collectionPrefix","collectionSlug","expectedFileKey","req","signedReceipt","useCompositePrefixes","receipt"],"mappings":"AAGA,SAASA,yBAAyB,QAAQ,mBAAkB;AAE5D,SAASC,2BAA2B,QAAQ,mCAAkC;AAW9E;;;;;;CAMC,GACD,OAAO,SAASC,oCAAoC,EAClDC,gBAAgB,EAChBC,cAAc,EACdC,eAAe,EACfC,GAAG,EACHC,aAAa,EACbC,oBAAoB,EACf;IACL,MAAMC,UAAUT,0BAA0B;QAAEI;QAAgBE;QAAKC;IAAc;IAE/EN,4BAA4B;QAAEE;QAAkBE;QAAiBI;QAASD;IAAqB;IAE/F,OAAOC;AACT"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createClientUploadReceipt } from 'payload/internal';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { verifyClientUploadReceiptForFileKey } from './verifyClientUploadReceiptForFileKey.js';
|
|
4
|
+
const req = {
|
|
5
|
+
payload: {
|
|
6
|
+
secret: 'test-secret'
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
const issueReceipt = ({ collectionSlug = 'media', filename = 'photo.png', prefix })=>createClientUploadReceipt({
|
|
10
|
+
collectionSlug,
|
|
11
|
+
context: {
|
|
12
|
+
prefix
|
|
13
|
+
},
|
|
14
|
+
filename,
|
|
15
|
+
req
|
|
16
|
+
});
|
|
17
|
+
const verify = ({ expectedFileKey, signedReceipt, useCompositePrefixes })=>verifyClientUploadReceiptForFileKey({
|
|
18
|
+
collectionPrefix: 'media',
|
|
19
|
+
collectionSlug: 'media',
|
|
20
|
+
expectedFileKey,
|
|
21
|
+
req,
|
|
22
|
+
signedReceipt,
|
|
23
|
+
useCompositePrefixes
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* This guard is what lets adapters accept a caller-supplied storage key: the key is
|
|
27
|
+
* recomputed from the signed prefix and filename, so a receipt issued for one location
|
|
28
|
+
* cannot be replayed against another.
|
|
29
|
+
*/ describe('verifyClientUploadReceiptForFileKey', ()=>{
|
|
30
|
+
it('should accept the key the receipt was issued for', ()=>{
|
|
31
|
+
const signedReceipt = issueReceipt({
|
|
32
|
+
prefix: 'media/documents'
|
|
33
|
+
});
|
|
34
|
+
expect(verify({
|
|
35
|
+
expectedFileKey: 'media/documents/photo.png',
|
|
36
|
+
signedReceipt
|
|
37
|
+
}).filename).toBe('photo.png');
|
|
38
|
+
});
|
|
39
|
+
it.each([
|
|
40
|
+
[
|
|
41
|
+
'a sibling of the collection prefix',
|
|
42
|
+
'media-archive/photo.png'
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
'another collection',
|
|
46
|
+
'invoices/photo.png'
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
'another document prefix in the same collection',
|
|
50
|
+
'media/other-doc/photo.png'
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
'another filename under the issued prefix',
|
|
54
|
+
'media/documents/payroll.pdf'
|
|
55
|
+
]
|
|
56
|
+
])('should reject %s', (_case, expectedFileKey)=>{
|
|
57
|
+
const signedReceipt = issueReceipt({
|
|
58
|
+
prefix: 'media/documents'
|
|
59
|
+
});
|
|
60
|
+
expect(()=>verify({
|
|
61
|
+
expectedFileKey,
|
|
62
|
+
signedReceipt
|
|
63
|
+
})).toThrow('Client upload reference does not match this request.');
|
|
64
|
+
});
|
|
65
|
+
it('should reject a receipt issued for another collection', ()=>{
|
|
66
|
+
const signedReceipt = issueReceipt({
|
|
67
|
+
collectionSlug: 'invoices',
|
|
68
|
+
prefix: 'media/documents'
|
|
69
|
+
});
|
|
70
|
+
expect(()=>verify({
|
|
71
|
+
expectedFileKey: 'media/documents/photo.png',
|
|
72
|
+
signedReceipt
|
|
73
|
+
})).toThrow('Invalid or expired client upload reference.');
|
|
74
|
+
});
|
|
75
|
+
it('should reject a receipt whose prefix is not a string', ()=>{
|
|
76
|
+
const signedReceipt = issueReceipt({
|
|
77
|
+
prefix: {
|
|
78
|
+
nested: true
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
expect(()=>verify({
|
|
82
|
+
expectedFileKey: 'media/photo.png',
|
|
83
|
+
signedReceipt
|
|
84
|
+
})).toThrow('Client upload reference does not match this request.');
|
|
85
|
+
});
|
|
86
|
+
it('should reject a tampered signature', ()=>{
|
|
87
|
+
const signedReceipt = issueReceipt({
|
|
88
|
+
prefix: 'media/documents'
|
|
89
|
+
});
|
|
90
|
+
const [encodedReceipt] = signedReceipt.split('.');
|
|
91
|
+
expect(()=>verify({
|
|
92
|
+
expectedFileKey: 'media/documents/photo.png',
|
|
93
|
+
signedReceipt: `${encodedReceipt}.not-the-signature`
|
|
94
|
+
})).toThrow();
|
|
95
|
+
});
|
|
96
|
+
it('should recompute the key in composite mode', ()=>{
|
|
97
|
+
const signedReceipt = issueReceipt({
|
|
98
|
+
prefix: 'documents'
|
|
99
|
+
});
|
|
100
|
+
expect(verify({
|
|
101
|
+
expectedFileKey: 'media/documents/photo.png',
|
|
102
|
+
signedReceipt,
|
|
103
|
+
useCompositePrefixes: true
|
|
104
|
+
}).filename).toBe('photo.png');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=verifyClientUploadReceiptForFileKey.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/verifyClientUploadReceiptForFileKey.spec.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { createClientUploadReceipt } from 'payload/internal'\nimport { describe, expect, it } from 'vitest'\n\nimport { verifyClientUploadReceiptForFileKey } from './verifyClientUploadReceiptForFileKey.js'\n\nconst req = { payload: { secret: 'test-secret' } } as unknown as PayloadRequest\n\nconst issueReceipt = ({\n collectionSlug = 'media',\n filename = 'photo.png',\n prefix,\n}: {\n collectionSlug?: string\n filename?: string\n prefix: unknown\n}) =>\n createClientUploadReceipt({\n collectionSlug,\n context: { prefix },\n filename,\n req,\n })\n\nconst verify = ({\n expectedFileKey,\n signedReceipt,\n useCompositePrefixes,\n}: {\n expectedFileKey: string\n signedReceipt: string\n useCompositePrefixes?: boolean\n}) =>\n verifyClientUploadReceiptForFileKey({\n collectionPrefix: 'media',\n collectionSlug: 'media',\n expectedFileKey,\n req,\n signedReceipt,\n useCompositePrefixes,\n })\n\n/**\n * This guard is what lets adapters accept a caller-supplied storage key: the key is\n * recomputed from the signed prefix and filename, so a receipt issued for one location\n * cannot be replayed against another.\n */\ndescribe('verifyClientUploadReceiptForFileKey', () => {\n it('should accept the key the receipt was issued for', () => {\n const signedReceipt = issueReceipt({ prefix: 'media/documents' })\n\n expect(verify({ expectedFileKey: 'media/documents/photo.png', signedReceipt }).filename).toBe(\n 'photo.png',\n )\n })\n\n it.each([\n ['a sibling of the collection prefix', 'media-archive/photo.png'],\n ['another collection', 'invoices/photo.png'],\n ['another document prefix in the same collection', 'media/other-doc/photo.png'],\n ['another filename under the issued prefix', 'media/documents/payroll.pdf'],\n ])('should reject %s', (_case, expectedFileKey) => {\n const signedReceipt = issueReceipt({ prefix: 'media/documents' })\n\n expect(() => verify({ expectedFileKey, signedReceipt })).toThrow(\n 'Client upload reference does not match this request.',\n )\n })\n\n it('should reject a receipt issued for another collection', () => {\n const signedReceipt = issueReceipt({ collectionSlug: 'invoices', prefix: 'media/documents' })\n\n expect(() => verify({ expectedFileKey: 'media/documents/photo.png', signedReceipt })).toThrow(\n 'Invalid or expired client upload reference.',\n )\n })\n\n it('should reject a receipt whose prefix is not a string', () => {\n const signedReceipt = issueReceipt({ prefix: { nested: true } })\n\n expect(() => verify({ expectedFileKey: 'media/photo.png', signedReceipt })).toThrow(\n 'Client upload reference does not match this request.',\n )\n })\n\n it('should reject a tampered signature', () => {\n const signedReceipt = issueReceipt({ prefix: 'media/documents' })\n const [encodedReceipt] = signedReceipt.split('.')\n\n expect(() =>\n verify({\n expectedFileKey: 'media/documents/photo.png',\n signedReceipt: `${encodedReceipt}.not-the-signature`,\n }),\n ).toThrow()\n })\n\n it('should recompute the key in composite mode', () => {\n const signedReceipt = issueReceipt({ prefix: 'documents' })\n\n expect(\n verify({\n expectedFileKey: 'media/documents/photo.png',\n signedReceipt,\n useCompositePrefixes: true,\n }).filename,\n ).toBe('photo.png')\n })\n})\n"],"names":["createClientUploadReceipt","describe","expect","it","verifyClientUploadReceiptForFileKey","req","payload","secret","issueReceipt","collectionSlug","filename","prefix","context","verify","expectedFileKey","signedReceipt","useCompositePrefixes","collectionPrefix","toBe","each","_case","toThrow","nested","encodedReceipt","split"],"mappings":"AAEA,SAASA,yBAAyB,QAAQ,mBAAkB;AAC5D,SAASC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,mCAAmC,QAAQ,2CAA0C;AAE9F,MAAMC,MAAM;IAAEC,SAAS;QAAEC,QAAQ;IAAc;AAAE;AAEjD,MAAMC,eAAe,CAAC,EACpBC,iBAAiB,OAAO,EACxBC,WAAW,WAAW,EACtBC,MAAM,EAKP,GACCX,0BAA0B;QACxBS;QACAG,SAAS;YAAED;QAAO;QAClBD;QACAL;IACF;AAEF,MAAMQ,SAAS,CAAC,EACdC,eAAe,EACfC,aAAa,EACbC,oBAAoB,EAKrB,GACCZ,oCAAoC;QAClCa,kBAAkB;QAClBR,gBAAgB;QAChBK;QACAT;QACAU;QACAC;IACF;AAEF;;;;CAIC,GACDf,SAAS,uCAAuC;IAC9CE,GAAG,oDAAoD;QACrD,MAAMY,gBAAgBP,aAAa;YAAEG,QAAQ;QAAkB;QAE/DT,OAAOW,OAAO;YAAEC,iBAAiB;YAA6BC;QAAc,GAAGL,QAAQ,EAAEQ,IAAI,CAC3F;IAEJ;IAEAf,GAAGgB,IAAI,CAAC;QACN;YAAC;YAAsC;SAA0B;QACjE;YAAC;YAAsB;SAAqB;QAC5C;YAAC;YAAkD;SAA4B;QAC/E;YAAC;YAA4C;SAA8B;KAC5E,EAAE,oBAAoB,CAACC,OAAON;QAC7B,MAAMC,gBAAgBP,aAAa;YAAEG,QAAQ;QAAkB;QAE/DT,OAAO,IAAMW,OAAO;gBAAEC;gBAAiBC;YAAc,IAAIM,OAAO,CAC9D;IAEJ;IAEAlB,GAAG,yDAAyD;QAC1D,MAAMY,gBAAgBP,aAAa;YAAEC,gBAAgB;YAAYE,QAAQ;QAAkB;QAE3FT,OAAO,IAAMW,OAAO;gBAAEC,iBAAiB;gBAA6BC;YAAc,IAAIM,OAAO,CAC3F;IAEJ;IAEAlB,GAAG,wDAAwD;QACzD,MAAMY,gBAAgBP,aAAa;YAAEG,QAAQ;gBAAEW,QAAQ;YAAK;QAAE;QAE9DpB,OAAO,IAAMW,OAAO;gBAAEC,iBAAiB;gBAAmBC;YAAc,IAAIM,OAAO,CACjF;IAEJ;IAEAlB,GAAG,sCAAsC;QACvC,MAAMY,gBAAgBP,aAAa;YAAEG,QAAQ;QAAkB;QAC/D,MAAM,CAACY,eAAe,GAAGR,cAAcS,KAAK,CAAC;QAE7CtB,OAAO,IACLW,OAAO;gBACLC,iBAAiB;gBACjBC,eAAe,GAAGQ,eAAe,kBAAkB,CAAC;YACtD,IACAF,OAAO;IACX;IAEAlB,GAAG,8CAA8C;QAC/C,MAAMY,gBAAgBP,aAAa;YAAEG,QAAQ;QAAY;QAEzDT,OACEW,OAAO;YACLC,iBAAiB;YACjBC;YACAC,sBAAsB;QACxB,GAAGN,QAAQ,EACXQ,IAAI,CAAC;IACT;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payloadcms/plugin-cloud-storage",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.90.1",
|
|
4
4
|
"description": "The official cloud storage plugin for Payload CMS",
|
|
5
5
|
"homepage": "https://payloadcms.com",
|
|
6
6
|
"repository": {
|
|
@@ -51,18 +51,18 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"find-node-modules": "^2.1.3",
|
|
53
53
|
"range-parser": "^1.2.1",
|
|
54
|
-
"@payloadcms/ui": "3.
|
|
54
|
+
"@payloadcms/ui": "3.90.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/find-node-modules": "^2.1.2",
|
|
58
58
|
"@types/react": "19.2.14",
|
|
59
59
|
"@types/react-dom": "19.2.3",
|
|
60
|
-
"payload": "3.
|
|
60
|
+
"payload": "3.90.1"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": "^19.0.1 || ^19.1.2 || ^19.2.1",
|
|
64
64
|
"react-dom": "^19.0.1 || ^19.1.2 || ^19.2.1",
|
|
65
|
-
"payload": "3.
|
|
65
|
+
"payload": "3.90.1"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
type GetFileKeyArgs = {
|
|
2
|
-
collectionPrefix?: string;
|
|
3
|
-
docPrefix?: string;
|
|
4
|
-
filename: string;
|
|
5
|
-
useCompositePrefixes?: boolean;
|
|
6
|
-
};
|
|
7
|
-
type GetFileKeyResult = {
|
|
8
|
-
fileKey: string;
|
|
9
|
-
sanitizedCollectionPrefix: string;
|
|
10
|
-
sanitizedDocPrefix: string;
|
|
11
|
-
sanitizedFilename: string;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Computes the file key (path) for storage.
|
|
15
|
-
*
|
|
16
|
-
* In non-composite mode (useCompositePrefixes: false), docPrefix overrides collectionPrefix.
|
|
17
|
-
* In composite mode (useCompositePrefixes: true), both are combined.
|
|
18
|
-
* Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.
|
|
19
|
-
*/
|
|
20
|
-
export declare function getFileKey({ collectionPrefix, docPrefix, filename, useCompositePrefixes, }: GetFileKeyArgs): GetFileKeyResult;
|
|
21
|
-
export {};
|
|
22
|
-
//# sourceMappingURL=getFileKey.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"getFileKey.d.ts","sourceRoot":"","sources":["../../src/utilities/getFileKey.ts"],"names":[],"mappings":"AAKA,KAAK,cAAc,GAAG;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED,KAAK,gBAAgB,GAAG;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,yBAAyB,EAAE,MAAM,CAAA;IACjC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EACzB,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,oBAA4B,GAC7B,EAAE,cAAc,GAAG,gBAAgB,CAenC"}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { sanitizeFilename } from 'payload/shared';
|
|
3
|
-
import { sanitizePrefix } from './sanitizePrefix.js';
|
|
4
|
-
/**
|
|
5
|
-
* Computes the file key (path) for storage.
|
|
6
|
-
*
|
|
7
|
-
* In non-composite mode (useCompositePrefixes: false), docPrefix overrides collectionPrefix.
|
|
8
|
-
* In composite mode (useCompositePrefixes: true), both are combined.
|
|
9
|
-
* Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.
|
|
10
|
-
*/ export function getFileKey({ collectionPrefix, docPrefix, filename, useCompositePrefixes = false }) {
|
|
11
|
-
const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '');
|
|
12
|
-
const safeDocPrefix = sanitizePrefix(docPrefix || '');
|
|
13
|
-
const safeFilename = sanitizeFilename(filename);
|
|
14
|
-
const fileKey = useCompositePrefixes ? path.posix.join(safeCollectionPrefix, safeDocPrefix, safeFilename) : path.posix.join(safeDocPrefix || safeCollectionPrefix, safeFilename);
|
|
15
|
-
return {
|
|
16
|
-
fileKey,
|
|
17
|
-
sanitizedCollectionPrefix: safeCollectionPrefix,
|
|
18
|
-
sanitizedDocPrefix: safeDocPrefix,
|
|
19
|
-
sanitizedFilename: safeFilename
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
//# sourceMappingURL=getFileKey.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/getFileKey.ts"],"sourcesContent":["import path from 'path'\nimport { sanitizeFilename } from 'payload/shared'\n\nimport { sanitizePrefix } from './sanitizePrefix.js'\n\ntype GetFileKeyArgs = {\n collectionPrefix?: string\n docPrefix?: string\n filename: string\n useCompositePrefixes?: boolean\n}\n\ntype GetFileKeyResult = {\n fileKey: string\n sanitizedCollectionPrefix: string\n sanitizedDocPrefix: string\n sanitizedFilename: string\n}\n\n/**\n * Computes the file key (path) for storage.\n *\n * In non-composite mode (useCompositePrefixes: false), docPrefix overrides collectionPrefix.\n * In composite mode (useCompositePrefixes: true), both are combined.\n * Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.\n */\nexport function getFileKey({\n collectionPrefix,\n docPrefix,\n filename,\n useCompositePrefixes = false,\n}: GetFileKeyArgs): GetFileKeyResult {\n const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '')\n const safeDocPrefix = sanitizePrefix(docPrefix || '')\n const safeFilename = sanitizeFilename(filename)\n\n const fileKey = useCompositePrefixes\n ? path.posix.join(safeCollectionPrefix, safeDocPrefix, safeFilename)\n : path.posix.join(safeDocPrefix || safeCollectionPrefix, safeFilename)\n\n return {\n fileKey,\n sanitizedCollectionPrefix: safeCollectionPrefix,\n sanitizedDocPrefix: safeDocPrefix,\n sanitizedFilename: safeFilename,\n }\n}\n"],"names":["path","sanitizeFilename","sanitizePrefix","getFileKey","collectionPrefix","docPrefix","filename","useCompositePrefixes","safeCollectionPrefix","safeDocPrefix","safeFilename","fileKey","posix","join","sanitizedCollectionPrefix","sanitizedDocPrefix","sanitizedFilename"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AACvB,SAASC,gBAAgB,QAAQ,iBAAgB;AAEjD,SAASC,cAAc,QAAQ,sBAAqB;AAgBpD;;;;;;CAMC,GACD,OAAO,SAASC,WAAW,EACzBC,gBAAgB,EAChBC,SAAS,EACTC,QAAQ,EACRC,uBAAuB,KAAK,EACb;IACf,MAAMC,uBAAuBN,eAAeE,oBAAoB;IAChE,MAAMK,gBAAgBP,eAAeG,aAAa;IAClD,MAAMK,eAAeT,iBAAiBK;IAEtC,MAAMK,UAAUJ,uBACZP,KAAKY,KAAK,CAACC,IAAI,CAACL,sBAAsBC,eAAeC,gBACrDV,KAAKY,KAAK,CAACC,IAAI,CAACJ,iBAAiBD,sBAAsBE;IAE3D,OAAO;QACLC;QACAG,2BAA2BN;QAC3BO,oBAAoBN;QACpBO,mBAAmBN;IACrB;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/getFileKey.spec.ts"],"sourcesContent":["import { getFileKey } from './getFileKey.js'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('getFileKey', () => {\n describe('non-composite mode (useCompositePrefixes: false)', () => {\n it('should use docPrefix when provided, ignoring collectionPrefix', () => {\n const result = getFileKey({\n collectionPrefix: 'collection',\n docPrefix: 'document',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'document/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: 'document',\n sanitizedFilename: 'test.png',\n })\n })\n\n it('should fallback to collectionPrefix when docPrefix is empty', () => {\n const result = getFileKey({\n collectionPrefix: 'collection',\n docPrefix: '',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'collection/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n })\n\n it('should fallback to collectionPrefix when docPrefix is undefined', () => {\n const result = getFileKey({\n collectionPrefix: 'collection',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'collection/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n })\n\n it('should return only filename when both prefixes are empty', () => {\n const result = getFileKey({\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n })\n })\n\n describe('composite mode (useCompositePrefixes: true)', () => {\n it('should combine collectionPrefix and docPrefix', () => {\n const result = getFileKey({\n collectionPrefix: 'collection',\n docPrefix: 'document',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'collection/document/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: 'document',\n sanitizedFilename: 'test.png',\n })\n })\n\n it('should work with only collectionPrefix', () => {\n const result = getFileKey({\n collectionPrefix: 'collection',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'collection/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n })\n\n it('should work with only docPrefix', () => {\n const result = getFileKey({\n docPrefix: 'document',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'document/test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: 'document',\n sanitizedFilename: 'test.png',\n })\n })\n\n it('should return only filename when both prefixes are empty', () => {\n const result = getFileKey({\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n })\n })\n\n describe('sanitization', () => {\n it('should remove path traversal segments from collectionPrefix', () => {\n const result = getFileKey({\n collectionPrefix: '../../../etc',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'etc/test.png',\n sanitizedCollectionPrefix: 'etc',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n\n it('should remove path traversal segments from docPrefix', () => {\n const result = getFileKey({\n docPrefix: 'a/../../outside',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'a/outside/test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: 'a/outside',\n sanitizedFilename: 'test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n\n it('should remove control characters from prefixes', () => {\n const result = getFileKey({\n collectionPrefix: 'test\\x00\\x1fprefix',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n expect(result).toEqual({\n fileKey: 'testprefix/test.png',\n sanitizedCollectionPrefix: 'testprefix',\n sanitizedDocPrefix: '',\n sanitizedFilename: 'test.png',\n })\n expect(result.fileKey).not.toMatch(/[\\x00-\\x1f]/)\n })\n\n it('should sanitize both prefixes in composite mode', () => {\n const result = getFileKey({\n collectionPrefix: '../collection',\n docPrefix: '../../doc',\n filename: 'test.png',\n useCompositePrefixes: true,\n })\n expect(result).toEqual({\n fileKey: 'collection/doc/test.png',\n sanitizedCollectionPrefix: 'collection',\n sanitizedDocPrefix: 'doc',\n sanitizedFilename: 'test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n })\n})\n"],"names":["getFileKey","describe","expect","it","result","collectionPrefix","docPrefix","filename","useCompositePrefixes","toEqual","fileKey","sanitizedCollectionPrefix","sanitizedDocPrefix","sanitizedFilename","not","toContain","toMatch"],"mappings":"AAAA,SAASA,UAAU,QAAQ,kBAAiB;AAC5C,SAASC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7CF,SAAS,cAAc;IACrBA,SAAS,oDAAoD;QAC3DE,GAAG,iEAAiE;YAClE,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBC,WAAW;gBACXC,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;QAEAV,GAAG,+DAA+D;YAChE,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBC,WAAW;gBACXC,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;QAEAV,GAAG,mEAAmE;YACpE,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBE,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;QAEAV,GAAG,4DAA4D;YAC7D,MAAMC,SAASJ,WAAW;gBACxBO,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;IACF;IAEAZ,SAAS,+CAA+C;QACtDE,GAAG,iDAAiD;YAClD,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBC,WAAW;gBACXC,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;QAEAV,GAAG,0CAA0C;YAC3C,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBE,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;QAEAV,GAAG,mCAAmC;YACpC,MAAMC,SAASJ,WAAW;gBACxBM,WAAW;gBACXC,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;QAEAV,GAAG,4DAA4D;YAC7D,MAAMC,SAASJ,WAAW;gBACxBO,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;QACF;IACF;IAEAZ,SAAS,gBAAgB;QACvBE,GAAG,+DAA+D;YAChE,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBE,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;YACAX,OAAOE,OAAOM,OAAO,EAAEI,GAAG,CAACC,SAAS,CAAC;QACvC;QAEAZ,GAAG,wDAAwD;YACzD,MAAMC,SAASJ,WAAW;gBACxBM,WAAW;gBACXC,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;YACAX,OAAOE,OAAOM,OAAO,EAAEI,GAAG,CAACC,SAAS,CAAC;QACvC;QAEAZ,GAAG,kDAAkD;YACnD,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBE,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;YACAX,OAAOE,OAAOM,OAAO,EAAEI,GAAG,CAACE,OAAO,CAAC;QACrC;QAEAb,GAAG,mDAAmD;YACpD,MAAMC,SAASJ,WAAW;gBACxBK,kBAAkB;gBAClBC,WAAW;gBACXC,UAAU;gBACVC,sBAAsB;YACxB;YACAN,OAAOE,QAAQK,OAAO,CAAC;gBACrBC,SAAS;gBACTC,2BAA2B;gBAC3BC,oBAAoB;gBACpBC,mBAAmB;YACrB;YACAX,OAAOE,OAAOM,OAAO,EAAEI,GAAG,CAACC,SAAS,CAAC;QACvC;IACF;AACF"}
|