@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.
- package/README.md +2 -0
- package/dist/exports/client.d.ts +1 -1
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +1 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/utilities.d.ts +3 -2
- package/dist/exports/utilities.d.ts.map +1 -1
- package/dist/exports/utilities.js +3 -2
- package/dist/exports/utilities.js.map +1 -1
- package/dist/fields/getFields.d.ts.map +1 -1
- package/dist/fields/getFields.js +28 -1
- package/dist/fields/getFields.js.map +1 -1
- package/dist/hooks/afterChange.d.ts +3 -1
- package/dist/hooks/afterChange.d.ts.map +1 -1
- package/dist/hooks/afterChange.js +56 -9
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/afterChange.spec.js +150 -0
- package/dist/hooks/afterChange.spec.js.map +1 -0
- package/dist/hooks/afterDelete.d.ts +3 -1
- package/dist/hooks/afterDelete.d.ts.map +1 -1
- package/dist/hooks/afterDelete.js +16 -2
- package/dist/hooks/afterDelete.js.map +1 -1
- package/dist/hooks/afterRead.d.ts.map +1 -1
- package/dist/hooks/afterRead.js +13 -2
- package/dist/hooks/afterRead.js.map +1 -1
- package/dist/hooks/beforeChange.d.ts.map +1 -1
- package/dist/hooks/beforeChange.js +10 -1
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/hooks/normalizeUploadPrefix.d.ts +11 -0
- package/dist/hooks/normalizeUploadPrefix.d.ts.map +1 -0
- package/dist/hooks/normalizeUploadPrefix.js +42 -0
- package/dist/hooks/normalizeUploadPrefix.js.map +1 -0
- package/dist/hooks/normalizeUploadPrefix.spec.js +154 -0
- package/dist/hooks/normalizeUploadPrefix.spec.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +15 -6
- package/dist/plugin.js.map +1 -1
- package/dist/plugin.spec.js +166 -0
- package/dist/plugin.spec.js.map +1 -0
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts +15 -0
- package/dist/utilities/assertReceiptMatchesFileKey.d.ts.map +1 -0
- package/dist/utilities/assertReceiptMatchesFileKey.js +31 -0
- package/dist/utilities/assertReceiptMatchesFileKey.js.map +1 -0
- package/dist/utilities/buildPrefixWithObjectKey.d.ts +8 -0
- package/dist/utilities/buildPrefixWithObjectKey.d.ts.map +1 -0
- package/dist/utilities/buildPrefixWithObjectKey.js +13 -0
- package/dist/utilities/buildPrefixWithObjectKey.js.map +1 -0
- package/dist/utilities/buildStoragePathData.d.ts +55 -0
- package/dist/utilities/buildStoragePathData.d.ts.map +1 -0
- package/dist/utilities/buildStoragePathData.js +69 -0
- package/dist/utilities/buildStoragePathData.js.map +1 -0
- package/dist/utilities/buildStoragePathData.spec.js +254 -0
- package/dist/utilities/buildStoragePathData.spec.js.map +1 -0
- package/dist/utilities/getFilePrefix.d.ts +27 -11
- package/dist/utilities/getFilePrefix.d.ts.map +1 -1
- package/dist/utilities/getFilePrefix.js +75 -31
- package/dist/utilities/getFilePrefix.js.map +1 -1
- package/dist/utilities/getFilePrefix.spec.js +221 -35
- package/dist/utilities/getFilePrefix.spec.js.map +1 -1
- package/dist/utilities/resolveSignedURLKey.d.ts +11 -0
- package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
- package/dist/utilities/resolveSignedURLKey.js +43 -8
- package/dist/utilities/resolveSignedURLKey.js.map +1 -1
- package/dist/utilities/resolveSignedURLKey.spec.js +56 -0
- package/dist/utilities/resolveSignedURLKey.spec.js.map +1 -0
- package/dist/utilities/sanitizePrefix.d.ts +2 -2
- package/dist/utilities/sanitizePrefix.js +4 -4
- package/dist/utilities/sanitizePrefix.js.map +1 -1
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts +20 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.d.ts.map +1 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js +24 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.js.map +1 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js +108 -0
- package/dist/utilities/verifyClientUploadReceiptForFileKey.spec.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CollectionConfig, Field, FileData, ImageSize, PayloadRequest, TypeWithID, UploadCollectionSlug } from 'payload';
|
|
2
|
+
import type { SignedClientUploadReceipt } from 'payload/internal';
|
|
2
3
|
export interface File {
|
|
3
4
|
buffer: Buffer;
|
|
4
5
|
clientUploadContext?: unknown;
|
|
@@ -14,12 +15,27 @@ export type ClientUploadsAccess = (args: {
|
|
|
14
15
|
export type ClientUploadsConfig = {
|
|
15
16
|
access?: ClientUploadsAccess;
|
|
16
17
|
} | boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Context returned by a client-upload handler and submitted with the document
|
|
20
|
+
* create request. `prefix` plus the server-owned `_objectKey` segment locate the object.
|
|
21
|
+
*/
|
|
22
|
+
export type ClientUploadContext = {
|
|
23
|
+
_objectKey?: string;
|
|
24
|
+
prefix: string;
|
|
25
|
+
signedReceipt: SignedClientUploadReceipt;
|
|
26
|
+
};
|
|
17
27
|
export type HandleUpload = (args: {
|
|
18
28
|
clientUploadContext: unknown;
|
|
19
29
|
collection: CollectionConfig;
|
|
20
30
|
data: any;
|
|
21
31
|
file: File;
|
|
22
32
|
req: PayloadRequest;
|
|
33
|
+
/**
|
|
34
|
+
* Pre-resolved storage file path (`_objectKey` folded in, contained beneath the collection prefix).
|
|
35
|
+
* Path-based adapters upload to it; provider-id adapters (e.g. uploadthing) ignore it and assign
|
|
36
|
+
* their own key.
|
|
37
|
+
*/
|
|
38
|
+
storageFilePath: string;
|
|
23
39
|
}) => Partial<FileData & TypeWithID> | Promise<Partial<FileData & TypeWithID>> | Promise<void> | void;
|
|
24
40
|
export interface TypeWithPrefix {
|
|
25
41
|
prefix?: string;
|
|
@@ -29,6 +45,11 @@ export type HandleDelete = (args: {
|
|
|
29
45
|
doc: FileData & TypeWithID & TypeWithPrefix;
|
|
30
46
|
filename: string;
|
|
31
47
|
req: PayloadRequest;
|
|
48
|
+
/**
|
|
49
|
+
* Pre-resolved storage file path of the object to delete. Path-based adapters delete it; provider-id
|
|
50
|
+
* adapters (e.g. uploadthing) ignore it and locate the object via `doc`.
|
|
51
|
+
*/
|
|
52
|
+
storageFilePath: string;
|
|
32
53
|
}) => Promise<void> | void;
|
|
33
54
|
export type GenerateURL = (args: {
|
|
34
55
|
collection: CollectionConfig;
|
|
@@ -59,7 +80,8 @@ export interface GeneratedAdapter {
|
|
|
59
80
|
handleDelete: HandleDelete;
|
|
60
81
|
handleUpload: HandleUpload;
|
|
61
82
|
name: string;
|
|
62
|
-
onInit?: () => void;
|
|
83
|
+
onInit?: () => Promise<void> | void;
|
|
84
|
+
requiresClientUploadReceipt?: boolean;
|
|
63
85
|
staticHandler: StaticHandler;
|
|
64
86
|
}
|
|
65
87
|
export type Adapter = (args: {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,KAAK,EACL,QAAQ,EACR,SAAS,EACT,cAAc,EACd,UAAU,EACV,oBAAoB,EACrB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type {\n CollectionConfig,\n Field,\n FileData,\n ImageSize,\n PayloadRequest,\n TypeWithID,\n UploadCollectionSlug,\n} from 'payload'\n\nexport interface File {\n buffer: Buffer\n clientUploadContext?: unknown\n filename: string\n filesize: number\n mimeType: string\n tempFilePath?: string\n}\n\nexport type ClientUploadsAccess = (args: {\n collectionSlug: UploadCollectionSlug\n req: PayloadRequest\n}) => boolean | Promise<boolean>\n\nexport type ClientUploadsConfig =\n | {\n access?: ClientUploadsAccess\n }\n | boolean\n\nexport type HandleUpload = (args: {\n clientUploadContext: unknown\n collection: CollectionConfig\n data: any\n file: File\n req: PayloadRequest\n}) =>\n | Partial<FileData & TypeWithID>\n | Promise<Partial<FileData & TypeWithID>>\n | Promise<void>\n | void\n\nexport interface TypeWithPrefix {\n prefix?: string\n}\n\nexport type HandleDelete = (args: {\n collection: CollectionConfig\n doc: FileData & TypeWithID & TypeWithPrefix\n filename: string\n req: PayloadRequest\n}) => Promise<void> | void\n\nexport type GenerateURL = (args: {\n collection: CollectionConfig\n data: any\n filename: string\n prefix?: string\n}) => Promise<string> | string\n\nexport type StaticHandler = (\n req: PayloadRequest,\n args: {\n doc?: TypeWithID\n headers?: Headers\n params: { clientUploadContext?: unknown; collection: string; filename: string; prefix?: string }\n },\n) => Promise<Response> | Response\n\nexport interface GeneratedAdapter {\n clientUploads?: ClientUploadsConfig\n /**\n * Additional fields to be injected into the base collection and image sizes\n */\n fields?: Field[]\n /**\n * Generates the public URL for a file\n */\n generateURL?: GenerateURL\n handleDelete: HandleDelete\n handleUpload: HandleUpload\n name: string\n onInit?: () => void\n staticHandler: StaticHandler\n}\n\nexport type Adapter = (args: { collection: CollectionConfig; prefix?: string }) => GeneratedAdapter\n\nexport type AllowList = Array<{\n hostname: string\n pathname?: string\n port?: string\n protocol?: 'http' | 'https'\n search?: string\n}>\n\nexport type GenerateFileURL = (args: {\n collection: CollectionConfig\n filename: string\n prefix?: string\n size?: ImageSize\n}) => Promise<string> | string\n\nexport interface CollectionOptions {\n adapter: Adapter | null\n disableLocalStorage?: boolean\n disablePayloadAccessControl?: true\n generateFileURL?: GenerateFileURL\n prefix?: string\n}\n\nexport interface PluginOptions {\n /**\n * When enabled, fields (like the prefix field) will always be inserted into\n * the collection schema regardless of whether the plugin is enabled. This\n * ensures a consistent schema across all environments.\n *\n * This will be enabled by default in Payload v4.\n *\n * @default false\n */\n alwaysInsertFields?: boolean\n collections: Partial<Record<UploadCollectionSlug, CollectionOptions>>\n /**\n * Whether or not to enable the plugin\n *\n * Default: true\n */\n enabled?: boolean\n /**\n * When true (compositional prefixes), the stored `prefix` field is only the\n * document-level segment; the collection prefix comes from plugin options and\n * must not be pre-filled as the field default.\n *\n * Set by storage adapters that support compositional prefixes (e.g. S3, Azure, R2, Vercel Blob, GCS).\n *\n * @default false\n */\n useCompositePrefixes?: boolean\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type {\n CollectionConfig,\n Field,\n FileData,\n ImageSize,\n PayloadRequest,\n TypeWithID,\n UploadCollectionSlug,\n} from 'payload'\nimport type { SignedClientUploadReceipt } from 'payload/internal'\n\nexport interface File {\n buffer: Buffer\n clientUploadContext?: unknown\n filename: string\n filesize: number\n mimeType: string\n tempFilePath?: string\n}\n\nexport type ClientUploadsAccess = (args: {\n collectionSlug: UploadCollectionSlug\n req: PayloadRequest\n}) => boolean | Promise<boolean>\n\nexport type ClientUploadsConfig =\n | {\n access?: ClientUploadsAccess\n }\n | boolean\n\n/**\n * Context returned by a client-upload handler and submitted with the document\n * create request. `prefix` plus the server-owned `_objectKey` segment locate the object.\n */\nexport type ClientUploadContext = {\n _objectKey?: string\n prefix: string\n signedReceipt: SignedClientUploadReceipt\n}\n\nexport type HandleUpload = (args: {\n clientUploadContext: unknown\n collection: CollectionConfig\n data: any\n file: File\n req: PayloadRequest\n /**\n * Pre-resolved storage file path (`_objectKey` folded in, contained beneath the collection prefix).\n * Path-based adapters upload to it; provider-id adapters (e.g. uploadthing) ignore it and assign\n * their own key.\n */\n storageFilePath: string\n}) =>\n | Partial<FileData & TypeWithID>\n | Promise<Partial<FileData & TypeWithID>>\n | Promise<void>\n | void\n\nexport interface TypeWithPrefix {\n prefix?: string\n}\n\nexport type HandleDelete = (args: {\n collection: CollectionConfig\n doc: FileData & TypeWithID & TypeWithPrefix\n filename: string\n req: PayloadRequest\n /**\n * Pre-resolved storage file path of the object to delete. Path-based adapters delete it; provider-id\n * adapters (e.g. uploadthing) ignore it and locate the object via `doc`.\n */\n storageFilePath: string\n}) => Promise<void> | void\n\nexport type GenerateURL = (args: {\n collection: CollectionConfig\n data: any\n filename: string\n prefix?: string\n}) => Promise<string> | string\n\nexport type StaticHandler = (\n req: PayloadRequest,\n args: {\n doc?: TypeWithID\n headers?: Headers\n params: { clientUploadContext?: unknown; collection: string; filename: string; prefix?: string }\n },\n) => Promise<Response> | Response\n\nexport interface GeneratedAdapter {\n clientUploads?: ClientUploadsConfig\n /**\n * Additional fields to be injected into the base collection and image sizes\n */\n fields?: Field[]\n /**\n * Generates the public URL for a file\n */\n generateURL?: GenerateURL\n handleDelete: HandleDelete\n handleUpload: HandleUpload\n name: string\n onInit?: () => Promise<void> | void\n requiresClientUploadReceipt?: boolean\n staticHandler: StaticHandler\n}\n\nexport type Adapter = (args: { collection: CollectionConfig; prefix?: string }) => GeneratedAdapter\n\nexport type AllowList = Array<{\n hostname: string\n pathname?: string\n port?: string\n protocol?: 'http' | 'https'\n search?: string\n}>\n\nexport type GenerateFileURL = (args: {\n collection: CollectionConfig\n filename: string\n prefix?: string\n size?: ImageSize\n}) => Promise<string> | string\n\nexport interface CollectionOptions {\n adapter: Adapter | null\n disableLocalStorage?: boolean\n disablePayloadAccessControl?: true\n generateFileURL?: GenerateFileURL\n prefix?: string\n}\n\nexport interface PluginOptions {\n /**\n * When enabled, fields (like the prefix field) will always be inserted into\n * the collection schema regardless of whether the plugin is enabled. This\n * ensures a consistent schema across all environments.\n *\n * This will be enabled by default in Payload v4.\n *\n * @default false\n */\n alwaysInsertFields?: boolean\n collections: Partial<Record<UploadCollectionSlug, CollectionOptions>>\n /**\n * Whether or not to enable the plugin\n *\n * Default: true\n */\n enabled?: boolean\n /**\n * When true (compositional prefixes), the stored `prefix` field is only the\n * document-level segment; the collection prefix comes from plugin options and\n * must not be pre-filled as the field default.\n *\n * Set by storage adapters that support compositional prefixes (e.g. S3, Azure, R2, Vercel Blob, GCS).\n *\n * @default false\n */\n useCompositePrefixes?: boolean\n}\n"],"names":[],"mappings":"AAsIA,WA4BC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins the semantic prefix and the `_objectKey` segment into the object's folder; either may be empty.
|
|
3
|
+
*/
|
|
4
|
+
export declare const buildPrefixWithObjectKey: ({ objectKey, prefix, }: {
|
|
5
|
+
objectKey?: string;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
}) => string;
|
|
8
|
+
//# sourceMappingURL=buildPrefixWithObjectKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildPrefixWithObjectKey.d.ts","sourceRoot":"","sources":["../../src/utilities/buildPrefixWithObjectKey.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,wBAAwB,2BAGlC;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,KAAG,MASH,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { sanitizePrefix } from './sanitizePrefix.js';
|
|
2
|
+
/**
|
|
3
|
+
* Joins the semantic prefix and the `_objectKey` segment into the object's folder; either may be empty.
|
|
4
|
+
*/ export const buildPrefixWithObjectKey = ({ objectKey, prefix })=>{
|
|
5
|
+
const safePrefix = sanitizePrefix(prefix ?? '');
|
|
6
|
+
const safeObjectKey = sanitizePrefix(objectKey ?? '');
|
|
7
|
+
if (safePrefix && safeObjectKey) {
|
|
8
|
+
return `${safePrefix}/${safeObjectKey}`;
|
|
9
|
+
}
|
|
10
|
+
return safePrefix || safeObjectKey;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=buildPrefixWithObjectKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/buildPrefixWithObjectKey.ts"],"sourcesContent":["import { sanitizePrefix } from './sanitizePrefix.js'\n\n/**\n * Joins the semantic prefix and the `_objectKey` segment into the object's folder; either may be empty.\n */\nexport const buildPrefixWithObjectKey = ({\n objectKey,\n prefix,\n}: {\n objectKey?: string\n prefix?: string\n}): string => {\n const safePrefix = sanitizePrefix(prefix ?? '')\n const safeObjectKey = sanitizePrefix(objectKey ?? '')\n\n if (safePrefix && safeObjectKey) {\n return `${safePrefix}/${safeObjectKey}`\n }\n\n return safePrefix || safeObjectKey\n}\n"],"names":["sanitizePrefix","buildPrefixWithObjectKey","objectKey","prefix","safePrefix","safeObjectKey"],"mappings":"AAAA,SAASA,cAAc,QAAQ,sBAAqB;AAEpD;;CAEC,GACD,OAAO,MAAMC,2BAA2B,CAAC,EACvCC,SAAS,EACTC,MAAM,EAIP;IACC,MAAMC,aAAaJ,eAAeG,UAAU;IAC5C,MAAME,gBAAgBL,eAAeE,aAAa;IAElD,IAAIE,cAAcC,eAAe;QAC/B,OAAO,GAAGD,WAAW,CAAC,EAAEC,eAAe;IACzC;IAEA,OAAOD,cAAcC;AACvB,EAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type BuildFilePathDataArgs = {
|
|
2
|
+
collectionPrefix?: string;
|
|
3
|
+
docPrefix?: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
useCompositePrefixes?: boolean;
|
|
6
|
+
};
|
|
7
|
+
type BuildFilePathDataResult = {
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use `storageFilePath` instead.
|
|
10
|
+
*
|
|
11
|
+
* The full path to the file within the storage backend.
|
|
12
|
+
*/
|
|
13
|
+
fileKey: string;
|
|
14
|
+
sanitizedCollectionPrefix: string;
|
|
15
|
+
sanitizedDocPrefix: string;
|
|
16
|
+
sanitizedFilename: string;
|
|
17
|
+
/**
|
|
18
|
+
* The full path to the file within the storage backend.
|
|
19
|
+
*/
|
|
20
|
+
storageFilePath: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function isStoragePathWithinCollectionPrefix({ collectionPrefix, docPrefix, }: {
|
|
23
|
+
collectionPrefix?: string;
|
|
24
|
+
docPrefix: string;
|
|
25
|
+
}): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves where a new upload must be stored, without needing a filename.
|
|
28
|
+
*
|
|
29
|
+
* In non-composite mode (useCompositePrefixes: false), docPrefix is treated as a complete prefix
|
|
30
|
+
* when it is already within collectionPrefix and is otherwise nested beneath collectionPrefix.
|
|
31
|
+
* In composite mode (useCompositePrefixes: true), the collection prefix is joined when the key is
|
|
32
|
+
* built, so the persisted document prefix stays relative to it.
|
|
33
|
+
*
|
|
34
|
+
* Prefix-only callers (such as the upload prefix hooks) use this directly; use
|
|
35
|
+
* {@link buildUploadStoragePathData} when a full object key is needed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildUploadPrefix({ collectionPrefix, docPrefix, useCompositePrefixes, }: Omit<BuildFilePathDataArgs, 'filename'>): {
|
|
38
|
+
sanitizedCollectionPrefix: string;
|
|
39
|
+
sanitizedDocPrefix: string;
|
|
40
|
+
uploadPrefix: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a new upload destination beneath the configured collection prefix.
|
|
44
|
+
* Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildUploadStoragePathData({ collectionPrefix, docPrefix, filename, useCompositePrefixes, }: BuildFilePathDataArgs): BuildFilePathDataResult;
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the exact location represented by persisted upload metadata.
|
|
49
|
+
* Non-composite document prefixes historically override the collection prefix;
|
|
50
|
+
* keep that behavior for reads, URLs, and cleanup of pre-upgrade files.
|
|
51
|
+
* New writes must use buildUploadStoragePathData instead.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildStoragePathData({ collectionPrefix, docPrefix, filename, useCompositePrefixes, }: BuildFilePathDataArgs): BuildFilePathDataResult;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=buildStoragePathData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildStoragePathData.d.ts","sourceRoot":"","sources":["../../src/utilities/buildStoragePathData.ts"],"names":[],"mappings":"AAKA,KAAK,qBAAqB,GAAG;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC7B;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,yBAAyB,EAAE,MAAM,CAAA;IACjC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,wBAAgB,mCAAmC,CAAC,EAClD,gBAAgB,EAChB,SAAS,GACV,EAAE;IACD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;CAClB,GAAG,OAAO,CAQV;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,gBAAgB,EAChB,SAAS,EACT,oBAA4B,GAC7B,EAAE,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,GAAG;IAC3C,yBAAyB,EAAE,MAAM,CAAA;IACjC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;CACrB,CAiBA;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,oBAA4B,GAC7B,EAAE,qBAAqB,GAAG,uBAAuB,CAmBjD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,oBAA4B,GAC7B,EAAE,qBAAqB,GAAG,uBAAuB,CAejD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { sanitizeFilename } from 'payload/shared';
|
|
3
|
+
import { sanitizePrefix } from './sanitizePrefix.js';
|
|
4
|
+
export function isStoragePathWithinCollectionPrefix({ collectionPrefix, docPrefix }) {
|
|
5
|
+
const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '');
|
|
6
|
+
return !safeCollectionPrefix || docPrefix === safeCollectionPrefix || docPrefix.startsWith(`${safeCollectionPrefix}/`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolves where a new upload must be stored, without needing a filename.
|
|
10
|
+
*
|
|
11
|
+
* In non-composite mode (useCompositePrefixes: false), docPrefix is treated as a complete prefix
|
|
12
|
+
* when it is already within collectionPrefix and is otherwise nested beneath collectionPrefix.
|
|
13
|
+
* In composite mode (useCompositePrefixes: true), the collection prefix is joined when the key is
|
|
14
|
+
* built, so the persisted document prefix stays relative to it.
|
|
15
|
+
*
|
|
16
|
+
* Prefix-only callers (such as the upload prefix hooks) use this directly; use
|
|
17
|
+
* {@link buildUploadStoragePathData} when a full object key is needed.
|
|
18
|
+
*/ export function buildUploadPrefix({ collectionPrefix, docPrefix, useCompositePrefixes = false }) {
|
|
19
|
+
const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '');
|
|
20
|
+
const safeDocPrefix = sanitizePrefix(docPrefix || '');
|
|
21
|
+
const uploadPrefix = safeDocPrefix && !isStoragePathWithinCollectionPrefix({
|
|
22
|
+
collectionPrefix: safeCollectionPrefix,
|
|
23
|
+
docPrefix: safeDocPrefix
|
|
24
|
+
}) ? path.posix.join(safeCollectionPrefix, safeDocPrefix) : safeDocPrefix || safeCollectionPrefix;
|
|
25
|
+
return {
|
|
26
|
+
sanitizedCollectionPrefix: safeCollectionPrefix,
|
|
27
|
+
sanitizedDocPrefix: !useCompositePrefixes && safeDocPrefix ? uploadPrefix : safeDocPrefix,
|
|
28
|
+
uploadPrefix
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolves a new upload destination beneath the configured collection prefix.
|
|
33
|
+
* Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.
|
|
34
|
+
*/ export function buildUploadStoragePathData({ collectionPrefix, docPrefix, filename, useCompositePrefixes = false }) {
|
|
35
|
+
const { sanitizedCollectionPrefix, sanitizedDocPrefix, uploadPrefix } = buildUploadPrefix({
|
|
36
|
+
collectionPrefix,
|
|
37
|
+
docPrefix,
|
|
38
|
+
useCompositePrefixes
|
|
39
|
+
});
|
|
40
|
+
const safeFilename = sanitizeFilename(filename);
|
|
41
|
+
const storageFilePath = useCompositePrefixes ? path.posix.join(sanitizedCollectionPrefix, sanitizedDocPrefix, safeFilename) : path.posix.join(uploadPrefix, safeFilename);
|
|
42
|
+
return {
|
|
43
|
+
fileKey: storageFilePath,
|
|
44
|
+
sanitizedCollectionPrefix,
|
|
45
|
+
sanitizedDocPrefix,
|
|
46
|
+
sanitizedFilename: safeFilename,
|
|
47
|
+
storageFilePath
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the exact location represented by persisted upload metadata.
|
|
52
|
+
* Non-composite document prefixes historically override the collection prefix;
|
|
53
|
+
* keep that behavior for reads, URLs, and cleanup of pre-upgrade files.
|
|
54
|
+
* New writes must use buildUploadStoragePathData instead.
|
|
55
|
+
*/ export function buildStoragePathData({ collectionPrefix, docPrefix, filename, useCompositePrefixes = false }) {
|
|
56
|
+
const sanitizedCollectionPrefix = sanitizePrefix(collectionPrefix || '');
|
|
57
|
+
const sanitizedDocPrefix = sanitizePrefix(docPrefix || '');
|
|
58
|
+
const sanitizedFilename = sanitizeFilename(filename);
|
|
59
|
+
const storageFilePath = useCompositePrefixes ? path.posix.join(sanitizedCollectionPrefix, sanitizedDocPrefix, sanitizedFilename) : path.posix.join(sanitizedDocPrefix || sanitizedCollectionPrefix, sanitizedFilename);
|
|
60
|
+
return {
|
|
61
|
+
fileKey: storageFilePath,
|
|
62
|
+
sanitizedCollectionPrefix,
|
|
63
|
+
sanitizedDocPrefix,
|
|
64
|
+
sanitizedFilename,
|
|
65
|
+
storageFilePath
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=buildStoragePathData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/buildStoragePathData.ts"],"sourcesContent":["import path from 'path'\nimport { sanitizeFilename } from 'payload/shared'\n\nimport { sanitizePrefix } from './sanitizePrefix.js'\n\ntype BuildFilePathDataArgs = {\n collectionPrefix?: string\n docPrefix?: string\n filename: string\n useCompositePrefixes?: boolean\n}\n\ntype BuildFilePathDataResult = {\n /**\n * @deprecated Use `storageFilePath` instead.\n *\n * The full path to the file within the storage backend.\n */\n fileKey: string\n sanitizedCollectionPrefix: string\n sanitizedDocPrefix: string\n sanitizedFilename: string\n /**\n * The full path to the file within the storage backend.\n */\n storageFilePath: string\n}\n\nexport function isStoragePathWithinCollectionPrefix({\n collectionPrefix,\n docPrefix,\n}: {\n collectionPrefix?: string\n docPrefix: string\n}): boolean {\n const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '')\n\n return (\n !safeCollectionPrefix ||\n docPrefix === safeCollectionPrefix ||\n docPrefix.startsWith(`${safeCollectionPrefix}/`)\n )\n}\n\n/**\n * Resolves where a new upload must be stored, without needing a filename.\n *\n * In non-composite mode (useCompositePrefixes: false), docPrefix is treated as a complete prefix\n * when it is already within collectionPrefix and is otherwise nested beneath collectionPrefix.\n * In composite mode (useCompositePrefixes: true), the collection prefix is joined when the key is\n * built, so the persisted document prefix stays relative to it.\n *\n * Prefix-only callers (such as the upload prefix hooks) use this directly; use\n * {@link buildUploadStoragePathData} when a full object key is needed.\n */\nexport function buildUploadPrefix({\n collectionPrefix,\n docPrefix,\n useCompositePrefixes = false,\n}: Omit<BuildFilePathDataArgs, 'filename'>): {\n sanitizedCollectionPrefix: string\n sanitizedDocPrefix: string\n uploadPrefix: string\n} {\n const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '')\n const safeDocPrefix = sanitizePrefix(docPrefix || '')\n const uploadPrefix =\n safeDocPrefix &&\n !isStoragePathWithinCollectionPrefix({\n collectionPrefix: safeCollectionPrefix,\n docPrefix: safeDocPrefix,\n })\n ? path.posix.join(safeCollectionPrefix, safeDocPrefix)\n : safeDocPrefix || safeCollectionPrefix\n\n return {\n sanitizedCollectionPrefix: safeCollectionPrefix,\n sanitizedDocPrefix: !useCompositePrefixes && safeDocPrefix ? uploadPrefix : safeDocPrefix,\n uploadPrefix,\n }\n}\n\n/**\n * Resolves a new upload destination beneath the configured collection prefix.\n * Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.\n */\nexport function buildUploadStoragePathData({\n collectionPrefix,\n docPrefix,\n filename,\n useCompositePrefixes = false,\n}: BuildFilePathDataArgs): BuildFilePathDataResult {\n const { sanitizedCollectionPrefix, sanitizedDocPrefix, uploadPrefix } = buildUploadPrefix({\n collectionPrefix,\n docPrefix,\n useCompositePrefixes,\n })\n const safeFilename = sanitizeFilename(filename)\n\n const storageFilePath = useCompositePrefixes\n ? path.posix.join(sanitizedCollectionPrefix, sanitizedDocPrefix, safeFilename)\n : path.posix.join(uploadPrefix, safeFilename)\n\n return {\n fileKey: storageFilePath,\n sanitizedCollectionPrefix,\n sanitizedDocPrefix,\n sanitizedFilename: safeFilename,\n storageFilePath,\n }\n}\n\n/**\n * Resolves the exact location represented by persisted upload metadata.\n * Non-composite document prefixes historically override the collection prefix;\n * keep that behavior for reads, URLs, and cleanup of pre-upgrade files.\n * New writes must use buildUploadStoragePathData instead.\n */\nexport function buildStoragePathData({\n collectionPrefix,\n docPrefix,\n filename,\n useCompositePrefixes = false,\n}: BuildFilePathDataArgs): BuildFilePathDataResult {\n const sanitizedCollectionPrefix = sanitizePrefix(collectionPrefix || '')\n const sanitizedDocPrefix = sanitizePrefix(docPrefix || '')\n const sanitizedFilename = sanitizeFilename(filename)\n const storageFilePath = useCompositePrefixes\n ? path.posix.join(sanitizedCollectionPrefix, sanitizedDocPrefix, sanitizedFilename)\n : path.posix.join(sanitizedDocPrefix || sanitizedCollectionPrefix, sanitizedFilename)\n\n return {\n fileKey: storageFilePath,\n sanitizedCollectionPrefix,\n sanitizedDocPrefix,\n sanitizedFilename,\n storageFilePath,\n }\n}\n"],"names":["path","sanitizeFilename","sanitizePrefix","isStoragePathWithinCollectionPrefix","collectionPrefix","docPrefix","safeCollectionPrefix","startsWith","buildUploadPrefix","useCompositePrefixes","safeDocPrefix","uploadPrefix","posix","join","sanitizedCollectionPrefix","sanitizedDocPrefix","buildUploadStoragePathData","filename","safeFilename","storageFilePath","fileKey","sanitizedFilename","buildStoragePathData"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AACvB,SAASC,gBAAgB,QAAQ,iBAAgB;AAEjD,SAASC,cAAc,QAAQ,sBAAqB;AAyBpD,OAAO,SAASC,oCAAoC,EAClDC,gBAAgB,EAChBC,SAAS,EAIV;IACC,MAAMC,uBAAuBJ,eAAeE,oBAAoB;IAEhE,OACE,CAACE,wBACDD,cAAcC,wBACdD,UAAUE,UAAU,CAAC,GAAGD,qBAAqB,CAAC,CAAC;AAEnD;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASE,kBAAkB,EAChCJ,gBAAgB,EAChBC,SAAS,EACTI,uBAAuB,KAAK,EACY;IAKxC,MAAMH,uBAAuBJ,eAAeE,oBAAoB;IAChE,MAAMM,gBAAgBR,eAAeG,aAAa;IAClD,MAAMM,eACJD,iBACA,CAACP,oCAAoC;QACnCC,kBAAkBE;QAClBD,WAAWK;IACb,KACIV,KAAKY,KAAK,CAACC,IAAI,CAACP,sBAAsBI,iBACtCA,iBAAiBJ;IAEvB,OAAO;QACLQ,2BAA2BR;QAC3BS,oBAAoB,CAACN,wBAAwBC,gBAAgBC,eAAeD;QAC5EC;IACF;AACF;AAEA;;;CAGC,GACD,OAAO,SAASK,2BAA2B,EACzCZ,gBAAgB,EAChBC,SAAS,EACTY,QAAQ,EACRR,uBAAuB,KAAK,EACN;IACtB,MAAM,EAAEK,yBAAyB,EAAEC,kBAAkB,EAAEJ,YAAY,EAAE,GAAGH,kBAAkB;QACxFJ;QACAC;QACAI;IACF;IACA,MAAMS,eAAejB,iBAAiBgB;IAEtC,MAAME,kBAAkBV,uBACpBT,KAAKY,KAAK,CAACC,IAAI,CAACC,2BAA2BC,oBAAoBG,gBAC/DlB,KAAKY,KAAK,CAACC,IAAI,CAACF,cAAcO;IAElC,OAAO;QACLE,SAASD;QACTL;QACAC;QACAM,mBAAmBH;QACnBC;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASG,qBAAqB,EACnClB,gBAAgB,EAChBC,SAAS,EACTY,QAAQ,EACRR,uBAAuB,KAAK,EACN;IACtB,MAAMK,4BAA4BZ,eAAeE,oBAAoB;IACrE,MAAMW,qBAAqBb,eAAeG,aAAa;IACvD,MAAMgB,oBAAoBpB,iBAAiBgB;IAC3C,MAAME,kBAAkBV,uBACpBT,KAAKY,KAAK,CAACC,IAAI,CAACC,2BAA2BC,oBAAoBM,qBAC/DrB,KAAKY,KAAK,CAACC,IAAI,CAACE,sBAAsBD,2BAA2BO;IAErE,OAAO;QACLD,SAASD;QACTL;QACAC;QACAM;QACAF;IACF;AACF"}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { buildStoragePathData, buildUploadStoragePathData, isStoragePathWithinCollectionPrefix } from './buildStoragePathData.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
describe('buildStoragePathData', ()=>{
|
|
4
|
+
describe('non-composite mode (useCompositePrefixes: false)', ()=>{
|
|
5
|
+
it('should preserve the key of a legacy document outside the collection prefix', ()=>{
|
|
6
|
+
const result = buildStoragePathData({
|
|
7
|
+
collectionPrefix: 'media/',
|
|
8
|
+
docPrefix: 'media-archive',
|
|
9
|
+
filename: 'test.png',
|
|
10
|
+
useCompositePrefixes: false
|
|
11
|
+
});
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
fileKey: 'media-archive/test.png',
|
|
14
|
+
sanitizedCollectionPrefix: 'media',
|
|
15
|
+
sanitizedDocPrefix: 'media-archive',
|
|
16
|
+
sanitizedFilename: 'test.png',
|
|
17
|
+
storageFilePath: 'media-archive/test.png'
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it('should fallback to collectionPrefix when docPrefix is empty', ()=>{
|
|
21
|
+
const result = buildStoragePathData({
|
|
22
|
+
collectionPrefix: 'collection',
|
|
23
|
+
docPrefix: '',
|
|
24
|
+
filename: 'test.png',
|
|
25
|
+
useCompositePrefixes: false
|
|
26
|
+
});
|
|
27
|
+
expect(result).toEqual({
|
|
28
|
+
fileKey: 'collection/test.png',
|
|
29
|
+
sanitizedCollectionPrefix: 'collection',
|
|
30
|
+
sanitizedDocPrefix: '',
|
|
31
|
+
sanitizedFilename: 'test.png',
|
|
32
|
+
storageFilePath: 'collection/test.png'
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it('should fallback to collectionPrefix when docPrefix is undefined', ()=>{
|
|
36
|
+
const result = buildStoragePathData({
|
|
37
|
+
collectionPrefix: 'collection',
|
|
38
|
+
filename: 'test.png',
|
|
39
|
+
useCompositePrefixes: false
|
|
40
|
+
});
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
fileKey: 'collection/test.png',
|
|
43
|
+
sanitizedCollectionPrefix: 'collection',
|
|
44
|
+
sanitizedDocPrefix: '',
|
|
45
|
+
sanitizedFilename: 'test.png',
|
|
46
|
+
storageFilePath: 'collection/test.png'
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('should return only filename when both prefixes are empty', ()=>{
|
|
50
|
+
const result = buildStoragePathData({
|
|
51
|
+
filename: 'test.png',
|
|
52
|
+
useCompositePrefixes: false
|
|
53
|
+
});
|
|
54
|
+
expect(result).toEqual({
|
|
55
|
+
fileKey: 'test.png',
|
|
56
|
+
sanitizedCollectionPrefix: '',
|
|
57
|
+
sanitizedDocPrefix: '',
|
|
58
|
+
sanitizedFilename: 'test.png',
|
|
59
|
+
storageFilePath: 'test.png'
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('composite mode (useCompositePrefixes: true)', ()=>{
|
|
64
|
+
it('should combine collectionPrefix and docPrefix', ()=>{
|
|
65
|
+
const result = buildStoragePathData({
|
|
66
|
+
collectionPrefix: 'collection',
|
|
67
|
+
docPrefix: 'document',
|
|
68
|
+
filename: 'test.png',
|
|
69
|
+
useCompositePrefixes: true
|
|
70
|
+
});
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
fileKey: 'collection/document/test.png',
|
|
73
|
+
sanitizedCollectionPrefix: 'collection',
|
|
74
|
+
sanitizedDocPrefix: 'document',
|
|
75
|
+
sanitizedFilename: 'test.png',
|
|
76
|
+
storageFilePath: 'collection/document/test.png'
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('should work with only collectionPrefix', ()=>{
|
|
80
|
+
const result = buildStoragePathData({
|
|
81
|
+
collectionPrefix: 'collection',
|
|
82
|
+
filename: 'test.png',
|
|
83
|
+
useCompositePrefixes: true
|
|
84
|
+
});
|
|
85
|
+
expect(result).toEqual({
|
|
86
|
+
fileKey: 'collection/test.png',
|
|
87
|
+
sanitizedCollectionPrefix: 'collection',
|
|
88
|
+
sanitizedDocPrefix: '',
|
|
89
|
+
sanitizedFilename: 'test.png',
|
|
90
|
+
storageFilePath: 'collection/test.png'
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
it('should work with only docPrefix', ()=>{
|
|
94
|
+
const result = buildStoragePathData({
|
|
95
|
+
docPrefix: 'document',
|
|
96
|
+
filename: 'test.png',
|
|
97
|
+
useCompositePrefixes: true
|
|
98
|
+
});
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
fileKey: 'document/test.png',
|
|
101
|
+
sanitizedCollectionPrefix: '',
|
|
102
|
+
sanitizedDocPrefix: 'document',
|
|
103
|
+
sanitizedFilename: 'test.png',
|
|
104
|
+
storageFilePath: 'document/test.png'
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it('should return only filename when both prefixes are empty', ()=>{
|
|
108
|
+
const result = buildStoragePathData({
|
|
109
|
+
filename: 'test.png',
|
|
110
|
+
useCompositePrefixes: true
|
|
111
|
+
});
|
|
112
|
+
expect(result).toEqual({
|
|
113
|
+
fileKey: 'test.png',
|
|
114
|
+
sanitizedCollectionPrefix: '',
|
|
115
|
+
sanitizedDocPrefix: '',
|
|
116
|
+
sanitizedFilename: 'test.png',
|
|
117
|
+
storageFilePath: 'test.png'
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('collection prefix boundaries', ()=>{
|
|
122
|
+
it('should accept a file key within a slash-terminated collection prefix', ()=>{
|
|
123
|
+
expect(isStoragePathWithinCollectionPrefix({
|
|
124
|
+
collectionPrefix: 'media/',
|
|
125
|
+
docPrefix: 'media/documents/test.png'
|
|
126
|
+
})).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('sanitization', ()=>{
|
|
130
|
+
it.each([
|
|
131
|
+
'./media/./images',
|
|
132
|
+
'media\\images',
|
|
133
|
+
'/media//images'
|
|
134
|
+
])('should canonicalize equivalent prefix paths', (docPrefix)=>{
|
|
135
|
+
const result = buildStoragePathData({
|
|
136
|
+
docPrefix,
|
|
137
|
+
filename: 'test.png'
|
|
138
|
+
});
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
fileKey: 'media/images/test.png',
|
|
141
|
+
sanitizedCollectionPrefix: '',
|
|
142
|
+
sanitizedDocPrefix: 'media/images',
|
|
143
|
+
sanitizedFilename: 'test.png',
|
|
144
|
+
storageFilePath: 'media/images/test.png'
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
it('should remove path traversal segments from collectionPrefix', ()=>{
|
|
148
|
+
const result = buildStoragePathData({
|
|
149
|
+
collectionPrefix: '../../../etc',
|
|
150
|
+
filename: 'test.png',
|
|
151
|
+
useCompositePrefixes: false
|
|
152
|
+
});
|
|
153
|
+
expect(result).toEqual({
|
|
154
|
+
fileKey: 'etc/test.png',
|
|
155
|
+
sanitizedCollectionPrefix: 'etc',
|
|
156
|
+
sanitizedDocPrefix: '',
|
|
157
|
+
sanitizedFilename: 'test.png',
|
|
158
|
+
storageFilePath: 'etc/test.png'
|
|
159
|
+
});
|
|
160
|
+
expect(result.fileKey).not.toContain('..');
|
|
161
|
+
});
|
|
162
|
+
it('should remove path traversal segments from docPrefix', ()=>{
|
|
163
|
+
const result = buildStoragePathData({
|
|
164
|
+
docPrefix: 'a/../../outside',
|
|
165
|
+
filename: 'test.png',
|
|
166
|
+
useCompositePrefixes: false
|
|
167
|
+
});
|
|
168
|
+
expect(result).toEqual({
|
|
169
|
+
fileKey: 'a/outside/test.png',
|
|
170
|
+
sanitizedCollectionPrefix: '',
|
|
171
|
+
sanitizedDocPrefix: 'a/outside',
|
|
172
|
+
sanitizedFilename: 'test.png',
|
|
173
|
+
storageFilePath: 'a/outside/test.png'
|
|
174
|
+
});
|
|
175
|
+
expect(result.fileKey).not.toContain('..');
|
|
176
|
+
});
|
|
177
|
+
it('should remove control characters from prefixes', ()=>{
|
|
178
|
+
const result = buildStoragePathData({
|
|
179
|
+
collectionPrefix: 'test\x00\x1fprefix',
|
|
180
|
+
filename: 'test.png',
|
|
181
|
+
useCompositePrefixes: false
|
|
182
|
+
});
|
|
183
|
+
expect(result).toEqual({
|
|
184
|
+
fileKey: 'testprefix/test.png',
|
|
185
|
+
sanitizedCollectionPrefix: 'testprefix',
|
|
186
|
+
sanitizedDocPrefix: '',
|
|
187
|
+
sanitizedFilename: 'test.png',
|
|
188
|
+
storageFilePath: 'testprefix/test.png'
|
|
189
|
+
});
|
|
190
|
+
expect(result.fileKey).not.toMatch(/[\x00-\x1f]/);
|
|
191
|
+
});
|
|
192
|
+
it('should normalize both prefixes after removing control characters in composite mode', ()=>{
|
|
193
|
+
const result = buildStoragePathData({
|
|
194
|
+
collectionPrefix: '.\x00./collection',
|
|
195
|
+
docPrefix: '.\x00./doc',
|
|
196
|
+
filename: 'test.png',
|
|
197
|
+
useCompositePrefixes: true
|
|
198
|
+
});
|
|
199
|
+
expect(result).toEqual({
|
|
200
|
+
fileKey: 'collection/doc/test.png',
|
|
201
|
+
sanitizedCollectionPrefix: 'collection',
|
|
202
|
+
sanitizedDocPrefix: 'doc',
|
|
203
|
+
sanitizedFilename: 'test.png',
|
|
204
|
+
storageFilePath: 'collection/doc/test.png'
|
|
205
|
+
});
|
|
206
|
+
expect(result.fileKey).not.toContain('..');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('buildUploadStoragePathData', ()=>{
|
|
211
|
+
it.each([
|
|
212
|
+
[
|
|
213
|
+
'invoices',
|
|
214
|
+
false,
|
|
215
|
+
'media/invoices/file.png',
|
|
216
|
+
'media/invoices'
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
'media/invoices',
|
|
220
|
+
false,
|
|
221
|
+
'media/invoices/file.png',
|
|
222
|
+
'media/invoices'
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
'',
|
|
226
|
+
false,
|
|
227
|
+
'media/file.png',
|
|
228
|
+
''
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
'invoices',
|
|
232
|
+
true,
|
|
233
|
+
'media/invoices/file.png',
|
|
234
|
+
'invoices'
|
|
235
|
+
]
|
|
236
|
+
])('should persist a readable destination for %s (composite %s)', (docPrefix, useCompositePrefixes, fileKey, storedPrefix)=>{
|
|
237
|
+
const result = buildUploadStoragePathData({
|
|
238
|
+
collectionPrefix: 'media',
|
|
239
|
+
docPrefix,
|
|
240
|
+
filename: 'file.png',
|
|
241
|
+
useCompositePrefixes
|
|
242
|
+
});
|
|
243
|
+
expect(result.storageFilePath).toBe(fileKey);
|
|
244
|
+
expect(result.sanitizedDocPrefix).toBe(storedPrefix);
|
|
245
|
+
expect(buildStoragePathData({
|
|
246
|
+
collectionPrefix: 'media',
|
|
247
|
+
docPrefix: result.sanitizedDocPrefix,
|
|
248
|
+
filename: 'file.png',
|
|
249
|
+
useCompositePrefixes
|
|
250
|
+
}).fileKey).toBe(fileKey);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
//# sourceMappingURL=buildStoragePathData.spec.js.map
|