@payloadcms/plugin-cloud-storage 3.89.0 → 3.90.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.
Files changed (78) hide show
  1. package/README.md +2 -0
  2. package/dist/exports/client.d.ts +1 -1
  3. package/dist/exports/client.d.ts.map +1 -1
  4. package/dist/exports/client.js +1 -1
  5. package/dist/exports/client.js.map +1 -1
  6. package/dist/exports/utilities.d.ts +3 -2
  7. package/dist/exports/utilities.d.ts.map +1 -1
  8. package/dist/exports/utilities.js +3 -2
  9. package/dist/exports/utilities.js.map +1 -1
  10. package/dist/fields/getFields.d.ts.map +1 -1
  11. package/dist/fields/getFields.js +28 -1
  12. package/dist/fields/getFields.js.map +1 -1
  13. package/dist/hooks/afterChange.d.ts +3 -1
  14. package/dist/hooks/afterChange.d.ts.map +1 -1
  15. package/dist/hooks/afterChange.js +56 -9
  16. package/dist/hooks/afterChange.js.map +1 -1
  17. package/dist/hooks/afterChange.spec.js +150 -0
  18. package/dist/hooks/afterChange.spec.js.map +1 -0
  19. package/dist/hooks/afterDelete.d.ts +3 -1
  20. package/dist/hooks/afterDelete.d.ts.map +1 -1
  21. package/dist/hooks/afterDelete.js +16 -2
  22. package/dist/hooks/afterDelete.js.map +1 -1
  23. package/dist/hooks/afterRead.d.ts.map +1 -1
  24. package/dist/hooks/afterRead.js +13 -2
  25. package/dist/hooks/afterRead.js.map +1 -1
  26. package/dist/hooks/beforeChange.d.ts.map +1 -1
  27. package/dist/hooks/beforeChange.js +10 -1
  28. package/dist/hooks/beforeChange.js.map +1 -1
  29. package/dist/hooks/normalizeUploadPrefix.d.ts +11 -0
  30. package/dist/hooks/normalizeUploadPrefix.d.ts.map +1 -0
  31. package/dist/hooks/normalizeUploadPrefix.js +42 -0
  32. package/dist/hooks/normalizeUploadPrefix.js.map +1 -0
  33. package/dist/hooks/normalizeUploadPrefix.spec.js +154 -0
  34. package/dist/hooks/normalizeUploadPrefix.spec.js.map +1 -0
  35. package/dist/plugin.d.ts.map +1 -1
  36. package/dist/plugin.js +15 -6
  37. package/dist/plugin.js.map +1 -1
  38. package/dist/plugin.spec.js +166 -0
  39. package/dist/plugin.spec.js.map +1 -0
  40. package/dist/types.d.ts +23 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/utilities/assertReceiptMatchesFileKey.d.ts +15 -0
  44. package/dist/utilities/assertReceiptMatchesFileKey.d.ts.map +1 -0
  45. package/dist/utilities/assertReceiptMatchesFileKey.js +31 -0
  46. package/dist/utilities/assertReceiptMatchesFileKey.js.map +1 -0
  47. package/dist/utilities/buildPrefixWithObjectKey.d.ts +8 -0
  48. package/dist/utilities/buildPrefixWithObjectKey.d.ts.map +1 -0
  49. package/dist/utilities/buildPrefixWithObjectKey.js +13 -0
  50. package/dist/utilities/buildPrefixWithObjectKey.js.map +1 -0
  51. package/dist/utilities/buildStoragePathData.d.ts +55 -0
  52. package/dist/utilities/buildStoragePathData.d.ts.map +1 -0
  53. package/dist/utilities/buildStoragePathData.js +69 -0
  54. package/dist/utilities/buildStoragePathData.js.map +1 -0
  55. package/dist/utilities/buildStoragePathData.spec.js +254 -0
  56. package/dist/utilities/buildStoragePathData.spec.js.map +1 -0
  57. package/dist/utilities/getFilePrefix.d.ts +27 -11
  58. package/dist/utilities/getFilePrefix.d.ts.map +1 -1
  59. package/dist/utilities/getFilePrefix.js +75 -31
  60. package/dist/utilities/getFilePrefix.js.map +1 -1
  61. package/dist/utilities/getFilePrefix.spec.js +221 -35
  62. package/dist/utilities/getFilePrefix.spec.js.map +1 -1
  63. package/dist/utilities/resolveSignedURLKey.d.ts +11 -0
  64. package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
  65. package/dist/utilities/resolveSignedURLKey.js +43 -8
  66. package/dist/utilities/resolveSignedURLKey.js.map +1 -1
  67. package/dist/utilities/resolveSignedURLKey.spec.js +56 -0
  68. package/dist/utilities/resolveSignedURLKey.spec.js.map +1 -0
  69. package/dist/utilities/sanitizePrefix.d.ts +2 -2
  70. package/dist/utilities/sanitizePrefix.js +4 -4
  71. package/dist/utilities/sanitizePrefix.js.map +1 -1
  72. package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts +20 -0
  73. package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts.map +1 -0
  74. package/dist/utilities/verifyClientUploadReceiptForFileKey.js +24 -0
  75. package/dist/utilities/verifyClientUploadReceiptForFileKey.js.map +1 -0
  76. package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js +108 -0
  77. package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js.map +1 -0
  78. package/package.json +4 -4
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/buildStoragePathData.spec.ts"],"sourcesContent":["import {\n buildStoragePathData,\n buildUploadStoragePathData,\n isStoragePathWithinCollectionPrefix,\n} from './buildStoragePathData.js'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('buildStoragePathData', () => {\n describe('non-composite mode (useCompositePrefixes: false)', () => {\n it('should preserve the key of a legacy document outside the collection prefix', () => {\n const result = buildStoragePathData({\n collectionPrefix: 'media/',\n docPrefix: 'media-archive',\n filename: 'test.png',\n useCompositePrefixes: false,\n })\n\n expect(result).toEqual({\n fileKey: 'media-archive/test.png',\n sanitizedCollectionPrefix: 'media',\n sanitizedDocPrefix: 'media-archive',\n sanitizedFilename: 'test.png',\n storageFilePath: 'media-archive/test.png',\n })\n })\n\n it('should fallback to collectionPrefix when docPrefix is empty', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'collection/test.png',\n })\n })\n\n it('should fallback to collectionPrefix when docPrefix is undefined', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'collection/test.png',\n })\n })\n\n it('should return only filename when both prefixes are empty', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'test.png',\n })\n })\n })\n\n describe('composite mode (useCompositePrefixes: true)', () => {\n it('should combine collectionPrefix and docPrefix', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'collection/document/test.png',\n })\n })\n\n it('should work with only collectionPrefix', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'collection/test.png',\n })\n })\n\n it('should work with only docPrefix', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'document/test.png',\n })\n })\n\n it('should return only filename when both prefixes are empty', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'test.png',\n })\n })\n })\n\n describe('collection prefix boundaries', () => {\n it('should accept a file key within a slash-terminated collection prefix', () => {\n expect(\n isStoragePathWithinCollectionPrefix({\n collectionPrefix: 'media/',\n docPrefix: 'media/documents/test.png',\n }),\n ).toBe(true)\n })\n })\n\n describe('sanitization', () => {\n it.each(['./media/./images', 'media\\\\images', '/media//images'])(\n 'should canonicalize equivalent prefix paths',\n (docPrefix) => {\n const result = buildStoragePathData({ docPrefix, filename: 'test.png' })\n\n expect(result).toEqual({\n fileKey: 'media/images/test.png',\n sanitizedCollectionPrefix: '',\n sanitizedDocPrefix: 'media/images',\n sanitizedFilename: 'test.png',\n storageFilePath: 'media/images/test.png',\n })\n },\n )\n\n it('should remove path traversal segments from collectionPrefix', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'etc/test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n\n it('should remove path traversal segments from docPrefix', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'a/outside/test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n\n it('should remove control characters from prefixes', () => {\n const result = buildStoragePathData({\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 storageFilePath: 'testprefix/test.png',\n })\n expect(result.fileKey).not.toMatch(/[\\x00-\\x1f]/)\n })\n\n it('should normalize both prefixes after removing control characters in composite mode', () => {\n const result = buildStoragePathData({\n collectionPrefix: '.\\x00./collection',\n docPrefix: '.\\x00./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 storageFilePath: 'collection/doc/test.png',\n })\n expect(result.fileKey).not.toContain('..')\n })\n })\n})\n\ndescribe('buildUploadStoragePathData', () => {\n it.each([\n ['invoices', false, 'media/invoices/file.png', 'media/invoices'],\n ['media/invoices', false, 'media/invoices/file.png', 'media/invoices'],\n ['', false, 'media/file.png', ''],\n ['invoices', true, 'media/invoices/file.png', 'invoices'],\n ] as const)(\n 'should persist a readable destination for %s (composite %s)',\n (docPrefix, useCompositePrefixes, fileKey, storedPrefix) => {\n const result = buildUploadStoragePathData({\n collectionPrefix: 'media',\n docPrefix,\n filename: 'file.png',\n useCompositePrefixes,\n })\n\n expect(result.storageFilePath).toBe(fileKey)\n expect(result.sanitizedDocPrefix).toBe(storedPrefix)\n expect(\n buildStoragePathData({\n collectionPrefix: 'media',\n docPrefix: result.sanitizedDocPrefix,\n filename: 'file.png',\n useCompositePrefixes,\n }).fileKey,\n ).toBe(fileKey)\n },\n )\n})\n"],"names":["buildStoragePathData","buildUploadStoragePathData","isStoragePathWithinCollectionPrefix","describe","expect","it","result","collectionPrefix","docPrefix","filename","useCompositePrefixes","toEqual","fileKey","sanitizedCollectionPrefix","sanitizedDocPrefix","sanitizedFilename","storageFilePath","toBe","each","not","toContain","toMatch","storedPrefix"],"mappings":"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"}
@@ -1,20 +1,36 @@
1
- import type { CollectionConfig, PayloadRequest } from 'payload';
1
+ import type { CollectionConfig, PayloadRequest, TypeWithID } from 'payload';
2
2
  /**
3
- * Resolves the file prefix from the highest-priority available source and
4
- * always returns a sanitized value.
3
+ * Resolves the folder a stored object lives under (semantic `prefix` plus `_objectKey`).
5
4
  *
6
- * Resolution order:
7
- * 1. `prefixQueryParam`
8
- * 2. `clientUploadContext.prefix`
9
- * 3. Stored document `prefix` from the database
10
- *
11
- * Resolved values are passed through `sanitizePrefix`.
5
+ * An already-authorized `doc` wins outright. Otherwise a verified `clientUploadContext` is used
6
+ * (its `prefix` re-contained with {@link buildUploadStoragePathData}, since it hasn't been verified against
7
+ * a signed receipt by every adapter). Otherwise the document is resolved with
8
+ * `overrideAccess: false`, any query-param prefix only narrows that lookup, and `_objectKey` is
9
+ * read from the resolved document.
12
10
  */
13
- export declare function getFilePrefix({ clientUploadContext, collection, filename, prefixQueryParam, req, }: {
11
+ type GetFilePrefixArgs = {
14
12
  clientUploadContext?: unknown;
15
13
  collection: CollectionConfig;
14
+ collectionPrefix?: string;
15
+ doc?: {
16
+ _objectKey?: string;
17
+ prefix?: string;
18
+ } & TypeWithID;
16
19
  filename: string;
20
+ /**
21
+ * Only narrows the access-controlled fallback lookup below; never trusted or
22
+ * returned directly. Prefer passing the access-checked document as `doc` instead.
23
+ */
17
24
  prefixQueryParam?: string;
18
25
  req: PayloadRequest;
19
- }): Promise<string>;
26
+ useCompositePrefixes?: boolean;
27
+ };
28
+ export declare function getFilePrefix({ clientUploadContext, collection, collectionPrefix, doc, filename, prefixQueryParam, req, useCompositePrefixes, }: GetFilePrefixArgs): Promise<string>;
29
+ /**
30
+ * Serve-path helper: resolves the authorized folder with {@link getFilePrefix}, then returns the
31
+ * full object key via {@link buildStoragePathData}. Read handlers use this so a key is never built from an
32
+ * unresolved prefix.
33
+ */
34
+ export declare function getStorageFilePath(args: GetFilePrefixArgs): Promise<string>;
35
+ export {};
20
36
  //# sourceMappingURL=getFilePrefix.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"getFilePrefix.d.ts","sourceRoot":"","sources":["../../src/utilities/getFilePrefix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAA;AAI7E;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,EAClC,mBAAmB,EACnB,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,GAAG,GACJ,EAAE;IACD,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,GAAG,EAAE,cAAc,CAAA;CACpB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoClB"}
1
+ {"version":3,"file":"getFilePrefix.d.ts","sourceRoot":"","sources":["../../src/utilities/getFilePrefix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAgB,MAAM,SAAS,CAAA;AAMzF;;;;;;;;GAQG;AAEH,KAAK,iBAAiB,GAAG;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,GAAG,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,UAAU,CAAA;IAC3D,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,GAAG,EAAE,cAAc,CAAA;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AACD,wBAAsB,aAAa,CAAC,EAClC,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,GAAG,EACH,QAAQ,EACR,gBAAgB,EAChB,GAAG,EACH,oBAA4B,GAC7B,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuErC;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CASjF"}
@@ -1,46 +1,90 @@
1
+ import { buildPrefixWithObjectKey } from './buildPrefixWithObjectKey.js';
2
+ import { buildStoragePathData, buildUploadStoragePathData } from './buildStoragePathData.js';
1
3
  import { sanitizePrefix } from './sanitizePrefix.js';
2
- /**
3
- * Resolves the file prefix from the highest-priority available source and
4
- * always returns a sanitized value.
5
- *
6
- * Resolution order:
7
- * 1. `prefixQueryParam`
8
- * 2. `clientUploadContext.prefix`
9
- * 3. Stored document `prefix` from the database
10
- *
11
- * Resolved values are passed through `sanitizePrefix`.
12
- */ export async function getFilePrefix({ clientUploadContext, collection, filename, prefixQueryParam, req }) {
13
- if (typeof prefixQueryParam === 'string') {
14
- return sanitizePrefix(prefixQueryParam);
4
+ export async function getFilePrefix({ clientUploadContext, collection, collectionPrefix, doc, filename, prefixQueryParam, req, useCompositePrefixes = false }) {
5
+ // The serve path already loaded and authorized this document — trust it over any
6
+ // client-supplied upload context or query prefix.
7
+ if (doc) {
8
+ return buildPrefixWithObjectKey({
9
+ objectKey: doc._objectKey,
10
+ prefix: doc.prefix
11
+ });
15
12
  }
16
- // Prioritize from clientUploadContext if there is:
13
+ // Client uploads call handlers without a document yet. Re-contain the claimed
14
+ // prefix — it hasn't been verified against a signed receipt by every adapter.
17
15
  if (clientUploadContext && typeof clientUploadContext === 'object' && 'prefix' in clientUploadContext && typeof clientUploadContext.prefix === 'string') {
18
- return sanitizePrefix(clientUploadContext.prefix);
16
+ const contextObjectKey = '_objectKey' in clientUploadContext ? clientUploadContext._objectKey : undefined;
17
+ const containedPrefix = buildUploadStoragePathData({
18
+ collectionPrefix,
19
+ docPrefix: clientUploadContext.prefix,
20
+ filename,
21
+ useCompositePrefixes
22
+ }).sanitizedDocPrefix;
23
+ return buildPrefixWithObjectKey({
24
+ objectKey: contextObjectKey,
25
+ prefix: containedPrefix
26
+ });
19
27
  }
28
+ // Reads without a query prefix or read-access constraints skip the endpoint's document lookup.
20
29
  const imageSizes = collection?.upload?.imageSizes || [];
30
+ const filenameClause = {
31
+ or: [
32
+ {
33
+ filename: {
34
+ equals: filename
35
+ }
36
+ },
37
+ ...imageSizes.map((imageSize)=>({
38
+ [`sizes.${imageSize.name}.filename`]: {
39
+ equals: filename
40
+ }
41
+ }))
42
+ ]
43
+ };
44
+ // Only filter by prefix when the collection persists a `prefix` field (querying a missing field throws).
45
+ const hasPrefixField = collection.fields?.some((field)=>'name' in field && field.name === 'prefix');
46
+ const where = typeof prefixQueryParam === 'string' && hasPrefixField ? {
47
+ and: [
48
+ filenameClause,
49
+ {
50
+ prefix: {
51
+ equals: sanitizePrefix(prefixQueryParam)
52
+ }
53
+ }
54
+ ]
55
+ } : filenameClause;
21
56
  const files = await req.payload.find({
22
57
  collection: collection.slug,
23
58
  depth: 0,
24
59
  draft: true,
25
60
  limit: 1,
61
+ overrideAccess: false,
26
62
  pagination: false,
27
- where: {
28
- or: [
29
- {
30
- filename: {
31
- equals: filename
32
- }
33
- },
34
- ...imageSizes.map((imageSize)=>({
35
- [`sizes.${imageSize.name}.filename`]: {
36
- equals: filename
37
- }
38
- }))
39
- ]
40
- }
63
+ req,
64
+ select: {
65
+ _objectKey: true,
66
+ prefix: true
67
+ },
68
+ where
69
+ });
70
+ const found = files?.docs?.[0];
71
+ return buildPrefixWithObjectKey({
72
+ objectKey: found?._objectKey,
73
+ prefix: found?.prefix
41
74
  });
42
- const prefix = files?.docs?.[0]?.prefix;
43
- return prefix ? sanitizePrefix(prefix) : '';
75
+ }
76
+ /**
77
+ * Serve-path helper: resolves the authorized folder with {@link getFilePrefix}, then returns the
78
+ * full object key via {@link buildStoragePathData}. Read handlers use this so a key is never built from an
79
+ * unresolved prefix.
80
+ */ export async function getStorageFilePath(args) {
81
+ const docPrefix = await getFilePrefix(args);
82
+ return buildStoragePathData({
83
+ collectionPrefix: args.collectionPrefix,
84
+ docPrefix,
85
+ filename: args.filename,
86
+ useCompositePrefixes: args.useCompositePrefixes
87
+ }).storageFilePath;
44
88
  }
45
89
 
46
90
  //# sourceMappingURL=getFilePrefix.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/getFilePrefix.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest, UploadConfig } from 'payload'\n\nimport { sanitizePrefix } from './sanitizePrefix.js'\n\n/**\n * Resolves the file prefix from the highest-priority available source and\n * always returns a sanitized value.\n *\n * Resolution order:\n * 1. `prefixQueryParam`\n * 2. `clientUploadContext.prefix`\n * 3. Stored document `prefix` from the database\n *\n * Resolved values are passed through `sanitizePrefix`.\n */\nexport async function getFilePrefix({\n clientUploadContext,\n collection,\n filename,\n prefixQueryParam,\n req,\n}: {\n clientUploadContext?: unknown\n collection: CollectionConfig\n filename: string\n prefixQueryParam?: string\n req: PayloadRequest\n}): Promise<string> {\n if (typeof prefixQueryParam === 'string') {\n return sanitizePrefix(prefixQueryParam)\n }\n\n // Prioritize from clientUploadContext if there is:\n if (\n clientUploadContext &&\n typeof clientUploadContext === 'object' &&\n 'prefix' in clientUploadContext &&\n typeof clientUploadContext.prefix === 'string'\n ) {\n return sanitizePrefix(clientUploadContext.prefix)\n }\n\n const imageSizes = (collection?.upload as UploadConfig)?.imageSizes || []\n\n const files = await req.payload.find({\n collection: collection.slug,\n depth: 0,\n draft: true,\n limit: 1,\n pagination: false,\n where: {\n or: [\n {\n filename: { equals: filename },\n },\n ...imageSizes.map((imageSize) => ({\n [`sizes.${imageSize.name}.filename`]: { equals: filename },\n })),\n ],\n },\n })\n const prefix = files?.docs?.[0]?.prefix\n return prefix ? sanitizePrefix(prefix as string) : ''\n}\n"],"names":["sanitizePrefix","getFilePrefix","clientUploadContext","collection","filename","prefixQueryParam","req","prefix","imageSizes","upload","files","payload","find","slug","depth","draft","limit","pagination","where","or","equals","map","imageSize","name","docs"],"mappings":"AAEA,SAASA,cAAc,QAAQ,sBAAqB;AAEpD;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,cAAc,EAClCC,mBAAmB,EACnBC,UAAU,EACVC,QAAQ,EACRC,gBAAgB,EAChBC,GAAG,EAOJ;IACC,IAAI,OAAOD,qBAAqB,UAAU;QACxC,OAAOL,eAAeK;IACxB;IAEA,mDAAmD;IACnD,IACEH,uBACA,OAAOA,wBAAwB,YAC/B,YAAYA,uBACZ,OAAOA,oBAAoBK,MAAM,KAAK,UACtC;QACA,OAAOP,eAAeE,oBAAoBK,MAAM;IAClD;IAEA,MAAMC,aAAa,AAACL,YAAYM,QAAyBD,cAAc,EAAE;IAEzE,MAAME,QAAQ,MAAMJ,IAAIK,OAAO,CAACC,IAAI,CAAC;QACnCT,YAAYA,WAAWU,IAAI;QAC3BC,OAAO;QACPC,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,OAAO;YACLC,IAAI;gBACF;oBACEf,UAAU;wBAAEgB,QAAQhB;oBAAS;gBAC/B;mBACGI,WAAWa,GAAG,CAAC,CAACC,YAAe,CAAA;wBAChC,CAAC,CAAC,MAAM,EAAEA,UAAUC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE;4BAAEH,QAAQhB;wBAAS;oBAC3D,CAAA;aACD;QACH;IACF;IACA,MAAMG,SAASG,OAAOc,MAAM,CAAC,EAAE,EAAEjB;IACjC,OAAOA,SAASP,eAAeO,UAAoB;AACrD"}
1
+ {"version":3,"sources":["../../src/utilities/getFilePrefix.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest, TypeWithID, UploadConfig } from 'payload'\n\nimport { buildPrefixWithObjectKey } from './buildPrefixWithObjectKey.js'\nimport { buildStoragePathData, buildUploadStoragePathData } from './buildStoragePathData.js'\nimport { sanitizePrefix } from './sanitizePrefix.js'\n\n/**\n * Resolves the folder a stored object lives under (semantic `prefix` plus `_objectKey`).\n *\n * An already-authorized `doc` wins outright. Otherwise a verified `clientUploadContext` is used\n * (its `prefix` re-contained with {@link buildUploadStoragePathData}, since it hasn't been verified against\n * a signed receipt by every adapter). Otherwise the document is resolved with\n * `overrideAccess: false`, any query-param prefix only narrows that lookup, and `_objectKey` is\n * read from the resolved document.\n */\n\ntype GetFilePrefixArgs = {\n clientUploadContext?: unknown\n collection: CollectionConfig\n collectionPrefix?: string\n doc?: { _objectKey?: string; prefix?: string } & TypeWithID\n filename: string\n /**\n * Only narrows the access-controlled fallback lookup below; never trusted or\n * returned directly. Prefer passing the access-checked document as `doc` instead.\n */\n prefixQueryParam?: string\n req: PayloadRequest\n useCompositePrefixes?: boolean\n}\nexport async function getFilePrefix({\n clientUploadContext,\n collection,\n collectionPrefix,\n doc,\n filename,\n prefixQueryParam,\n req,\n useCompositePrefixes = false,\n}: GetFilePrefixArgs): Promise<string> {\n // The serve path already loaded and authorized this document — trust it over any\n // client-supplied upload context or query prefix.\n if (doc) {\n return buildPrefixWithObjectKey({ objectKey: doc._objectKey, prefix: doc.prefix })\n }\n\n // Client uploads call handlers without a document yet. Re-contain the claimed\n // prefix — it hasn't been verified against a signed receipt by every adapter.\n if (\n clientUploadContext &&\n typeof clientUploadContext === 'object' &&\n 'prefix' in clientUploadContext &&\n typeof clientUploadContext.prefix === 'string'\n ) {\n const contextObjectKey: string | undefined =\n '_objectKey' in clientUploadContext\n ? ((clientUploadContext as { _objectKey?: unknown })._objectKey as string)\n : undefined\n const containedPrefix = buildUploadStoragePathData({\n collectionPrefix,\n docPrefix: clientUploadContext.prefix,\n filename,\n useCompositePrefixes,\n }).sanitizedDocPrefix\n return buildPrefixWithObjectKey({\n objectKey: contextObjectKey,\n prefix: containedPrefix,\n })\n }\n\n // Reads without a query prefix or read-access constraints skip the endpoint's document lookup.\n const imageSizes = (collection?.upload as UploadConfig)?.imageSizes || []\n\n const filenameClause = {\n or: [\n {\n filename: { equals: filename },\n },\n ...imageSizes.map((imageSize) => ({\n [`sizes.${imageSize.name}.filename`]: { equals: filename },\n })),\n ],\n }\n\n // Only filter by prefix when the collection persists a `prefix` field (querying a missing field throws).\n const hasPrefixField = collection.fields?.some(\n (field) => 'name' in field && field.name === 'prefix',\n )\n const where =\n typeof prefixQueryParam === 'string' && hasPrefixField\n ? { and: [filenameClause, { prefix: { equals: sanitizePrefix(prefixQueryParam) } }] }\n : filenameClause\n\n const files = await req.payload.find({\n collection: collection.slug,\n depth: 0,\n draft: true,\n limit: 1,\n overrideAccess: false,\n pagination: false,\n req,\n select: {\n _objectKey: true,\n prefix: true,\n },\n where,\n })\n\n const found = files?.docs?.[0] as { _objectKey?: string; prefix?: string } | undefined\n return buildPrefixWithObjectKey({ objectKey: found?._objectKey, prefix: found?.prefix })\n}\n\n/**\n * Serve-path helper: resolves the authorized folder with {@link getFilePrefix}, then returns the\n * full object key via {@link buildStoragePathData}. Read handlers use this so a key is never built from an\n * unresolved prefix.\n */\nexport async function getStorageFilePath(args: GetFilePrefixArgs): Promise<string> {\n const docPrefix = await getFilePrefix(args)\n\n return buildStoragePathData({\n collectionPrefix: args.collectionPrefix,\n docPrefix,\n filename: args.filename,\n useCompositePrefixes: args.useCompositePrefixes,\n }).storageFilePath\n}\n"],"names":["buildPrefixWithObjectKey","buildStoragePathData","buildUploadStoragePathData","sanitizePrefix","getFilePrefix","clientUploadContext","collection","collectionPrefix","doc","filename","prefixQueryParam","req","useCompositePrefixes","objectKey","_objectKey","prefix","contextObjectKey","undefined","containedPrefix","docPrefix","sanitizedDocPrefix","imageSizes","upload","filenameClause","or","equals","map","imageSize","name","hasPrefixField","fields","some","field","where","and","files","payload","find","slug","depth","draft","limit","overrideAccess","pagination","select","found","docs","getStorageFilePath","args","storageFilePath"],"mappings":"AAEA,SAASA,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,oBAAoB,EAAEC,0BAA0B,QAAQ,4BAA2B;AAC5F,SAASC,cAAc,QAAQ,sBAAqB;AA0BpD,OAAO,eAAeC,cAAc,EAClCC,mBAAmB,EACnBC,UAAU,EACVC,gBAAgB,EAChBC,GAAG,EACHC,QAAQ,EACRC,gBAAgB,EAChBC,GAAG,EACHC,uBAAuB,KAAK,EACV;IAClB,iFAAiF;IACjF,kDAAkD;IAClD,IAAIJ,KAAK;QACP,OAAOR,yBAAyB;YAAEa,WAAWL,IAAIM,UAAU;YAAEC,QAAQP,IAAIO,MAAM;QAAC;IAClF;IAEA,8EAA8E;IAC9E,8EAA8E;IAC9E,IACEV,uBACA,OAAOA,wBAAwB,YAC/B,YAAYA,uBACZ,OAAOA,oBAAoBU,MAAM,KAAK,UACtC;QACA,MAAMC,mBACJ,gBAAgBX,sBACX,AAACA,oBAAiDS,UAAU,GAC7DG;QACN,MAAMC,kBAAkBhB,2BAA2B;YACjDK;YACAY,WAAWd,oBAAoBU,MAAM;YACrCN;YACAG;QACF,GAAGQ,kBAAkB;QACrB,OAAOpB,yBAAyB;YAC9Ba,WAAWG;YACXD,QAAQG;QACV;IACF;IAEA,+FAA+F;IAC/F,MAAMG,aAAa,AAACf,YAAYgB,QAAyBD,cAAc,EAAE;IAEzE,MAAME,iBAAiB;QACrBC,IAAI;YACF;gBACEf,UAAU;oBAAEgB,QAAQhB;gBAAS;YAC/B;eACGY,WAAWK,GAAG,CAAC,CAACC,YAAe,CAAA;oBAChC,CAAC,CAAC,MAAM,EAAEA,UAAUC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE;wBAAEH,QAAQhB;oBAAS;gBAC3D,CAAA;SACD;IACH;IAEA,yGAAyG;IACzG,MAAMoB,iBAAiBvB,WAAWwB,MAAM,EAAEC,KACxC,CAACC,QAAU,UAAUA,SAASA,MAAMJ,IAAI,KAAK;IAE/C,MAAMK,QACJ,OAAOvB,qBAAqB,YAAYmB,iBACpC;QAAEK,KAAK;YAACX;YAAgB;gBAAER,QAAQ;oBAAEU,QAAQtB,eAAeO;gBAAkB;YAAE;SAAE;IAAC,IAClFa;IAEN,MAAMY,QAAQ,MAAMxB,IAAIyB,OAAO,CAACC,IAAI,CAAC;QACnC/B,YAAYA,WAAWgC,IAAI;QAC3BC,OAAO;QACPC,OAAO;QACPC,OAAO;QACPC,gBAAgB;QAChBC,YAAY;QACZhC;QACAiC,QAAQ;YACN9B,YAAY;YACZC,QAAQ;QACV;QACAkB;IACF;IAEA,MAAMY,QAAQV,OAAOW,MAAM,CAAC,EAAE;IAC9B,OAAO9C,yBAAyB;QAAEa,WAAWgC,OAAO/B;QAAYC,QAAQ8B,OAAO9B;IAAO;AACxF;AAEA;;;;CAIC,GACD,OAAO,eAAegC,mBAAmBC,IAAuB;IAC9D,MAAM7B,YAAY,MAAMf,cAAc4C;IAEtC,OAAO/C,qBAAqB;QAC1BM,kBAAkByC,KAAKzC,gBAAgB;QACvCY;QACAV,UAAUuC,KAAKvC,QAAQ;QACvBG,sBAAsBoC,KAAKpC,oBAAoB;IACjD,GAAGqC,eAAe;AACpB"}
@@ -1,80 +1,226 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { getFilePrefix } from './getFilePrefix.js';
3
- const makeReq = (docs = [])=>({
3
+ const makeReq = ({ docs = [] } = {})=>({
4
4
  payload: {
5
5
  find: vi.fn().mockResolvedValue({
6
6
  docs
7
7
  })
8
8
  }
9
9
  });
10
- const makeCollection = ()=>({
10
+ const makeCollection = ({ hasPrefixField = false } = {})=>({
11
11
  slug: 'media',
12
+ fields: hasPrefixField ? [
13
+ {
14
+ name: 'prefix',
15
+ type: 'text'
16
+ }
17
+ ] : [],
12
18
  upload: {}
13
19
  });
20
+ const whereOf = (req)=>JSON.stringify(req.payload.find.mock.calls[0][0].where);
14
21
  describe('getFilePrefix', ()=>{
15
- describe('prefixQueryParam shortcut', ()=>{
16
- it('should return prefixQueryParam immediately without querying the database', async ()=>{
17
- const req = makeReq();
22
+ it('prefers an already-authorized document over a client upload context or query prefix', async ()=>{
23
+ const req = makeReq({
24
+ docs: [
25
+ {
26
+ prefix: 'db-prefix'
27
+ }
28
+ ]
29
+ });
30
+ const doc = {
31
+ id: 1,
32
+ prefix: 'invoices'
33
+ };
34
+ const result = await getFilePrefix({
35
+ clientUploadContext: {
36
+ prefix: 'reference-prefix'
37
+ },
38
+ collection: makeCollection(),
39
+ collectionPrefix: 'media',
40
+ doc,
41
+ filename: 'logo.png',
42
+ prefixQueryParam: 'query-prefix',
43
+ req
44
+ });
45
+ expect(result).toBe('invoices');
46
+ expect(req.payload.find).not.toHaveBeenCalled();
47
+ });
48
+ it('does not fall back to a lookup when the checked document has no prefix', async ()=>{
49
+ const req = makeReq({
50
+ docs: [
51
+ {
52
+ prefix: 'db-prefix'
53
+ }
54
+ ]
55
+ });
56
+ const result = await getFilePrefix({
57
+ collection: makeCollection(),
58
+ collectionPrefix: 'media',
59
+ doc: {
60
+ id: 1
61
+ },
62
+ filename: 'logo.png',
63
+ req
64
+ });
65
+ expect(result).toBe('');
66
+ expect(req.payload.find).not.toHaveBeenCalled();
67
+ });
68
+ it('contains a client upload context prefix beneath the collection prefix', async ()=>{
69
+ const req = makeReq({
70
+ docs: [
71
+ {
72
+ prefix: 'invoices'
73
+ }
74
+ ]
75
+ });
76
+ const result = await getFilePrefix({
77
+ clientUploadContext: {
78
+ prefix: 'invoices'
79
+ },
80
+ collection: makeCollection(),
81
+ collectionPrefix: 'media',
82
+ filename: 'file.png',
83
+ req
84
+ });
85
+ expect(result).toBe('media/invoices');
86
+ expect(req.payload.find).not.toHaveBeenCalled();
87
+ });
88
+ describe('authorized document (trusted)', ()=>{
89
+ it('folds the object key into the document prefix and skips the database', async ()=>{
90
+ const req = makeReq({
91
+ docs: [
92
+ {
93
+ prefix: 'db-prefix'
94
+ }
95
+ ]
96
+ });
18
97
  const result = await getFilePrefix({
19
98
  collection: makeCollection(),
20
- prefixQueryParam: 'uuid-prefix',
99
+ doc: {
100
+ id: 1,
101
+ _objectKey: 'abc123',
102
+ prefix: 'doc-prefix'
103
+ },
21
104
  filename: 'logo.png',
22
105
  req
23
106
  });
24
- expect(result).toBe('uuid-prefix');
107
+ expect(result).toBe('doc-prefix/abc123');
25
108
  expect(req.payload.find).not.toHaveBeenCalled();
26
109
  });
27
- it('should return an empty string when prefixQueryParam is an empty string', async ()=>{
110
+ it('returns the document prefix alone when it has no object key', async ()=>{
28
111
  const req = makeReq();
29
112
  const result = await getFilePrefix({
30
113
  collection: makeCollection(),
31
- prefixQueryParam: '',
114
+ doc: {
115
+ id: 1,
116
+ prefix: 'doc-prefix'
117
+ },
32
118
  filename: 'logo.png',
33
119
  req
34
120
  });
35
- expect(result).toBe('');
36
- expect(req.payload.find).not.toHaveBeenCalled();
121
+ expect(result).toBe('doc-prefix');
37
122
  });
38
- it('should reject multi-encoded values', async ()=>{
123
+ it('sanitizes the document prefix', async ()=>{
39
124
  const req = makeReq();
40
125
  const result = await getFilePrefix({
41
126
  collection: makeCollection(),
42
- prefixQueryParam: '%252e%252e%252fsecret%252f..%252fok',
127
+ doc: {
128
+ id: 1,
129
+ prefix: '../secret'
130
+ },
43
131
  filename: 'logo.png',
44
132
  req
45
133
  });
46
- expect(result).toBe('');
47
- expect(req.payload.find).not.toHaveBeenCalled();
134
+ expect(result).toBe('secret');
48
135
  });
49
- it('should reject malformed percent-encoding', async ()=>{
136
+ });
137
+ describe('verified clientUploadContext (trusted)', ()=>{
138
+ it('folds the server-owned object key into the prefix and skips the database', async ()=>{
50
139
  const req = makeReq();
51
140
  const result = await getFilePrefix({
141
+ clientUploadContext: {
142
+ _objectKey: 'abc123',
143
+ prefix: 'documents'
144
+ },
52
145
  collection: makeCollection(),
53
- prefixQueryParam: '%E0%A4%A',
54
146
  filename: 'logo.png',
55
147
  req
56
148
  });
57
- expect(result).toBe('');
149
+ expect(result).toBe('documents/abc123');
58
150
  expect(req.payload.find).not.toHaveBeenCalled();
59
151
  });
60
- });
61
- describe('database fallback', ()=>{
62
- it('should query the database when prefixQueryParam is not provided', async ()=>{
63
- const req = makeReq([
64
- {
65
- prefix: 'db-prefix'
66
- }
67
- ]);
152
+ it('returns the prefix alone when the context has no object key', async ()=>{
153
+ const req = makeReq();
68
154
  const result = await getFilePrefix({
155
+ clientUploadContext: {
156
+ prefix: 'documents'
157
+ },
69
158
  collection: makeCollection(),
70
159
  filename: 'logo.png',
71
160
  req
72
161
  });
73
- expect(result).toBe('db-prefix');
162
+ expect(result).toBe('documents');
163
+ });
164
+ });
165
+ describe('access-controlled database resolution', ()=>{
166
+ it('does NOT short-circuit on a client-supplied prefix — the access-controlled query still runs', async ()=>{
167
+ const req = makeReq({
168
+ docs: [
169
+ {
170
+ _objectKey: 'abc123',
171
+ prefix: 'documents'
172
+ }
173
+ ]
174
+ });
175
+ await getFilePrefix({
176
+ collection: makeCollection({
177
+ hasPrefixField: true
178
+ }),
179
+ filename: 'logo.png',
180
+ prefixQueryParam: 'documents',
181
+ req
182
+ });
183
+ // A client-supplied prefix must not bypass the access-controlled lookup.
74
184
  expect(req.payload.find).toHaveBeenCalledOnce();
75
185
  });
76
- it('should return empty string when no document is found', async ()=>{
77
- const req = makeReq([]);
186
+ it('queries with access control enabled', async ()=>{
187
+ const req = makeReq({
188
+ docs: [
189
+ {
190
+ prefix: 'db-prefix'
191
+ }
192
+ ]
193
+ });
194
+ await getFilePrefix({
195
+ collection: makeCollection(),
196
+ filename: 'logo.png',
197
+ req
198
+ });
199
+ expect(req.payload.find).toHaveBeenCalledWith(expect.objectContaining({
200
+ overrideAccess: false,
201
+ req
202
+ }));
203
+ });
204
+ it('folds the stored object key into the resolved prefix', async ()=>{
205
+ const req = makeReq({
206
+ docs: [
207
+ {
208
+ _objectKey: 'abc123',
209
+ prefix: 'db-prefix'
210
+ }
211
+ ]
212
+ });
213
+ const result = await getFilePrefix({
214
+ collection: makeCollection(),
215
+ filename: 'logo.png',
216
+ req
217
+ });
218
+ expect(result).toBe('db-prefix/abc123');
219
+ });
220
+ it('returns an empty string when no document is found', async ()=>{
221
+ const req = makeReq({
222
+ docs: []
223
+ });
78
224
  const result = await getFilePrefix({
79
225
  collection: makeCollection(),
80
226
  filename: 'logo.png',
@@ -82,12 +228,14 @@ describe('getFilePrefix', ()=>{
82
228
  });
83
229
  expect(result).toBe('');
84
230
  });
85
- it('should prioritize clientUploadContext prefix over database query', async ()=>{
86
- const req = makeReq([
87
- {
88
- prefix: 'db-prefix'
89
- }
90
- ]);
231
+ it('prioritizes a verified clientUploadContext over the database query', async ()=>{
232
+ const req = makeReq({
233
+ docs: [
234
+ {
235
+ prefix: 'db-prefix'
236
+ }
237
+ ]
238
+ });
91
239
  const result = await getFilePrefix({
92
240
  clientUploadContext: {
93
241
  prefix: 'context-prefix'
@@ -100,6 +248,44 @@ describe('getFilePrefix', ()=>{
100
248
  expect(req.payload.find).not.toHaveBeenCalled();
101
249
  });
102
250
  });
251
+ describe('client prefix as a disambiguation filter only', ()=>{
252
+ it('adds the prefix filter when the collection persists a prefix field', async ()=>{
253
+ const req = makeReq({
254
+ docs: [
255
+ {
256
+ prefix: 'documents'
257
+ }
258
+ ]
259
+ });
260
+ await getFilePrefix({
261
+ collection: makeCollection({
262
+ hasPrefixField: true
263
+ }),
264
+ filename: 'logo.png',
265
+ prefixQueryParam: 'documents',
266
+ req
267
+ });
268
+ expect(whereOf(req)).toContain('prefix');
269
+ });
270
+ it('omits the prefix filter when the collection has no prefix field (avoids querying a missing field)', async ()=>{
271
+ const req = makeReq({
272
+ docs: [
273
+ {
274
+ prefix: ''
275
+ }
276
+ ]
277
+ });
278
+ await getFilePrefix({
279
+ collection: makeCollection({
280
+ hasPrefixField: false
281
+ }),
282
+ filename: 'logo.png',
283
+ prefixQueryParam: 'anything',
284
+ req
285
+ });
286
+ expect(whereOf(req)).not.toContain('prefix');
287
+ });
288
+ });
103
289
  });
104
290
 
105
291
  //# sourceMappingURL=getFilePrefix.spec.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/getFilePrefix.spec.ts"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest'\n\nimport type { CollectionConfig, PayloadRequest } from 'payload'\n\nimport { getFilePrefix } from './getFilePrefix.js'\n\nconst makeReq = (docs: unknown[] = []) =>\n ({\n payload: {\n find: vi.fn().mockResolvedValue({ docs }),\n },\n }) as unknown as PayloadRequest\n\nconst makeCollection = (): CollectionConfig =>\n ({ slug: 'media', upload: {} }) as unknown as CollectionConfig\n\ndescribe('getFilePrefix', () => {\n describe('prefixQueryParam shortcut', () => {\n it('should return prefixQueryParam immediately without querying the database', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n prefixQueryParam: 'uuid-prefix',\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('uuid-prefix')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('should return an empty string when prefixQueryParam is an empty string', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n prefixQueryParam: '',\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('should reject multi-encoded values', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n prefixQueryParam: '%252e%252e%252fsecret%252f..%252fok',\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('should reject malformed percent-encoding', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n prefixQueryParam: '%E0%A4%A',\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n })\n\n describe('database fallback', () => {\n it('should query the database when prefixQueryParam is not provided', async () => {\n const req = makeReq([{ prefix: 'db-prefix' }])\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('db-prefix')\n expect(req.payload.find).toHaveBeenCalledOnce()\n })\n\n it('should return empty string when no document is found', async () => {\n const req = makeReq([])\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n })\n\n it('should prioritize clientUploadContext prefix over database query', async () => {\n const req = makeReq([{ prefix: 'db-prefix' }])\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'context-prefix' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('context-prefix')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n })\n})\n"],"names":["describe","expect","it","vi","getFilePrefix","makeReq","docs","payload","find","fn","mockResolvedValue","makeCollection","slug","upload","req","result","collection","prefixQueryParam","filename","toBe","not","toHaveBeenCalled","prefix","toHaveBeenCalledOnce","clientUploadContext"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAIjD,SAASC,aAAa,QAAQ,qBAAoB;AAElD,MAAMC,UAAU,CAACC,OAAkB,EAAE,GAClC,CAAA;QACCC,SAAS;YACPC,MAAML,GAAGM,EAAE,GAAGC,iBAAiB,CAAC;gBAAEJ;YAAK;QACzC;IACF,CAAA;AAEF,MAAMK,iBAAiB,IACpB,CAAA;QAAEC,MAAM;QAASC,QAAQ,CAAC;IAAE,CAAA;AAE/Bb,SAAS,iBAAiB;IACxBA,SAAS,6BAA6B;QACpCE,GAAG,4EAA4E;YAC7E,MAAMY,MAAMT;YAEZ,MAAMU,SAAS,MAAMX,cAAc;gBACjCY,YAAYL;gBACZM,kBAAkB;gBAClBC,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;YACpBlB,OAAOa,IAAIP,OAAO,CAACC,IAAI,EAAEY,GAAG,CAACC,gBAAgB;QAC/C;QAEAnB,GAAG,0EAA0E;YAC3E,MAAMY,MAAMT;YAEZ,MAAMU,SAAS,MAAMX,cAAc;gBACjCY,YAAYL;gBACZM,kBAAkB;gBAClBC,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;YACpBlB,OAAOa,IAAIP,OAAO,CAACC,IAAI,EAAEY,GAAG,CAACC,gBAAgB;QAC/C;QAEAnB,GAAG,sCAAsC;YACvC,MAAMY,MAAMT;YAEZ,MAAMU,SAAS,MAAMX,cAAc;gBACjCY,YAAYL;gBACZM,kBAAkB;gBAClBC,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;YACpBlB,OAAOa,IAAIP,OAAO,CAACC,IAAI,EAAEY,GAAG,CAACC,gBAAgB;QAC/C;QAEAnB,GAAG,4CAA4C;YAC7C,MAAMY,MAAMT;YAEZ,MAAMU,SAAS,MAAMX,cAAc;gBACjCY,YAAYL;gBACZM,kBAAkB;gBAClBC,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;YACpBlB,OAAOa,IAAIP,OAAO,CAACC,IAAI,EAAEY,GAAG,CAACC,gBAAgB;QAC/C;IACF;IAEArB,SAAS,qBAAqB;QAC5BE,GAAG,mEAAmE;YACpE,MAAMY,MAAMT,QAAQ;gBAAC;oBAAEiB,QAAQ;gBAAY;aAAE;YAE7C,MAAMP,SAAS,MAAMX,cAAc;gBACjCY,YAAYL;gBACZO,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;YACpBlB,OAAOa,IAAIP,OAAO,CAACC,IAAI,EAAEe,oBAAoB;QAC/C;QAEArB,GAAG,wDAAwD;YACzD,MAAMY,MAAMT,QAAQ,EAAE;YAEtB,MAAMU,SAAS,MAAMX,cAAc;gBACjCY,YAAYL;gBACZO,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;QACtB;QAEAjB,GAAG,oEAAoE;YACrE,MAAMY,MAAMT,QAAQ;gBAAC;oBAAEiB,QAAQ;gBAAY;aAAE;YAE7C,MAAMP,SAAS,MAAMX,cAAc;gBACjCoB,qBAAqB;oBAAEF,QAAQ;gBAAiB;gBAChDN,YAAYL;gBACZO,UAAU;gBACVJ;YACF;YAEAb,OAAOc,QAAQI,IAAI,CAAC;YACpBlB,OAAOa,IAAIP,OAAO,CAACC,IAAI,EAAEY,GAAG,CAACC,gBAAgB;QAC/C;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/utilities/getFilePrefix.spec.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\n\nimport { describe, expect, it, vi } from 'vitest'\n\nimport { getFilePrefix } from './getFilePrefix.js'\n\nconst makeReq = ({ docs = [] }: { docs?: unknown[] } = {}) =>\n ({\n payload: {\n find: vi.fn().mockResolvedValue({ docs }),\n },\n }) as unknown as PayloadRequest\n\nconst makeCollection = ({\n hasPrefixField = false,\n}: { hasPrefixField?: boolean } = {}): CollectionConfig =>\n ({\n slug: 'media',\n fields: hasPrefixField ? [{ name: 'prefix', type: 'text' }] : [],\n upload: {},\n }) as unknown as CollectionConfig\n\nconst whereOf = (req: PayloadRequest) =>\n JSON.stringify(\n (req.payload.find as unknown as { mock: { calls: [{ where: unknown }][] } }).mock.calls[0][0]\n .where,\n )\n\ndescribe('getFilePrefix', () => {\n it('prefers an already-authorized document over a client upload context or query prefix', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n const doc = { id: 1, prefix: 'invoices' }\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'reference-prefix' },\n collection: makeCollection(),\n collectionPrefix: 'media',\n doc,\n filename: 'logo.png',\n prefixQueryParam: 'query-prefix',\n req,\n })\n\n expect(result).toBe('invoices')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('does not fall back to a lookup when the checked document has no prefix', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n collectionPrefix: 'media',\n doc: { id: 1 },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('contains a client upload context prefix beneath the collection prefix', async () => {\n const req = makeReq({ docs: [{ prefix: 'invoices' }] })\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'invoices' },\n collection: makeCollection(),\n collectionPrefix: 'media',\n filename: 'file.png',\n req,\n })\n\n expect(result).toBe('media/invoices')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n describe('authorized document (trusted)', () => {\n it('folds the object key into the document prefix and skips the database', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n doc: { id: 1, _objectKey: 'abc123', prefix: 'doc-prefix' },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('doc-prefix/abc123')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('returns the document prefix alone when it has no object key', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n doc: { id: 1, prefix: 'doc-prefix' },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('doc-prefix')\n })\n\n it('sanitizes the document prefix', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n doc: { id: 1, prefix: '../secret' },\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('secret')\n })\n })\n\n describe('verified clientUploadContext (trusted)', () => {\n it('folds the server-owned object key into the prefix and skips the database', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n clientUploadContext: { _objectKey: 'abc123', prefix: 'documents' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('documents/abc123')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n\n it('returns the prefix alone when the context has no object key', async () => {\n const req = makeReq()\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'documents' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('documents')\n })\n })\n\n describe('access-controlled database resolution', () => {\n it('does NOT short-circuit on a client-supplied prefix — the access-controlled query still runs', async () => {\n const req = makeReq({ docs: [{ _objectKey: 'abc123', prefix: 'documents' }] })\n\n await getFilePrefix({\n collection: makeCollection({ hasPrefixField: true }),\n filename: 'logo.png',\n prefixQueryParam: 'documents',\n req,\n })\n\n // A client-supplied prefix must not bypass the access-controlled lookup.\n expect(req.payload.find).toHaveBeenCalledOnce()\n })\n\n it('queries with access control enabled', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(req.payload.find).toHaveBeenCalledWith(\n expect.objectContaining({ overrideAccess: false, req }),\n )\n })\n\n it('folds the stored object key into the resolved prefix', async () => {\n const req = makeReq({ docs: [{ _objectKey: 'abc123', prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('db-prefix/abc123')\n })\n\n it('returns an empty string when no document is found', async () => {\n const req = makeReq({ docs: [] })\n\n const result = await getFilePrefix({\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('')\n })\n\n it('prioritizes a verified clientUploadContext over the database query', async () => {\n const req = makeReq({ docs: [{ prefix: 'db-prefix' }] })\n\n const result = await getFilePrefix({\n clientUploadContext: { prefix: 'context-prefix' },\n collection: makeCollection(),\n filename: 'logo.png',\n req,\n })\n\n expect(result).toBe('context-prefix')\n expect(req.payload.find).not.toHaveBeenCalled()\n })\n })\n\n describe('client prefix as a disambiguation filter only', () => {\n it('adds the prefix filter when the collection persists a prefix field', async () => {\n const req = makeReq({ docs: [{ prefix: 'documents' }] })\n\n await getFilePrefix({\n collection: makeCollection({ hasPrefixField: true }),\n filename: 'logo.png',\n prefixQueryParam: 'documents',\n req,\n })\n\n expect(whereOf(req)).toContain('prefix')\n })\n\n it('omits the prefix filter when the collection has no prefix field (avoids querying a missing field)', async () => {\n const req = makeReq({ docs: [{ prefix: '' }] })\n\n await getFilePrefix({\n collection: makeCollection({ hasPrefixField: false }),\n filename: 'logo.png',\n prefixQueryParam: 'anything',\n req,\n })\n\n expect(whereOf(req)).not.toContain('prefix')\n })\n })\n})\n"],"names":["describe","expect","it","vi","getFilePrefix","makeReq","docs","payload","find","fn","mockResolvedValue","makeCollection","hasPrefixField","slug","fields","name","type","upload","whereOf","req","JSON","stringify","mock","calls","where","prefix","doc","id","result","clientUploadContext","collection","collectionPrefix","filename","prefixQueryParam","toBe","not","toHaveBeenCalled","_objectKey","toHaveBeenCalledOnce","toHaveBeenCalledWith","objectContaining","overrideAccess","toContain"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { PayloadRequest } from 'payload';
2
+ import type { ClientUploadContext } from '../types.js';
2
3
  type Args = {
3
4
  collectionPrefix?: string;
4
5
  collectionSlug: string;
@@ -12,13 +13,23 @@ type Args = {
12
13
  * the filename via {@link getSafeFileName} so a duplicate upload does not
13
14
  * overwrite an existing blob.
14
15
  *
16
+ * A per-upload segment is persisted in `_objectKey` (not `prefix`), so the stored `prefix`
17
+ * and `filename` stay clean.
18
+ *
15
19
  * The resolved `sanitizedFilename` is returned so the browser-side handler
16
20
  * can update the form via `updateFilename`.
21
+ *
22
+ * The semantic prefix is resolved with {@link buildUploadPrefix} and the final key with
23
+ * {@link buildUploadStoragePathData}, so the receipt only ever signs a key beneath the configured
24
+ * collection prefix.
17
25
  */
18
26
  export declare function resolveSignedURLKey({ collectionPrefix, collectionSlug, docPrefix, filename, req, useCompositePrefixes, }: Args): Promise<{
27
+ _objectKey: `${string}-${string}-${string}-${string}-${string}`;
28
+ clientUploadContext: ClientUploadContext;
19
29
  fileKey: string;
20
30
  sanitizedDocPrefix: string;
21
31
  sanitizedFilename: string;
32
+ storageFilePath: string;
22
33
  }>;
23
34
  export {};
24
35
  //# sourceMappingURL=resolveSignedURLKey.d.ts.map