@payloadcms/plugin-cloud-storage 3.91.0-internal.6e3dc85 → 4.0.0-beta.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/LICENSE.md +1 -1
- package/README.md +4 -4
- package/dist/client/createClientUploadHandler.d.ts +8 -7
- package/dist/client/createClientUploadHandler.d.ts.map +1 -1
- package/dist/client/createClientUploadHandler.js +26 -22
- package/dist/client/createClientUploadHandler.js.map +1 -1
- 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 +2 -4
- package/dist/exports/utilities.d.ts.map +1 -1
- package/dist/exports/utilities.js +2 -4
- package/dist/exports/utilities.js.map +1 -1
- package/dist/fields/getFields.d.ts +1 -5
- package/dist/fields/getFields.d.ts.map +1 -1
- package/dist/fields/getFields.js +102 -35
- 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 +93 -27
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/afterDelete.d.ts.map +1 -1
- package/dist/hooks/afterDelete.js +10 -2
- package/dist/hooks/afterDelete.js.map +1 -1
- package/dist/hooks/afterRead.d.ts +6 -1
- package/dist/hooks/afterRead.d.ts.map +1 -1
- package/dist/hooks/afterRead.js +27 -11
- package/dist/hooks/afterRead.js.map +1 -1
- package/dist/hooks/beforeChange.d.ts +2 -1
- package/dist/hooks/beforeChange.d.ts.map +1 -1
- package/dist/hooks/beforeChange.js +14 -9
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/hooks/publicOriginalURL.d.ts +8 -0
- package/dist/hooks/publicOriginalURL.d.ts.map +1 -0
- package/dist/hooks/publicOriginalURL.js +34 -0
- package/dist/hooks/publicOriginalURL.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +198 -132
- package/dist/plugin.js.map +1 -1
- package/dist/types.d.ts +48 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/buildStoragePathData.d.ts +0 -6
- package/dist/utilities/buildStoragePathData.d.ts.map +1 -1
- package/dist/utilities/buildStoragePathData.js +0 -2
- package/dist/utilities/buildStoragePathData.js.map +1 -1
- package/dist/utilities/createFileOperations.d.ts +13 -0
- package/dist/utilities/createFileOperations.d.ts.map +1 -0
- package/dist/utilities/createFileOperations.js +258 -0
- package/dist/utilities/createFileOperations.js.map +1 -0
- package/dist/utilities/getFilePrefix.d.ts +9 -20
- package/dist/utilities/getFilePrefix.d.ts.map +1 -1
- package/dist/utilities/getFilePrefix.js +42 -31
- package/dist/utilities/getFilePrefix.js.map +1 -1
- package/dist/utilities/getIncomingFiles.js +4 -4
- package/dist/utilities/getIncomingFiles.js.map +1 -1
- package/dist/utilities/getStorageLocationSelect.d.ts +9 -0
- package/dist/utilities/getStorageLocationSelect.d.ts.map +1 -0
- package/dist/utilities/getStorageLocationSelect.js +54 -0
- package/dist/utilities/getStorageLocationSelect.js.map +1 -0
- package/dist/utilities/resolveSignedURLKey.d.ts +10 -10
- package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
- package/dist/utilities/resolveSignedURLKey.js +33 -26
- package/dist/utilities/resolveSignedURLKey.js.map +1 -1
- package/dist/utilities/sanitizePrefix.d.ts +1 -9
- package/dist/utilities/sanitizePrefix.d.ts.map +1 -1
- package/dist/utilities/sanitizePrefix.js +1 -21
- package/dist/utilities/sanitizePrefix.js.map +1 -1
- package/package.json +6 -6
- package/dist/admin/fields/getFields.d.ts +0 -17
- package/dist/admin/fields/getFields.d.ts.map +0 -1
- package/dist/admin/fields/getFields.js +0 -98
- package/dist/admin/fields/getFields.js.map +0 -1
- package/dist/admin/index.d.ts +0 -4
- package/dist/admin/index.d.ts.map +0 -1
- package/dist/admin/index.js +0 -34
- package/dist/admin/index.js.map +0 -1
- package/dist/hooks/afterChange.spec.js +0 -150
- package/dist/hooks/afterChange.spec.js.map +0 -1
- package/dist/hooks/normalizeUploadPrefix.spec.js +0 -154
- package/dist/hooks/normalizeUploadPrefix.spec.js.map +0 -1
- package/dist/plugin.spec.js +0 -166
- package/dist/plugin.spec.js.map +0 -1
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts +0 -15
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts.map +0 -1
- package/dist/utilities/assertReceiptMatchesFileKey.js +0 -31
- package/dist/utilities/assertReceiptMatchesFileKey.js.map +0 -1
- package/dist/utilities/buildStoragePathData.spec.js +0 -254
- package/dist/utilities/buildStoragePathData.spec.js.map +0 -1
- package/dist/utilities/getFilePrefix.spec.js +0 -291
- package/dist/utilities/getFilePrefix.spec.js.map +0 -1
- package/dist/utilities/initClientUploads.d.ts +0 -13
- package/dist/utilities/initClientUploads.d.ts.map +0 -1
- package/dist/utilities/initClientUploads.js +0 -63
- package/dist/utilities/initClientUploads.js.map +0 -1
- package/dist/utilities/resolveSignedURLKey.spec.js +0 -56
- package/dist/utilities/resolveSignedURLKey.spec.js.map +0 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts +0 -20
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts.map +0 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js +0 -24
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js.map +0 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js +0 -108
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js.map +0 -1
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { getAfterChangeHook } from './afterChange.js';
|
|
3
|
-
describe('upload replacement cleanup', ()=>{
|
|
4
|
-
it.each([
|
|
5
|
-
[
|
|
6
|
-
'invoices',
|
|
7
|
-
'media/invoices',
|
|
8
|
-
[
|
|
9
|
-
{
|
|
10
|
-
filename: 'file.png',
|
|
11
|
-
prefix: 'invoices'
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
filename: 'thumb.png',
|
|
15
|
-
prefix: 'invoices'
|
|
16
|
-
}
|
|
17
|
-
]
|
|
18
|
-
],
|
|
19
|
-
[
|
|
20
|
-
'',
|
|
21
|
-
'media',
|
|
22
|
-
[]
|
|
23
|
-
],
|
|
24
|
-
[
|
|
25
|
-
'media/invoices',
|
|
26
|
-
'media/invoices',
|
|
27
|
-
[]
|
|
28
|
-
]
|
|
29
|
-
])('should compare object locations when replacing %s with %s', async (oldPrefix, newPrefix, expectedDeletes)=>{
|
|
30
|
-
const handleDelete = vi.fn();
|
|
31
|
-
const hook = getAfterChangeHook({
|
|
32
|
-
adapter: {
|
|
33
|
-
handleUpload: vi.fn(),
|
|
34
|
-
handleDelete
|
|
35
|
-
},
|
|
36
|
-
collection: {
|
|
37
|
-
slug: 'media'
|
|
38
|
-
},
|
|
39
|
-
collectionPrefix: 'media'
|
|
40
|
-
});
|
|
41
|
-
await hook({
|
|
42
|
-
doc: {
|
|
43
|
-
id: 1,
|
|
44
|
-
filename: 'file.png',
|
|
45
|
-
mimeType: 'image/png',
|
|
46
|
-
sizes: {
|
|
47
|
-
thumbnail: {
|
|
48
|
-
filename: 'thumb.png'
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
prefix: newPrefix
|
|
52
|
-
},
|
|
53
|
-
previousDoc: {
|
|
54
|
-
id: 1,
|
|
55
|
-
filename: 'file.png',
|
|
56
|
-
mimeType: 'image/png',
|
|
57
|
-
sizes: {
|
|
58
|
-
thumbnail: {
|
|
59
|
-
filename: 'thumb.png'
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
prefix: oldPrefix
|
|
63
|
-
},
|
|
64
|
-
operation: 'update',
|
|
65
|
-
req: {
|
|
66
|
-
context: {},
|
|
67
|
-
file: {
|
|
68
|
-
data: Buffer.from('file'),
|
|
69
|
-
size: 4
|
|
70
|
-
},
|
|
71
|
-
payload: {
|
|
72
|
-
logger: {
|
|
73
|
-
error: vi.fn()
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
expect(handleDelete.mock.calls.map(([{ doc, filename }])=>({
|
|
79
|
-
filename,
|
|
80
|
-
prefix: doc.prefix
|
|
81
|
-
}))).toEqual(expectedDeletes);
|
|
82
|
-
});
|
|
83
|
-
it('should delete a replaced client upload at its _objectKey folder', async ()=>{
|
|
84
|
-
const handleDelete = vi.fn();
|
|
85
|
-
const hook = getAfterChangeHook({
|
|
86
|
-
adapter: {
|
|
87
|
-
handleUpload: vi.fn(),
|
|
88
|
-
handleDelete
|
|
89
|
-
},
|
|
90
|
-
collection: {
|
|
91
|
-
slug: 'media'
|
|
92
|
-
},
|
|
93
|
-
collectionPrefix: 'media'
|
|
94
|
-
});
|
|
95
|
-
await hook({
|
|
96
|
-
doc: {
|
|
97
|
-
id: 1,
|
|
98
|
-
_objectKey: 'new-key',
|
|
99
|
-
filename: 'file.png',
|
|
100
|
-
mimeType: 'image/png',
|
|
101
|
-
prefix: 'media/invoices',
|
|
102
|
-
sizes: {
|
|
103
|
-
thumbnail: {
|
|
104
|
-
filename: 'thumb.png'
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
previousDoc: {
|
|
109
|
-
id: 1,
|
|
110
|
-
_objectKey: 'old-key',
|
|
111
|
-
filename: 'file.png',
|
|
112
|
-
mimeType: 'image/png',
|
|
113
|
-
prefix: 'media/invoices',
|
|
114
|
-
sizes: {
|
|
115
|
-
thumbnail: {
|
|
116
|
-
filename: 'thumb.png'
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
operation: 'update',
|
|
121
|
-
req: {
|
|
122
|
-
context: {},
|
|
123
|
-
file: {
|
|
124
|
-
data: Buffer.from('file'),
|
|
125
|
-
size: 4
|
|
126
|
-
},
|
|
127
|
-
payload: {
|
|
128
|
-
logger: {
|
|
129
|
-
error: vi.fn()
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
expect(handleDelete.mock.calls.map(([{ filename, storageFilePath }])=>({
|
|
135
|
-
filename,
|
|
136
|
-
storageFilePath
|
|
137
|
-
}))).toEqual([
|
|
138
|
-
{
|
|
139
|
-
filename: 'file.png',
|
|
140
|
-
storageFilePath: 'media/invoices/old-key/file.png'
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
filename: 'thumb.png',
|
|
144
|
-
storageFilePath: 'media/invoices/old-key/thumb.png'
|
|
145
|
-
}
|
|
146
|
-
]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
//# sourceMappingURL=afterChange.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/afterChange.spec.ts"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest'\n\nimport { getAfterChangeHook } from './afterChange.js'\n\ndescribe('upload replacement cleanup', () => {\n it.each([\n [\n 'invoices',\n 'media/invoices',\n [\n { filename: 'file.png', prefix: 'invoices' },\n { filename: 'thumb.png', prefix: 'invoices' },\n ],\n ],\n ['', 'media', []],\n ['media/invoices', 'media/invoices', []],\n ] as const)(\n 'should compare object locations when replacing %s with %s',\n async (oldPrefix, newPrefix, expectedDeletes) => {\n const handleDelete = vi.fn()\n const hook = getAfterChangeHook({\n adapter: { handleUpload: vi.fn(), handleDelete } as never,\n collection: { slug: 'media' } as never,\n collectionPrefix: 'media',\n })\n\n await hook({\n doc: {\n id: 1,\n filename: 'file.png',\n mimeType: 'image/png',\n sizes: { thumbnail: { filename: 'thumb.png' } },\n prefix: newPrefix,\n },\n previousDoc: {\n id: 1,\n filename: 'file.png',\n mimeType: 'image/png',\n sizes: { thumbnail: { filename: 'thumb.png' } },\n prefix: oldPrefix,\n },\n operation: 'update',\n req: {\n context: {},\n file: { data: Buffer.from('file'), size: 4 },\n payload: { logger: { error: vi.fn() } },\n },\n } as never)\n\n expect(\n handleDelete.mock.calls.map(([{ doc, filename }]) => ({\n filename,\n prefix: doc.prefix,\n })),\n ).toEqual(expectedDeletes)\n },\n )\n\n it('should delete a replaced client upload at its _objectKey folder', async () => {\n const handleDelete = vi.fn()\n const hook = getAfterChangeHook({\n adapter: { handleUpload: vi.fn(), handleDelete } as never,\n collection: { slug: 'media' } as never,\n collectionPrefix: 'media',\n })\n\n await hook({\n doc: {\n id: 1,\n _objectKey: 'new-key',\n filename: 'file.png',\n mimeType: 'image/png',\n prefix: 'media/invoices',\n sizes: { thumbnail: { filename: 'thumb.png' } },\n },\n previousDoc: {\n id: 1,\n _objectKey: 'old-key',\n filename: 'file.png',\n mimeType: 'image/png',\n prefix: 'media/invoices',\n sizes: { thumbnail: { filename: 'thumb.png' } },\n },\n operation: 'update',\n req: {\n context: {},\n file: { data: Buffer.from('file'), size: 4 },\n payload: { logger: { error: vi.fn() } },\n },\n } as never)\n\n expect(\n handleDelete.mock.calls.map(([{ filename, storageFilePath }]) => ({\n filename,\n storageFilePath,\n })),\n ).toEqual([\n { filename: 'file.png', storageFilePath: 'media/invoices/old-key/file.png' },\n { filename: 'thumb.png', storageFilePath: 'media/invoices/old-key/thumb.png' },\n ])\n })\n})\n"],"names":["describe","expect","it","vi","getAfterChangeHook","each","filename","prefix","oldPrefix","newPrefix","expectedDeletes","handleDelete","fn","hook","adapter","handleUpload","collection","slug","collectionPrefix","doc","id","mimeType","sizes","thumbnail","previousDoc","operation","req","context","file","data","Buffer","from","size","payload","logger","error","mock","calls","map","toEqual","_objectKey","storageFilePath"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAEjD,SAASC,kBAAkB,QAAQ,mBAAkB;AAErDJ,SAAS,8BAA8B;IACrCE,GAAGG,IAAI,CAAC;QACN;YACE;YACA;YACA;gBACE;oBAAEC,UAAU;oBAAYC,QAAQ;gBAAW;gBAC3C;oBAAED,UAAU;oBAAaC,QAAQ;gBAAW;aAC7C;SACF;QACD;YAAC;YAAI;YAAS,EAAE;SAAC;QACjB;YAAC;YAAkB;YAAkB,EAAE;SAAC;KACzC,EACC,6DACA,OAAOC,WAAWC,WAAWC;QAC3B,MAAMC,eAAeR,GAAGS,EAAE;QAC1B,MAAMC,OAAOT,mBAAmB;YAC9BU,SAAS;gBAAEC,cAAcZ,GAAGS,EAAE;gBAAID;YAAa;YAC/CK,YAAY;gBAAEC,MAAM;YAAQ;YAC5BC,kBAAkB;QACpB;QAEA,MAAML,KAAK;YACTM,KAAK;gBACHC,IAAI;gBACJd,UAAU;gBACVe,UAAU;gBACVC,OAAO;oBAAEC,WAAW;wBAAEjB,UAAU;oBAAY;gBAAE;gBAC9CC,QAAQE;YACV;YACAe,aAAa;gBACXJ,IAAI;gBACJd,UAAU;gBACVe,UAAU;gBACVC,OAAO;oBAAEC,WAAW;wBAAEjB,UAAU;oBAAY;gBAAE;gBAC9CC,QAAQC;YACV;YACAiB,WAAW;YACXC,KAAK;gBACHC,SAAS,CAAC;gBACVC,MAAM;oBAAEC,MAAMC,OAAOC,IAAI,CAAC;oBAASC,MAAM;gBAAE;gBAC3CC,SAAS;oBAAEC,QAAQ;wBAAEC,OAAOhC,GAAGS,EAAE;oBAAG;gBAAE;YACxC;QACF;QAEAX,OACEU,aAAayB,IAAI,CAACC,KAAK,CAACC,GAAG,CAAC,CAAC,CAAC,EAAEnB,GAAG,EAAEb,QAAQ,EAAE,CAAC,GAAM,CAAA;gBACpDA;gBACAC,QAAQY,IAAIZ,MAAM;YACpB,CAAA,IACAgC,OAAO,CAAC7B;IACZ;IAGFR,GAAG,mEAAmE;QACpE,MAAMS,eAAeR,GAAGS,EAAE;QAC1B,MAAMC,OAAOT,mBAAmB;YAC9BU,SAAS;gBAAEC,cAAcZ,GAAGS,EAAE;gBAAID;YAAa;YAC/CK,YAAY;gBAAEC,MAAM;YAAQ;YAC5BC,kBAAkB;QACpB;QAEA,MAAML,KAAK;YACTM,KAAK;gBACHC,IAAI;gBACJoB,YAAY;gBACZlC,UAAU;gBACVe,UAAU;gBACVd,QAAQ;gBACRe,OAAO;oBAAEC,WAAW;wBAAEjB,UAAU;oBAAY;gBAAE;YAChD;YACAkB,aAAa;gBACXJ,IAAI;gBACJoB,YAAY;gBACZlC,UAAU;gBACVe,UAAU;gBACVd,QAAQ;gBACRe,OAAO;oBAAEC,WAAW;wBAAEjB,UAAU;oBAAY;gBAAE;YAChD;YACAmB,WAAW;YACXC,KAAK;gBACHC,SAAS,CAAC;gBACVC,MAAM;oBAAEC,MAAMC,OAAOC,IAAI,CAAC;oBAASC,MAAM;gBAAE;gBAC3CC,SAAS;oBAAEC,QAAQ;wBAAEC,OAAOhC,GAAGS,EAAE;oBAAG;gBAAE;YACxC;QACF;QAEAX,OACEU,aAAayB,IAAI,CAACC,KAAK,CAACC,GAAG,CAAC,CAAC,CAAC,EAAEhC,QAAQ,EAAEmC,eAAe,EAAE,CAAC,GAAM,CAAA;gBAChEnC;gBACAmC;YACF,CAAA,IACAF,OAAO,CAAC;YACR;gBAAEjC,UAAU;gBAAYmC,iBAAiB;YAAkC;YAC3E;gBAAEnC,UAAU;gBAAamC,iBAAiB;YAAmC;SAC9E;IACH;AACF"}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getFields } from '../fields/getFields.js';
|
|
3
|
-
import { getNormalizeUploadPrefixFieldHook, getNormalizeUploadPrefixHook } from './normalizeUploadPrefix.js';
|
|
4
|
-
const runHook = ({ data, originalDoc, req = {
|
|
5
|
-
context: {},
|
|
6
|
-
file: {}
|
|
7
|
-
} })=>getNormalizeUploadPrefixHook({
|
|
8
|
-
collectionPrefix: 'media'
|
|
9
|
-
})({
|
|
10
|
-
data,
|
|
11
|
-
originalDoc,
|
|
12
|
-
req
|
|
13
|
-
});
|
|
14
|
-
describe('new upload prefixes', ()=>{
|
|
15
|
-
it('should normalize after custom prefix field hooks', async ()=>{
|
|
16
|
-
const fields = getFields({
|
|
17
|
-
collection: {
|
|
18
|
-
fields: [
|
|
19
|
-
{
|
|
20
|
-
name: 'prefix',
|
|
21
|
-
type: 'text',
|
|
22
|
-
hooks: {
|
|
23
|
-
beforeChange: [
|
|
24
|
-
()=>'custom'
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
prefix: 'media'
|
|
31
|
-
});
|
|
32
|
-
const field = fields.find((field)=>'name' in field && field.name === 'prefix');
|
|
33
|
-
let value = 'original';
|
|
34
|
-
for (const hook of field.hooks?.beforeChange || []){
|
|
35
|
-
value = await hook({
|
|
36
|
-
value,
|
|
37
|
-
data: {
|
|
38
|
-
filename: 'file.png'
|
|
39
|
-
},
|
|
40
|
-
req: {
|
|
41
|
-
file: {},
|
|
42
|
-
context: {}
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
expect(value).toBe('media/custom');
|
|
47
|
-
});
|
|
48
|
-
it('should contain a prefix supplied with a file upload', async ()=>{
|
|
49
|
-
const result = await runHook({
|
|
50
|
-
data: {
|
|
51
|
-
filename: 'file.png',
|
|
52
|
-
prefix: 'legacy'
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
expect(result.prefix).toBe('media/legacy');
|
|
56
|
-
});
|
|
57
|
-
it('should leave internal cloud storage writes alone', async ()=>{
|
|
58
|
-
const result = await runHook({
|
|
59
|
-
data: {
|
|
60
|
-
filename: 'file.png',
|
|
61
|
-
prefix: 'legacy'
|
|
62
|
-
},
|
|
63
|
-
req: {
|
|
64
|
-
context: {
|
|
65
|
-
skipCloudStorage: true
|
|
66
|
-
},
|
|
67
|
-
file: {}
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
expect(result.prefix).toBe('legacy');
|
|
71
|
-
});
|
|
72
|
-
/**
|
|
73
|
-
* The `prefix` field is writable through the REST/GraphQL/Local API, so a request
|
|
74
|
-
* carrying no file must not be able to point a document at a foreign object.
|
|
75
|
-
*/ it('should contain a caller-supplied prefix on a metadata-only update', async ()=>{
|
|
76
|
-
const result = await runHook({
|
|
77
|
-
data: {
|
|
78
|
-
prefix: 'tenant-b/private'
|
|
79
|
-
},
|
|
80
|
-
originalDoc: {
|
|
81
|
-
prefix: 'media/legacy'
|
|
82
|
-
},
|
|
83
|
-
req: {
|
|
84
|
-
context: {}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
expect(result.prefix).toBe('media/tenant-b/private');
|
|
88
|
-
});
|
|
89
|
-
it('should contain a caller-supplied prefix on a create without a file', async ()=>{
|
|
90
|
-
const result = await runHook({
|
|
91
|
-
data: {
|
|
92
|
-
prefix: 'tenant-b/private'
|
|
93
|
-
},
|
|
94
|
-
req: {
|
|
95
|
-
context: {}
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
expect(result.prefix).toBe('media/tenant-b/private');
|
|
99
|
-
});
|
|
100
|
-
it('should leave a stored prefix alone when the request omits it', async ()=>{
|
|
101
|
-
const result = await runHook({
|
|
102
|
-
data: {
|
|
103
|
-
filename: 'legacy.png'
|
|
104
|
-
},
|
|
105
|
-
originalDoc: {
|
|
106
|
-
prefix: 'legacy-invoices'
|
|
107
|
-
},
|
|
108
|
-
req: {
|
|
109
|
-
context: {}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
expect(result.prefix).toBeUndefined();
|
|
113
|
-
});
|
|
114
|
-
it('should leave a stored prefix alone when the request repeats it unchanged', async ()=>{
|
|
115
|
-
const result = await runHook({
|
|
116
|
-
data: {
|
|
117
|
-
prefix: 'legacy-invoices'
|
|
118
|
-
},
|
|
119
|
-
originalDoc: {
|
|
120
|
-
prefix: 'legacy-invoices'
|
|
121
|
-
},
|
|
122
|
-
req: {
|
|
123
|
-
context: {}
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
expect(result.prefix).toBe('legacy-invoices');
|
|
127
|
-
});
|
|
128
|
-
describe('prefix field hook', ()=>{
|
|
129
|
-
const runFieldHook = ({ previousValue, req = {
|
|
130
|
-
context: {}
|
|
131
|
-
}, value })=>getNormalizeUploadPrefixFieldHook({
|
|
132
|
-
collectionPrefix: 'media'
|
|
133
|
-
})({
|
|
134
|
-
data: {},
|
|
135
|
-
previousValue,
|
|
136
|
-
req,
|
|
137
|
-
value
|
|
138
|
-
});
|
|
139
|
-
it('should contain a caller-supplied value without a file', async ()=>{
|
|
140
|
-
expect(await runFieldHook({
|
|
141
|
-
previousValue: 'media/legacy',
|
|
142
|
-
value: 'tenant-b/private'
|
|
143
|
-
})).toBe('media/tenant-b/private');
|
|
144
|
-
});
|
|
145
|
-
it('should leave an unchanged value alone', async ()=>{
|
|
146
|
-
expect(await runFieldHook({
|
|
147
|
-
previousValue: 'legacy-invoices',
|
|
148
|
-
value: 'legacy-invoices'
|
|
149
|
-
})).toBe('legacy-invoices');
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
//# sourceMappingURL=normalizeUploadPrefix.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/normalizeUploadPrefix.spec.ts"],"sourcesContent":["import type { CollectionConfig, TextField } from 'payload'\n\nimport { describe, expect, it } from 'vitest'\n\nimport { getFields } from '../fields/getFields.js'\nimport {\n getNormalizeUploadPrefixFieldHook,\n getNormalizeUploadPrefixHook,\n} from './normalizeUploadPrefix.js'\n\nconst runHook = ({\n data,\n originalDoc,\n req = { context: {}, file: {} },\n}: {\n data: Record<string, unknown>\n originalDoc?: Record<string, unknown>\n req?: Record<string, unknown>\n}) =>\n getNormalizeUploadPrefixHook({ collectionPrefix: 'media' })({\n data,\n originalDoc,\n req,\n } as never)\n\ndescribe('new upload prefixes', () => {\n it('should normalize after custom prefix field hooks', async () => {\n const fields = getFields({\n collection: {\n fields: [{ name: 'prefix', type: 'text', hooks: { beforeChange: [() => 'custom'] } }],\n } as CollectionConfig,\n prefix: 'media',\n })\n const field = fields.find((field) => 'name' in field && field.name === 'prefix') as TextField\n let value = 'original'\n\n for (const hook of field.hooks?.beforeChange || []) {\n value = await hook({\n value,\n data: { filename: 'file.png' },\n req: { file: {}, context: {} },\n } as never)\n }\n\n expect(value).toBe('media/custom')\n })\n\n it('should contain a prefix supplied with a file upload', async () => {\n const result = await runHook({ data: { filename: 'file.png', prefix: 'legacy' } })\n\n expect(result.prefix).toBe('media/legacy')\n })\n\n it('should leave internal cloud storage writes alone', async () => {\n const result = await runHook({\n data: { filename: 'file.png', prefix: 'legacy' },\n req: { context: { skipCloudStorage: true }, file: {} },\n })\n\n expect(result.prefix).toBe('legacy')\n })\n\n /**\n * The `prefix` field is writable through the REST/GraphQL/Local API, so a request\n * carrying no file must not be able to point a document at a foreign object.\n */\n it('should contain a caller-supplied prefix on a metadata-only update', async () => {\n const result = await runHook({\n data: { prefix: 'tenant-b/private' },\n originalDoc: { prefix: 'media/legacy' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('media/tenant-b/private')\n })\n\n it('should contain a caller-supplied prefix on a create without a file', async () => {\n const result = await runHook({\n data: { prefix: 'tenant-b/private' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('media/tenant-b/private')\n })\n\n it('should leave a stored prefix alone when the request omits it', async () => {\n const result = await runHook({\n data: { filename: 'legacy.png' },\n originalDoc: { prefix: 'legacy-invoices' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBeUndefined()\n })\n\n it('should leave a stored prefix alone when the request repeats it unchanged', async () => {\n const result = await runHook({\n data: { prefix: 'legacy-invoices' },\n originalDoc: { prefix: 'legacy-invoices' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('legacy-invoices')\n })\n\n describe('prefix field hook', () => {\n const runFieldHook = ({\n previousValue,\n req = { context: {} },\n value,\n }: {\n previousValue?: string\n req?: Record<string, unknown>\n value: unknown\n }) =>\n getNormalizeUploadPrefixFieldHook({ collectionPrefix: 'media' })({\n data: {},\n previousValue,\n req,\n value,\n } as never)\n\n it('should contain a caller-supplied value without a file', async () => {\n expect(await runFieldHook({ previousValue: 'media/legacy', value: 'tenant-b/private' })).toBe(\n 'media/tenant-b/private',\n )\n })\n\n it('should leave an unchanged value alone', async () => {\n expect(\n await runFieldHook({ previousValue: 'legacy-invoices', value: 'legacy-invoices' }),\n ).toBe('legacy-invoices')\n })\n })\n})\n"],"names":["describe","expect","it","getFields","getNormalizeUploadPrefixFieldHook","getNormalizeUploadPrefixHook","runHook","data","originalDoc","req","context","file","collectionPrefix","fields","collection","name","type","hooks","beforeChange","prefix","field","find","value","hook","filename","toBe","result","skipCloudStorage","toBeUndefined","runFieldHook","previousValue"],"mappings":"AAEA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,SAAS,QAAQ,yBAAwB;AAClD,SACEC,iCAAiC,EACjCC,4BAA4B,QACvB,6BAA4B;AAEnC,MAAMC,UAAU,CAAC,EACfC,IAAI,EACJC,WAAW,EACXC,MAAM;IAAEC,SAAS,CAAC;IAAGC,MAAM,CAAC;AAAE,CAAC,EAKhC,GACCN,6BAA6B;QAAEO,kBAAkB;IAAQ,GAAG;QAC1DL;QACAC;QACAC;IACF;AAEFT,SAAS,uBAAuB;IAC9BE,GAAG,oDAAoD;QACrD,MAAMW,SAASV,UAAU;YACvBW,YAAY;gBACVD,QAAQ;oBAAC;wBAAEE,MAAM;wBAAUC,MAAM;wBAAQC,OAAO;4BAAEC,cAAc;gCAAC,IAAM;6BAAS;wBAAC;oBAAE;iBAAE;YACvF;YACAC,QAAQ;QACV;QACA,MAAMC,QAAQP,OAAOQ,IAAI,CAAC,CAACD,QAAU,UAAUA,SAASA,MAAML,IAAI,KAAK;QACvE,IAAIO,QAAQ;QAEZ,KAAK,MAAMC,QAAQH,MAAMH,KAAK,EAAEC,gBAAgB,EAAE,CAAE;YAClDI,QAAQ,MAAMC,KAAK;gBACjBD;gBACAf,MAAM;oBAAEiB,UAAU;gBAAW;gBAC7Bf,KAAK;oBAAEE,MAAM,CAAC;oBAAGD,SAAS,CAAC;gBAAE;YAC/B;QACF;QAEAT,OAAOqB,OAAOG,IAAI,CAAC;IACrB;IAEAvB,GAAG,uDAAuD;QACxD,MAAMwB,SAAS,MAAMpB,QAAQ;YAAEC,MAAM;gBAAEiB,UAAU;gBAAYL,QAAQ;YAAS;QAAE;QAEhFlB,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAvB,GAAG,oDAAoD;QACrD,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEiB,UAAU;gBAAYL,QAAQ;YAAS;YAC/CV,KAAK;gBAAEC,SAAS;oBAAEiB,kBAAkB;gBAAK;gBAAGhB,MAAM,CAAC;YAAE;QACvD;QAEAV,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEA;;;GAGC,GACDvB,GAAG,qEAAqE;QACtE,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEY,QAAQ;YAAmB;YACnCX,aAAa;gBAAEW,QAAQ;YAAe;YACtCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAvB,GAAG,sEAAsE;QACvE,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEY,QAAQ;YAAmB;YACnCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAvB,GAAG,gEAAgE;QACjE,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEiB,UAAU;YAAa;YAC/BhB,aAAa;gBAAEW,QAAQ;YAAkB;YACzCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAES,aAAa;IACrC;IAEA1B,GAAG,4EAA4E;QAC7E,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEY,QAAQ;YAAkB;YAClCX,aAAa;gBAAEW,QAAQ;YAAkB;YACzCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAzB,SAAS,qBAAqB;QAC5B,MAAM6B,eAAe,CAAC,EACpBC,aAAa,EACbrB,MAAM;YAAEC,SAAS,CAAC;QAAE,CAAC,EACrBY,KAAK,EAKN,GACClB,kCAAkC;gBAAEQ,kBAAkB;YAAQ,GAAG;gBAC/DL,MAAM,CAAC;gBACPuB;gBACArB;gBACAa;YACF;QAEFpB,GAAG,yDAAyD;YAC1DD,OAAO,MAAM4B,aAAa;gBAAEC,eAAe;gBAAgBR,OAAO;YAAmB,IAAIG,IAAI,CAC3F;QAEJ;QAEAvB,GAAG,yCAAyC;YAC1CD,OACE,MAAM4B,aAAa;gBAAEC,eAAe;gBAAmBR,OAAO;YAAkB,IAChFG,IAAI,CAAC;QACT;IACF;AACF"}
|
package/dist/plugin.spec.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { getExternalFile } from 'payload/internal';
|
|
3
|
-
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { cloudStoragePlugin } from './plugin.js';
|
|
5
|
-
const collectionSlug = 'media';
|
|
6
|
-
describe('cloudStoragePlugin external file fetching', ()=>{
|
|
7
|
-
const server = createServer((_req, res)=>{
|
|
8
|
-
res.writeHead(200, {
|
|
9
|
-
'Content-Length': '12',
|
|
10
|
-
'Content-Type': 'text/plain'
|
|
11
|
-
});
|
|
12
|
-
res.end('stored file\n');
|
|
13
|
-
});
|
|
14
|
-
let port;
|
|
15
|
-
beforeAll(async ()=>{
|
|
16
|
-
await new Promise((resolve)=>server.listen(0, resolve));
|
|
17
|
-
port = server.address().port;
|
|
18
|
-
});
|
|
19
|
-
afterEach(()=>{
|
|
20
|
-
vi.unstubAllEnvs();
|
|
21
|
-
});
|
|
22
|
-
afterAll(async ()=>{
|
|
23
|
-
await new Promise((resolve, reject)=>{
|
|
24
|
-
server.close((error)=>error ? reject(error) : resolve());
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
it('should filter private URLs when Payload file access control is disabled', async ()=>{
|
|
28
|
-
vi.stubEnv('NODE_ENV', 'production');
|
|
29
|
-
const uploadConfig = buildUploadConfig();
|
|
30
|
-
await expect(fetchExternalFile({
|
|
31
|
-
uploadConfig,
|
|
32
|
-
url: `http://127.0.0.1:${port}/file.txt`
|
|
33
|
-
})).rejects.toThrow('Blocked unsafe attempt to 127.0.0.1');
|
|
34
|
-
});
|
|
35
|
-
it('should allow private URLs when skipSafeFetch is explicitly true', async ()=>{
|
|
36
|
-
vi.stubEnv('NODE_ENV', 'production');
|
|
37
|
-
const uploadConfig = buildUploadConfig(true);
|
|
38
|
-
const file = await fetchExternalFile({
|
|
39
|
-
uploadConfig,
|
|
40
|
-
url: `http://127.0.0.1:${port}/file.txt`
|
|
41
|
-
});
|
|
42
|
-
expect(file.data.toString()).toBe('stored file\n');
|
|
43
|
-
});
|
|
44
|
-
it('should allow private URLs that match an explicit skipSafeFetch allowlist', async ()=>{
|
|
45
|
-
vi.stubEnv('NODE_ENV', 'production');
|
|
46
|
-
const uploadConfig = buildUploadConfig([
|
|
47
|
-
{
|
|
48
|
-
hostname: '127.0.0.1',
|
|
49
|
-
port: String(port),
|
|
50
|
-
protocol: 'http'
|
|
51
|
-
}
|
|
52
|
-
]);
|
|
53
|
-
const file = await fetchExternalFile({
|
|
54
|
-
uploadConfig,
|
|
55
|
-
url: `http://127.0.0.1:${port}/file.txt`
|
|
56
|
-
});
|
|
57
|
-
expect(file.data.toString()).toBe('stored file\n');
|
|
58
|
-
});
|
|
59
|
-
it('should allow localhost URLs outside production', async ()=>{
|
|
60
|
-
vi.stubEnv('NODE_ENV', 'development');
|
|
61
|
-
const uploadConfig = buildUploadConfig();
|
|
62
|
-
const file = await fetchExternalFile({
|
|
63
|
-
uploadConfig,
|
|
64
|
-
url: `http://localhost:${port}/file.txt`
|
|
65
|
-
});
|
|
66
|
-
expect(file.data.toString()).toBe('stored file\n');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
function buildUploadConfig(skipSafeFetch) {
|
|
70
|
-
const config = cloudStoragePlugin({
|
|
71
|
-
collections: {
|
|
72
|
-
[collectionSlug]: {
|
|
73
|
-
adapter: ()=>({
|
|
74
|
-
handleDelete: async ()=>undefined,
|
|
75
|
-
handleUpload: async ()=>undefined,
|
|
76
|
-
name: 'test-adapter',
|
|
77
|
-
staticHandler: async ()=>new Response(null, {
|
|
78
|
-
status: 404
|
|
79
|
-
})
|
|
80
|
-
}),
|
|
81
|
-
disablePayloadAccessControl: true
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
})({
|
|
85
|
-
collections: [
|
|
86
|
-
{
|
|
87
|
-
fields: [],
|
|
88
|
-
slug: collectionSlug,
|
|
89
|
-
upload: typeof skipSafeFetch === 'undefined' ? true : {
|
|
90
|
-
skipSafeFetch
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
]
|
|
94
|
-
});
|
|
95
|
-
const uploadConfig = config.collections?.[0]?.upload;
|
|
96
|
-
if (!uploadConfig || typeof uploadConfig !== 'object') {
|
|
97
|
-
throw new Error('Expected an upload configuration');
|
|
98
|
-
}
|
|
99
|
-
return uploadConfig;
|
|
100
|
-
}
|
|
101
|
-
async function fetchExternalFile({ uploadConfig, url }) {
|
|
102
|
-
return getExternalFile({
|
|
103
|
-
data: {
|
|
104
|
-
filename: 'file.txt',
|
|
105
|
-
url
|
|
106
|
-
},
|
|
107
|
-
req: {
|
|
108
|
-
headers: new Headers(),
|
|
109
|
-
payload: {
|
|
110
|
-
config: {
|
|
111
|
-
cookiePrefix: 'payload'
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
uploadConfig
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
const adapter = ()=>({
|
|
119
|
-
handleDelete: ()=>undefined,
|
|
120
|
-
handleUpload: ()=>undefined,
|
|
121
|
-
name: 'test-adapter',
|
|
122
|
-
staticHandler: ()=>new Response()
|
|
123
|
-
});
|
|
124
|
-
describe('cloudStoragePlugin', ()=>{
|
|
125
|
-
it('should normalize a stored prefix during a server-mediated upload', async ()=>{
|
|
126
|
-
const config = cloudStoragePlugin({
|
|
127
|
-
collections: {
|
|
128
|
-
media: {
|
|
129
|
-
adapter
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
})({
|
|
133
|
-
collections: [
|
|
134
|
-
{
|
|
135
|
-
fields: [],
|
|
136
|
-
slug: 'media',
|
|
137
|
-
upload: true
|
|
138
|
-
}
|
|
139
|
-
]
|
|
140
|
-
});
|
|
141
|
-
const hooks = config.collections?.[0]?.hooks?.beforeChange || [];
|
|
142
|
-
const req = {
|
|
143
|
-
context: {},
|
|
144
|
-
file: {
|
|
145
|
-
mimetype: 'image/png',
|
|
146
|
-
name: 'photo.png',
|
|
147
|
-
size: 42
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
let data = {
|
|
151
|
-
filename: 'photo.png',
|
|
152
|
-
prefix: '/tenant/../acme'
|
|
153
|
-
};
|
|
154
|
-
for (const hook of hooks){
|
|
155
|
-
const result = await hook({
|
|
156
|
-
data,
|
|
157
|
-
operation: 'create',
|
|
158
|
-
req
|
|
159
|
-
});
|
|
160
|
-
data = result || data;
|
|
161
|
-
}
|
|
162
|
-
expect(data.prefix).toBe('tenant/acme');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
//# sourceMappingURL=plugin.spec.js.map
|
package/dist/plugin.spec.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.spec.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net'\nimport type { Config, PayloadRequest, UploadConfig } from 'payload'\n\nimport { createServer } from 'node:http'\nimport { getExternalFile } from 'payload/internal'\nimport { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'\n\nimport { cloudStoragePlugin } from './plugin.js'\n\nconst collectionSlug = 'media'\n\ndescribe('cloudStoragePlugin external file fetching', () => {\n const server = createServer((_req, res) => {\n res.writeHead(200, {\n 'Content-Length': '12',\n 'Content-Type': 'text/plain',\n })\n res.end('stored file\\n')\n })\n\n let port: number\n\n beforeAll(async () => {\n await new Promise<void>((resolve) => server.listen(0, resolve))\n port = (server.address() as AddressInfo).port\n })\n\n afterEach(() => {\n vi.unstubAllEnvs()\n })\n\n afterAll(async () => {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => (error ? reject(error) : resolve()))\n })\n })\n\n it('should filter private URLs when Payload file access control is disabled', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig()\n\n await expect(\n fetchExternalFile({ uploadConfig, url: `http://127.0.0.1:${port}/file.txt` }),\n ).rejects.toThrow('Blocked unsafe attempt to 127.0.0.1')\n })\n\n it('should allow private URLs when skipSafeFetch is explicitly true', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig(true)\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://127.0.0.1:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n\n it('should allow private URLs that match an explicit skipSafeFetch allowlist', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig([\n {\n hostname: '127.0.0.1',\n port: String(port),\n protocol: 'http',\n },\n ])\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://127.0.0.1:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n\n it('should allow localhost URLs outside production', async () => {\n vi.stubEnv('NODE_ENV', 'development')\n const uploadConfig = buildUploadConfig()\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://localhost:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n})\n\nfunction buildUploadConfig(skipSafeFetch?: UploadConfig['skipSafeFetch']): UploadConfig {\n const config = cloudStoragePlugin({\n collections: {\n [collectionSlug]: {\n adapter: () => ({\n handleDelete: async () => undefined,\n handleUpload: async () => undefined,\n name: 'test-adapter',\n staticHandler: async () => new Response(null, { status: 404 }),\n }),\n disablePayloadAccessControl: true,\n },\n },\n })({\n collections: [\n {\n fields: [],\n slug: collectionSlug,\n upload: typeof skipSafeFetch === 'undefined' ? true : { skipSafeFetch },\n },\n ],\n } as Config)\n\n const uploadConfig = config.collections?.[0]?.upload\n if (!uploadConfig || typeof uploadConfig !== 'object') {\n throw new Error('Expected an upload configuration')\n }\n\n return uploadConfig\n}\n\nasync function fetchExternalFile({\n uploadConfig,\n url,\n}: {\n uploadConfig: UploadConfig\n url: string\n}) {\n return getExternalFile({\n data: {\n filename: 'file.txt',\n url,\n },\n req: {\n headers: new Headers(),\n payload: {\n config: {\n cookiePrefix: 'payload',\n },\n },\n } as PayloadRequest,\n uploadConfig,\n })\n}\n\nconst adapter = () => ({\n handleDelete: () => undefined,\n handleUpload: () => undefined,\n name: 'test-adapter',\n staticHandler: () => new Response(),\n})\n\ndescribe('cloudStoragePlugin', () => {\n it('should normalize a stored prefix during a server-mediated upload', async () => {\n const config = cloudStoragePlugin({\n collections: {\n media: { adapter },\n },\n } as any)({\n collections: [{ fields: [], slug: 'media', upload: true }],\n } as any)\n const hooks = config.collections?.[0]?.hooks?.beforeChange || []\n const req = {\n context: {},\n file: { mimetype: 'image/png', name: 'photo.png', size: 42 },\n }\n let data = { filename: 'photo.png', prefix: '/tenant/../acme' }\n\n for (const hook of hooks) {\n const result = await hook({ data, operation: 'create', req } as any)\n data = result || data\n }\n\n expect(data.prefix).toBe('tenant/acme')\n })\n})\n"],"names":["createServer","getExternalFile","afterAll","afterEach","beforeAll","describe","expect","it","vi","cloudStoragePlugin","collectionSlug","server","_req","res","writeHead","end","port","Promise","resolve","listen","address","unstubAllEnvs","reject","close","error","stubEnv","uploadConfig","buildUploadConfig","fetchExternalFile","url","rejects","toThrow","file","data","toString","toBe","hostname","String","protocol","skipSafeFetch","config","collections","adapter","handleDelete","undefined","handleUpload","name","staticHandler","Response","status","disablePayloadAccessControl","fields","slug","upload","Error","filename","req","headers","Headers","payload","cookiePrefix","media","hooks","beforeChange","context","mimetype","size","prefix","hook","result","operation"],"mappings":"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"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ClientUploadReceipt } from 'payload/internal';
|
|
2
|
-
type Args = {
|
|
3
|
-
collectionPrefix?: string;
|
|
4
|
-
expectedFileKey: string;
|
|
5
|
-
receipt: ClientUploadReceipt;
|
|
6
|
-
useCompositePrefixes?: boolean;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Asserts a signed client-upload receipt authorizes writing to `expectedFileKey` by recomputing
|
|
10
|
-
* the key from the receipt's own prefix, `_objectKey`, and filename. Collection binding is enforced
|
|
11
|
-
* separately by {@link verifyClientUploadReceipt}.
|
|
12
|
-
*/
|
|
13
|
-
export declare function assertReceiptMatchesFileKey({ collectionPrefix, expectedFileKey, receipt, useCompositePrefixes, }: Args): void;
|
|
14
|
-
export {};
|
|
15
|
-
//# sourceMappingURL=assertReceiptMatchesFileKey.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"assertReceiptMatchesFileKey.d.ts","sourceRoot":"","sources":["../../src/utilities/assertReceiptMatchesFileKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAO3D,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;IACvB,OAAO,EAAE,mBAAmB,CAAA;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,EAC1C,gBAAqB,EACrB,eAAe,EACf,OAAO,EACP,oBAA4B,GAC7B,EAAE,IAAI,GAAG,IAAI,CAqBb"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { APIError } from 'payload';
|
|
2
|
-
import { buildPrefixWithObjectKey } from './buildPrefixWithObjectKey.js';
|
|
3
|
-
import { buildStoragePathData } from './buildStoragePathData.js';
|
|
4
|
-
/**
|
|
5
|
-
* Asserts a signed client-upload receipt authorizes writing to `expectedFileKey` by recomputing
|
|
6
|
-
* the key from the receipt's own prefix, `_objectKey`, and filename. Collection binding is enforced
|
|
7
|
-
* separately by {@link verifyClientUploadReceipt}.
|
|
8
|
-
*/ export function assertReceiptMatchesFileKey({ collectionPrefix = '', expectedFileKey, receipt, useCompositePrefixes = false }) {
|
|
9
|
-
const receiptPrefix = receipt.context.prefix;
|
|
10
|
-
if (typeof receiptPrefix !== 'string') {
|
|
11
|
-
throw new APIError('Client upload reference does not match this request.', 400);
|
|
12
|
-
}
|
|
13
|
-
const objectKey = receipt?.context?._objectKey ?? '';
|
|
14
|
-
if (objectKey && typeof objectKey !== 'string') {
|
|
15
|
-
throw new APIError('Client upload reference does not match this request.', 400);
|
|
16
|
-
}
|
|
17
|
-
const { storageFilePath } = buildStoragePathData({
|
|
18
|
-
collectionPrefix,
|
|
19
|
-
docPrefix: buildPrefixWithObjectKey({
|
|
20
|
-
objectKey,
|
|
21
|
-
prefix: receiptPrefix
|
|
22
|
-
}),
|
|
23
|
-
filename: receipt.filename,
|
|
24
|
-
useCompositePrefixes
|
|
25
|
-
});
|
|
26
|
-
if (storageFilePath !== expectedFileKey) {
|
|
27
|
-
throw new APIError('Client upload reference does not match this request.', 400);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
//# sourceMappingURL=assertReceiptMatchesFileKey.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/assertReceiptMatchesFileKey.ts"],"sourcesContent":["import type { ClientUploadReceipt } from 'payload/internal'\n\nimport { APIError } from 'payload'\n\nimport { buildPrefixWithObjectKey } from './buildPrefixWithObjectKey.js'\nimport { buildStoragePathData } from './buildStoragePathData.js'\n\ntype Args = {\n collectionPrefix?: string\n expectedFileKey: string\n receipt: ClientUploadReceipt\n useCompositePrefixes?: boolean\n}\n\n/**\n * Asserts a signed client-upload receipt authorizes writing to `expectedFileKey` by recomputing\n * the key from the receipt's own prefix, `_objectKey`, and filename. Collection binding is enforced\n * separately by {@link verifyClientUploadReceipt}.\n */\nexport function assertReceiptMatchesFileKey({\n collectionPrefix = '',\n expectedFileKey,\n receipt,\n useCompositePrefixes = false,\n}: Args): void {\n const receiptPrefix = receipt.context.prefix\n if (typeof receiptPrefix !== 'string') {\n throw new APIError('Client upload reference does not match this request.', 400)\n }\n\n const objectKey: string = (receipt?.context?._objectKey as string) ?? ''\n if (objectKey && typeof objectKey !== 'string') {\n throw new APIError('Client upload reference does not match this request.', 400)\n }\n\n const { storageFilePath } = buildStoragePathData({\n collectionPrefix,\n docPrefix: buildPrefixWithObjectKey({ objectKey, prefix: receiptPrefix }),\n filename: receipt.filename,\n useCompositePrefixes,\n })\n\n if (storageFilePath !== expectedFileKey) {\n throw new APIError('Client upload reference does not match this request.', 400)\n }\n}\n"],"names":["APIError","buildPrefixWithObjectKey","buildStoragePathData","assertReceiptMatchesFileKey","collectionPrefix","expectedFileKey","receipt","useCompositePrefixes","receiptPrefix","context","prefix","objectKey","_objectKey","storageFilePath","docPrefix","filename"],"mappings":"AAEA,SAASA,QAAQ,QAAQ,UAAS;AAElC,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,oBAAoB,QAAQ,4BAA2B;AAShE;;;;CAIC,GACD,OAAO,SAASC,4BAA4B,EAC1CC,mBAAmB,EAAE,EACrBC,eAAe,EACfC,OAAO,EACPC,uBAAuB,KAAK,EACvB;IACL,MAAMC,gBAAgBF,QAAQG,OAAO,CAACC,MAAM;IAC5C,IAAI,OAAOF,kBAAkB,UAAU;QACrC,MAAM,IAAIR,SAAS,wDAAwD;IAC7E;IAEA,MAAMW,YAAoB,AAACL,SAASG,SAASG,cAAyB;IACtE,IAAID,aAAa,OAAOA,cAAc,UAAU;QAC9C,MAAM,IAAIX,SAAS,wDAAwD;IAC7E;IAEA,MAAM,EAAEa,eAAe,EAAE,GAAGX,qBAAqB;QAC/CE;QACAU,WAAWb,yBAAyB;YAAEU;YAAWD,QAAQF;QAAc;QACvEO,UAAUT,QAAQS,QAAQ;QAC1BR;IACF;IAEA,IAAIM,oBAAoBR,iBAAiB;QACvC,MAAM,IAAIL,SAAS,wDAAwD;IAC7E;AACF"}
|