@payloadcms/storage-azure 3.89.0-internal.95c9cba → 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 (53) hide show
  1. package/README.md +12 -9
  2. package/dist/adapter.d.ts +1 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js +10 -14
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/adapter.spec.d.ts +2 -0
  7. package/dist/adapter.spec.d.ts.map +1 -0
  8. package/dist/adapter.spec.js +15 -0
  9. package/dist/adapter.spec.js.map +1 -0
  10. package/dist/client/handleAzureUpload.d.ts +2 -3
  11. package/dist/client/handleAzureUpload.d.ts.map +1 -1
  12. package/dist/client/handleAzureUpload.js +11 -3
  13. package/dist/client/handleAzureUpload.js.map +1 -1
  14. package/dist/client/handleAzureUpload.spec.js +34 -7
  15. package/dist/client/handleAzureUpload.spec.js.map +1 -1
  16. package/dist/deleteFile.d.ts +2 -5
  17. package/dist/deleteFile.d.ts.map +1 -1
  18. package/dist/deleteFile.js +2 -9
  19. package/dist/deleteFile.js.map +1 -1
  20. package/dist/generateSignedURL.d.ts.map +1 -1
  21. package/dist/generateSignedURL.js +19 -3
  22. package/dist/generateSignedURL.js.map +1 -1
  23. package/dist/generateSignedURL.spec.d.ts +2 -0
  24. package/dist/generateSignedURL.spec.d.ts.map +1 -0
  25. package/dist/generateSignedURL.spec.js +65 -0
  26. package/dist/generateSignedURL.spec.js.map +1 -0
  27. package/dist/generateURL.js +3 -3
  28. package/dist/generateURL.js.map +1 -1
  29. package/dist/getFile.d.ts +3 -3
  30. package/dist/getFile.d.ts.map +1 -1
  31. package/dist/getFile.js +10 -14
  32. package/dist/getFile.js.map +1 -1
  33. package/dist/index.containerAccess.spec.d.ts +2 -0
  34. package/dist/index.containerAccess.spec.d.ts.map +1 -0
  35. package/dist/index.containerAccess.spec.js +78 -0
  36. package/dist/index.containerAccess.spec.js.map +1 -0
  37. package/dist/index.d.ts +23 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +14 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.spec.d.ts +2 -0
  42. package/dist/index.spec.d.ts.map +1 -0
  43. package/dist/index.spec.js +44 -0
  44. package/dist/index.spec.js.map +1 -0
  45. package/dist/isClientUploadAllowed.d.ts +3 -0
  46. package/dist/isClientUploadAllowed.d.ts.map +1 -0
  47. package/dist/isClientUploadAllowed.js +3 -0
  48. package/dist/isClientUploadAllowed.js.map +1 -0
  49. package/dist/uploadFile.d.ts +2 -5
  50. package/dist/uploadFile.d.ts.map +1 -1
  51. package/dist/uploadFile.js +2 -9
  52. package/dist/uploadFile.js.map +1 -1
  53. package/package.json +4 -4
package/README.md CHANGED
@@ -67,12 +67,15 @@ export default buildConfig({
67
67
 
68
68
  ### Configuration Options
69
69
 
70
- | Option | Description | Default |
71
- | ---------------------- | ------------------------------------------------------------------------ | ------- |
72
- | `enabled` | Whether or not to enable the plugin | `true` |
73
- | `collections` | Collections to apply the Azure Blob adapter to | |
74
- | `allowContainerCreate` | Whether or not to allow the container to be created if it does not exist | `false` |
75
- | `baseURL` | Base URL for the Azure Blob storage account | |
76
- | `connectionString` | Azure Blob storage connection string | |
77
- | `containerName` | Azure Blob storage container name | |
78
- | `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
70
+ | Option | Description | Default |
71
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
72
+ | `enabled` | Whether or not to enable the plugin | `true` |
73
+ | `collections` | Collections to apply the Azure Blob adapter to | |
74
+ | `allowContainerCreate` | Whether or not to allow the container to be created if it does not exist | `false` |
75
+ | `containerAccess` | Public access level for a container the plugin creates. `'private'` (default) serves blobs only through Payload's access-controlled route; `'blob'` / `'container'` expose them to anonymous clients directly from Azure. Has no effect on containers that already exist. | `'private'` |
76
+ | `baseURL` | Base URL for the Azure Blob storage account | |
77
+ | `connectionString` | Azure Blob storage connection string | |
78
+ | `containerName` | Azure Blob storage container name | |
79
+ | `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
80
+
81
+ > **Breaking change (private by default):** When `allowContainerCreate: true`, the plugin now creates containers with **private** access. Previously it created them with public `blob` access. If you relied on files being served directly from the anonymous Azure blob URL (typically alongside `disablePayloadAccessControl: true`), set `containerAccess: 'blob'` to restore the old behaviour. This only affects **newly created** containers — existing containers keep their current access level, so you must flip their ACL manually if needed.
package/dist/adapter.d.ts CHANGED
@@ -5,7 +5,7 @@ interface CreateAzureAdapterArgs {
5
5
  baseURL: string;
6
6
  clientUploads?: ClientUploadsConfig;
7
7
  containerName: string;
8
- createContainerIfNotExists: () => void;
8
+ createContainerIfNotExists: () => Promise<void> | void;
9
9
  getStorageClient: () => ContainerClient;
10
10
  useCompositePrefixes?: boolean;
11
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,KAAK,EACV,OAAO,EACP,mBAAmB,EAEpB,MAAM,wCAAwC,CAAA;AAO/C,UAAU,sBAAsB;IAC9B,oBAAoB,EAAE,OAAO,CAAA;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,aAAa,EAAE,MAAM,CAAA;IACrB,0BAA0B,EAAE,MAAM,IAAI,CAAA;IACtC,gBAAgB,EAAE,MAAM,eAAe,CAAA;IACvC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,oBAAoB,EACpB,OAAO,EACP,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,gBAAgB,EAChB,oBAA4B,GAC7B,EAAE,sBAAsB,GAAG,OAAO,CAyDlC"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,KAAK,EACV,OAAO,EACP,mBAAmB,EAEpB,MAAM,wCAAwC,CAAA;AAQ/C,UAAU,sBAAsB;IAC9B,oBAAoB,EAAE,OAAO,CAAA;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,aAAa,EAAE,MAAM,CAAA;IACrB,0BAA0B,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACtD,gBAAgB,EAAE,MAAM,eAAe,CAAA;IACvC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,oBAAoB,EACpB,OAAO,EACP,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,gBAAgB,EAChB,oBAA4B,GAC7B,EAAE,sBAAsB,GAAG,OAAO,CAiDlC"}
package/dist/adapter.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { deleteFile } from './deleteFile.js';
2
2
  import { generateURL } from './generateURL.js';
3
3
  import { getFile } from './getFile.js';
4
+ import { isAzureClientUploadAllowed } from './isClientUploadAllowed.js';
4
5
  import { uploadFile } from './uploadFile.js';
5
6
  export function createAzureAdapter({ allowContainerCreate, baseURL, clientUploads, containerName, createContainerIfNotExists, getStorageClient, useCompositePrefixes = false }) {
6
7
  return ({ collection, prefix = '' })=>({
7
8
  name: 'azure',
8
- clientUploads,
9
+ clientUploads: isAzureClientUploadAllowed(collection) ? clientUploads : false,
10
+ requiresClientUploadReceipt: true,
9
11
  generateURL: ({ filename, prefix: urlPrefix = '' })=>generateURL({
10
12
  baseURL,
11
13
  collectionPrefix: prefix,
@@ -14,34 +16,28 @@ export function createAzureAdapter({ allowContainerCreate, baseURL, clientUpload
14
16
  prefix: urlPrefix,
15
17
  useCompositePrefixes
16
18
  }),
17
- handleDelete: ({ doc: { prefix: docPrefix = '' }, filename })=>deleteFile({
19
+ handleDelete: ({ storageFilePath })=>deleteFile({
18
20
  client: getStorageClient(),
19
- collectionPrefix: prefix,
20
- docPrefix,
21
- filename,
22
- useCompositePrefixes
21
+ storageFilePath
23
22
  }),
24
- handleUpload: async ({ data, file })=>{
23
+ handleUpload: async ({ data, file, storageFilePath })=>{
25
24
  await uploadFile({
26
25
  buffer: file.buffer,
27
26
  client: getStorageClient(),
28
- collectionPrefix: prefix,
29
- docPrefix: data.prefix,
30
- filename: file.filename,
31
27
  mimeType: file.mimeType,
32
- tempFilePath: file.tempFilePath,
33
- useCompositePrefixes
28
+ storageFilePath,
29
+ tempFilePath: file.tempFilePath
34
30
  });
35
31
  return data;
36
32
  },
37
- staticHandler: (req, { headers, params: { clientUploadContext, filename, prefix: prefixQueryParam } })=>getFile({
33
+ staticHandler: (req, { doc, headers, params: { clientUploadContext, filename } })=>getFile({
38
34
  client: getStorageClient(),
39
35
  clientUploadContext,
40
36
  collection,
41
37
  collectionPrefix: prefix,
38
+ doc,
42
39
  filename,
43
40
  incomingHeaders: headers,
44
- prefixQueryParam,
45
41
  req,
46
42
  useCompositePrefixes
47
43
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapter.ts"],"sourcesContent":["import type { ContainerClient } from '@azure/storage-blob'\nimport type {\n Adapter,\n ClientUploadsConfig,\n GeneratedAdapter,\n} from '@payloadcms/plugin-cloud-storage/types'\n\nimport { deleteFile } from './deleteFile.js'\nimport { generateURL } from './generateURL.js'\nimport { getFile } from './getFile.js'\nimport { uploadFile } from './uploadFile.js'\n\ninterface CreateAzureAdapterArgs {\n allowContainerCreate: boolean\n baseURL: string\n clientUploads?: ClientUploadsConfig\n containerName: string\n createContainerIfNotExists: () => void\n getStorageClient: () => ContainerClient\n useCompositePrefixes?: boolean\n}\n\nexport function createAzureAdapter({\n allowContainerCreate,\n baseURL,\n clientUploads,\n containerName,\n createContainerIfNotExists,\n getStorageClient,\n useCompositePrefixes = false,\n}: CreateAzureAdapterArgs): Adapter {\n return ({ collection, prefix = '' }): GeneratedAdapter => ({\n name: 'azure',\n clientUploads,\n\n generateURL: ({ filename, prefix: urlPrefix = '' }) =>\n generateURL({\n baseURL,\n collectionPrefix: prefix,\n containerName,\n filename,\n prefix: urlPrefix,\n useCompositePrefixes,\n }),\n\n handleDelete: ({ doc: { prefix: docPrefix = '' }, filename }) =>\n deleteFile({\n client: getStorageClient(),\n collectionPrefix: prefix,\n docPrefix,\n filename,\n useCompositePrefixes,\n }),\n\n handleUpload: async ({ data, file }) => {\n await uploadFile({\n buffer: file.buffer,\n client: getStorageClient(),\n collectionPrefix: prefix,\n docPrefix: data.prefix,\n filename: file.filename,\n mimeType: file.mimeType,\n tempFilePath: file.tempFilePath,\n useCompositePrefixes,\n })\n\n return data\n },\n\n staticHandler: (\n req,\n { headers, params: { clientUploadContext, filename, prefix: prefixQueryParam } },\n ) =>\n getFile({\n client: getStorageClient(),\n clientUploadContext,\n collection,\n collectionPrefix: prefix,\n filename,\n incomingHeaders: headers,\n prefixQueryParam,\n req,\n useCompositePrefixes,\n }),\n\n ...(allowContainerCreate && { onInit: createContainerIfNotExists }),\n })\n}\n"],"names":["deleteFile","generateURL","getFile","uploadFile","createAzureAdapter","allowContainerCreate","baseURL","clientUploads","containerName","createContainerIfNotExists","getStorageClient","useCompositePrefixes","collection","prefix","name","filename","urlPrefix","collectionPrefix","handleDelete","doc","docPrefix","client","handleUpload","data","file","buffer","mimeType","tempFilePath","staticHandler","req","headers","params","clientUploadContext","prefixQueryParam","incomingHeaders","onInit"],"mappings":"AAOA,SAASA,UAAU,QAAQ,kBAAiB;AAC5C,SAASC,WAAW,QAAQ,mBAAkB;AAC9C,SAASC,OAAO,QAAQ,eAAc;AACtC,SAASC,UAAU,QAAQ,kBAAiB;AAY5C,OAAO,SAASC,mBAAmB,EACjCC,oBAAoB,EACpBC,OAAO,EACPC,aAAa,EACbC,aAAa,EACbC,0BAA0B,EAC1BC,gBAAgB,EAChBC,uBAAuB,KAAK,EACL;IACvB,OAAO,CAAC,EAAEC,UAAU,EAAEC,SAAS,EAAE,EAAE,GAAwB,CAAA;YACzDC,MAAM;YACNP;YAEAN,aAAa,CAAC,EAAEc,QAAQ,EAAEF,QAAQG,YAAY,EAAE,EAAE,GAChDf,YAAY;oBACVK;oBACAW,kBAAkBJ;oBAClBL;oBACAO;oBACAF,QAAQG;oBACRL;gBACF;YAEFO,cAAc,CAAC,EAAEC,KAAK,EAAEN,QAAQO,YAAY,EAAE,EAAE,EAAEL,QAAQ,EAAE,GAC1Df,WAAW;oBACTqB,QAAQX;oBACRO,kBAAkBJ;oBAClBO;oBACAL;oBACAJ;gBACF;YAEFW,cAAc,OAAO,EAAEC,IAAI,EAAEC,IAAI,EAAE;gBACjC,MAAMrB,WAAW;oBACfsB,QAAQD,KAAKC,MAAM;oBACnBJ,QAAQX;oBACRO,kBAAkBJ;oBAClBO,WAAWG,KAAKV,MAAM;oBACtBE,UAAUS,KAAKT,QAAQ;oBACvBW,UAAUF,KAAKE,QAAQ;oBACvBC,cAAcH,KAAKG,YAAY;oBAC/BhB;gBACF;gBAEA,OAAOY;YACT;YAEAK,eAAe,CACbC,KACA,EAAEC,OAAO,EAAEC,QAAQ,EAAEC,mBAAmB,EAAEjB,QAAQ,EAAEF,QAAQoB,gBAAgB,EAAE,EAAE,GAEhF/B,QAAQ;oBACNmB,QAAQX;oBACRsB;oBACApB;oBACAK,kBAAkBJ;oBAClBE;oBACAmB,iBAAiBJ;oBACjBG;oBACAJ;oBACAlB;gBACF;YAEF,GAAIN,wBAAwB;gBAAE8B,QAAQ1B;YAA2B,CAAC;QACpE,CAAA;AACF"}
1
+ {"version":3,"sources":["../src/adapter.ts"],"sourcesContent":["import type { ContainerClient } from '@azure/storage-blob'\nimport type {\n Adapter,\n ClientUploadsConfig,\n GeneratedAdapter,\n} from '@payloadcms/plugin-cloud-storage/types'\n\nimport { deleteFile } from './deleteFile.js'\nimport { generateURL } from './generateURL.js'\nimport { getFile } from './getFile.js'\nimport { isAzureClientUploadAllowed } from './isClientUploadAllowed.js'\nimport { uploadFile } from './uploadFile.js'\n\ninterface CreateAzureAdapterArgs {\n allowContainerCreate: boolean\n baseURL: string\n clientUploads?: ClientUploadsConfig\n containerName: string\n createContainerIfNotExists: () => Promise<void> | void\n getStorageClient: () => ContainerClient\n useCompositePrefixes?: boolean\n}\n\nexport function createAzureAdapter({\n allowContainerCreate,\n baseURL,\n clientUploads,\n containerName,\n createContainerIfNotExists,\n getStorageClient,\n useCompositePrefixes = false,\n}: CreateAzureAdapterArgs): Adapter {\n return ({ collection, prefix = '' }): GeneratedAdapter => ({\n name: 'azure',\n clientUploads: isAzureClientUploadAllowed(collection) ? clientUploads : false,\n requiresClientUploadReceipt: true,\n\n generateURL: ({ filename, prefix: urlPrefix = '' }) =>\n generateURL({\n baseURL,\n collectionPrefix: prefix,\n containerName,\n filename,\n prefix: urlPrefix,\n useCompositePrefixes,\n }),\n\n handleDelete: ({ storageFilePath }) =>\n deleteFile({\n client: getStorageClient(),\n storageFilePath,\n }),\n\n handleUpload: async ({ data, file, storageFilePath }) => {\n await uploadFile({\n buffer: file.buffer,\n client: getStorageClient(),\n mimeType: file.mimeType,\n storageFilePath,\n tempFilePath: file.tempFilePath,\n })\n\n return data\n },\n\n staticHandler: (req, { doc, headers, params: { clientUploadContext, filename } }) =>\n getFile({\n client: getStorageClient(),\n clientUploadContext,\n collection,\n collectionPrefix: prefix,\n doc,\n filename,\n incomingHeaders: headers,\n req,\n useCompositePrefixes,\n }),\n\n ...(allowContainerCreate && { onInit: createContainerIfNotExists }),\n })\n}\n"],"names":["deleteFile","generateURL","getFile","isAzureClientUploadAllowed","uploadFile","createAzureAdapter","allowContainerCreate","baseURL","clientUploads","containerName","createContainerIfNotExists","getStorageClient","useCompositePrefixes","collection","prefix","name","requiresClientUploadReceipt","filename","urlPrefix","collectionPrefix","handleDelete","storageFilePath","client","handleUpload","data","file","buffer","mimeType","tempFilePath","staticHandler","req","doc","headers","params","clientUploadContext","incomingHeaders","onInit"],"mappings":"AAOA,SAASA,UAAU,QAAQ,kBAAiB;AAC5C,SAASC,WAAW,QAAQ,mBAAkB;AAC9C,SAASC,OAAO,QAAQ,eAAc;AACtC,SAASC,0BAA0B,QAAQ,6BAA4B;AACvE,SAASC,UAAU,QAAQ,kBAAiB;AAY5C,OAAO,SAASC,mBAAmB,EACjCC,oBAAoB,EACpBC,OAAO,EACPC,aAAa,EACbC,aAAa,EACbC,0BAA0B,EAC1BC,gBAAgB,EAChBC,uBAAuB,KAAK,EACL;IACvB,OAAO,CAAC,EAAEC,UAAU,EAAEC,SAAS,EAAE,EAAE,GAAwB,CAAA;YACzDC,MAAM;YACNP,eAAeL,2BAA2BU,cAAcL,gBAAgB;YACxEQ,6BAA6B;YAE7Bf,aAAa,CAAC,EAAEgB,QAAQ,EAAEH,QAAQI,YAAY,EAAE,EAAE,GAChDjB,YAAY;oBACVM;oBACAY,kBAAkBL;oBAClBL;oBACAQ;oBACAH,QAAQI;oBACRN;gBACF;YAEFQ,cAAc,CAAC,EAAEC,eAAe,EAAE,GAChCrB,WAAW;oBACTsB,QAAQX;oBACRU;gBACF;YAEFE,cAAc,OAAO,EAAEC,IAAI,EAAEC,IAAI,EAAEJ,eAAe,EAAE;gBAClD,MAAMjB,WAAW;oBACfsB,QAAQD,KAAKC,MAAM;oBACnBJ,QAAQX;oBACRgB,UAAUF,KAAKE,QAAQ;oBACvBN;oBACAO,cAAcH,KAAKG,YAAY;gBACjC;gBAEA,OAAOJ;YACT;YAEAK,eAAe,CAACC,KAAK,EAAEC,GAAG,EAAEC,OAAO,EAAEC,QAAQ,EAAEC,mBAAmB,EAAEjB,QAAQ,EAAE,EAAE,GAC9Ef,QAAQ;oBACNoB,QAAQX;oBACRuB;oBACArB;oBACAM,kBAAkBL;oBAClBiB;oBACAd;oBACAkB,iBAAiBH;oBACjBF;oBACAlB;gBACF;YAEF,GAAIN,wBAAwB;gBAAE8B,QAAQ1B;YAA2B,CAAC;QACpE,CAAA;AACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.spec.d.ts","sourceRoot":"","sources":["../src/adapter.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { generateURL } from './generateURL.js';
3
+ describe('Azure storage adapter', ()=>{
4
+ it('should generate the pre-upgrade public URL for a legacy upload', ()=>{
5
+ expect(generateURL({
6
+ baseURL: 'https://storage.example.test',
7
+ collectionPrefix: 'docprefix-collection',
8
+ containerName: 'uploads',
9
+ filename: 'legacy.png',
10
+ prefix: 'legacy-invoices'
11
+ })).toBe('https://storage.example.test/uploads/legacy-invoices/legacy.png');
12
+ });
13
+ });
14
+
15
+ //# sourceMappingURL=adapter.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapter.spec.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\n\nimport { generateURL } from './generateURL.js'\n\ndescribe('Azure storage adapter', () => {\n it('should generate the pre-upgrade public URL for a legacy upload', () => {\n expect(\n generateURL({\n baseURL: 'https://storage.example.test',\n collectionPrefix: 'docprefix-collection',\n containerName: 'uploads',\n filename: 'legacy.png',\n prefix: 'legacy-invoices',\n }),\n ).toBe('https://storage.example.test/uploads/legacy-invoices/legacy.png')\n })\n})\n"],"names":["describe","expect","it","generateURL","baseURL","collectionPrefix","containerName","filename","prefix","toBe"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,WAAW,QAAQ,mBAAkB;AAE9CH,SAAS,yBAAyB;IAChCE,GAAG,kEAAkE;QACnED,OACEE,YAAY;YACVC,SAAS;YACTC,kBAAkB;YAClBC,eAAe;YACfC,UAAU;YACVC,QAAQ;QACV,IACAC,IAAI,CAAC;IACT;AACF"}
@@ -1,3 +1,4 @@
1
+ import type { ClientUploadContext } from '@payloadcms/plugin-cloud-storage/types';
1
2
  type HandleAzureUploadArgs = {
2
3
  apiRoute: string;
3
4
  /**
@@ -13,8 +14,6 @@ type HandleAzureUploadArgs = {
13
14
  serverURL: string;
14
15
  updateFilename: (value: string) => void;
15
16
  };
16
- export declare const handleAzureUpload: ({ apiRoute, chunkLargeFiles, collectionSlug, docPrefix, file, serverHandlerPath, serverURL, updateFilename, }: HandleAzureUploadArgs) => Promise<{
17
- prefix: string;
18
- }>;
17
+ export declare const handleAzureUpload: ({ apiRoute, chunkLargeFiles, collectionSlug, docPrefix, file, serverHandlerPath, serverURL, updateFilename, }: HandleAzureUploadArgs) => Promise<ClientUploadContext>;
19
18
  export {};
20
19
  //# sourceMappingURL=handleAzureUpload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handleAzureUpload.d.ts","sourceRoot":"","sources":["../../src/client/handleAzureUpload.ts"],"names":[],"mappings":"AAQA,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,eAAe,EAAE,OAAO,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,IAAI,CAAA;IACV,iBAAiB,EAAE,IAAI,MAAM,EAAE,CAAA;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,eAAO,MAAM,iBAAiB,kHAS3B,qBAAqB,KAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CA8DpD,CAAA"}
1
+ {"version":3,"file":"handleAzureUpload.d.ts","sourceRoot":"","sources":["../../src/client/handleAzureUpload.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AASjF,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,eAAe,EAAE,OAAO,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,IAAI,CAAA;IACV,iBAAiB,EAAE,IAAI,MAAM,EAAE,CAAA;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,eAAO,MAAM,iBAAiB,kHAS3B,qBAAqB,KAAG,OAAO,CAAC,mBAAmB,CAiFrD,CAAA"}
@@ -23,7 +23,10 @@ export const handleAzureUpload = async ({ apiRoute, chunkLargeFiles, collectionS
23
23
  if (!response.ok) {
24
24
  throw new Error('Failed to retrieve signed URL for Azure client upload');
25
25
  }
26
- const { docPrefix: sanitizedDocPrefix, filename: sanitizedFilename, url } = await response.json();
26
+ const { clientUploadContext, filename: sanitizedFilename, url } = await response.json();
27
+ if (typeof clientUploadContext !== 'object' || clientUploadContext === null || !('prefix' in clientUploadContext) || typeof clientUploadContext.prefix !== 'string' || !('signedReceipt' in clientUploadContext) || typeof clientUploadContext.signedReceipt !== 'string') {
28
+ throw new Error('Invalid Azure client upload reference');
29
+ }
27
30
  if (sanitizedFilename && sanitizedFilename !== file.name) {
28
31
  updateFilename(sanitizedFilename);
29
32
  }
@@ -40,7 +43,7 @@ export const handleAzureUpload = async ({ apiRoute, chunkLargeFiles, collectionS
40
43
  concurrency: CONCURRENCY
41
44
  });
42
45
  } else {
43
- await fetch(url, {
46
+ const upload = await fetch(url, {
44
47
  body: file,
45
48
  headers: {
46
49
  'Content-Length': file.size.toString(),
@@ -50,9 +53,14 @@ export const handleAzureUpload = async ({ apiRoute, chunkLargeFiles, collectionS
50
53
  },
51
54
  method: 'PUT'
52
55
  });
56
+ if (!upload.ok) {
57
+ throw new Error('Failed to upload file to Azure Blob Storage');
58
+ }
53
59
  }
54
60
  return {
55
- prefix: sanitizedDocPrefix
61
+ prefix: clientUploadContext.prefix,
62
+ // Validated as a string above; re-typed to its branded receipt form for the return contract.
63
+ signedReceipt: clientUploadContext.signedReceipt
56
64
  };
57
65
  };
58
66
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/handleAzureUpload.ts"],"sourcesContent":["'use client'\nimport { formatAdminURL } from 'payload/shared'\n\n// Balances upload parallelism against browser memory: `blockSize * concurrency`\n// bytes are buffered in flight (64 MiB * 4 = 256 MiB). Only used on the SDK path.\nconst BLOCK_SIZE = 64 * 1024 * 1024\nconst CONCURRENCY = 4\n\ntype HandleAzureUploadArgs = {\n apiRoute: string\n /**\n * When true, upload through the Azure Blob SDK, which splits the file into\n * blocks (Put Block + Put Block List) and lifts the ~5 GB single-request\n * limit. When false, use the legacy single Put Blob request.\n */\n chunkLargeFiles: boolean\n collectionSlug: string\n docPrefix?: string\n file: File\n serverHandlerPath: `/${string}`\n serverURL: string\n updateFilename: (value: string) => void\n}\n\nexport const handleAzureUpload = async ({\n apiRoute,\n chunkLargeFiles,\n collectionSlug,\n docPrefix,\n file,\n serverHandlerPath,\n serverURL,\n updateFilename,\n}: HandleAzureUploadArgs): Promise<{ prefix: string }> => {\n const endpointRoute = formatAdminURL({\n apiRoute,\n path: serverHandlerPath,\n serverURL,\n })\n\n const response = await fetch(endpointRoute, {\n body: JSON.stringify({\n collectionSlug,\n docPrefix,\n filename: file.name,\n mimeType: file.type,\n }),\n credentials: 'include',\n method: 'POST',\n })\n\n if (!response.ok) {\n throw new Error('Failed to retrieve signed URL for Azure client upload')\n }\n\n const {\n docPrefix: sanitizedDocPrefix,\n filename: sanitizedFilename,\n url,\n } = (await response.json()) as {\n docPrefix: string\n filename?: string\n url: string\n }\n\n if (sanitizedFilename && sanitizedFilename !== file.name) {\n updateFilename(sanitizedFilename)\n }\n\n if (chunkLargeFiles) {\n // Loaded on demand so @azure/storage-blob is only pulled into the client bundle\n // when a chunked client upload actually runs, rather than for every Azure user.\n const { BlockBlobClient } = await import('@azure/storage-blob')\n\n const blockBlobClient = new BlockBlobClient(url)\n\n await blockBlobClient.uploadData(file, {\n blobHTTPHeaders: { blobContentType: file.type },\n blockSize: BLOCK_SIZE,\n concurrency: CONCURRENCY,\n })\n } else {\n await fetch(url, {\n body: file,\n headers: {\n 'Content-Length': file.size.toString(),\n 'Content-Type': file.type,\n // Required for azure\n 'x-ms-blob-type': 'BlockBlob',\n },\n method: 'PUT',\n })\n }\n\n return { prefix: sanitizedDocPrefix }\n}\n"],"names":["formatAdminURL","BLOCK_SIZE","CONCURRENCY","handleAzureUpload","apiRoute","chunkLargeFiles","collectionSlug","docPrefix","file","serverHandlerPath","serverURL","updateFilename","endpointRoute","path","response","fetch","body","JSON","stringify","filename","name","mimeType","type","credentials","method","ok","Error","sanitizedDocPrefix","sanitizedFilename","url","json","BlockBlobClient","blockBlobClient","uploadData","blobHTTPHeaders","blobContentType","blockSize","concurrency","headers","size","toString","prefix"],"mappings":"AAAA;AACA,SAASA,cAAc,QAAQ,iBAAgB;AAE/C,gFAAgF;AAChF,kFAAkF;AAClF,MAAMC,aAAa,KAAK,OAAO;AAC/B,MAAMC,cAAc;AAkBpB,OAAO,MAAMC,oBAAoB,OAAO,EACtCC,QAAQ,EACRC,eAAe,EACfC,cAAc,EACdC,SAAS,EACTC,IAAI,EACJC,iBAAiB,EACjBC,SAAS,EACTC,cAAc,EACQ;IACtB,MAAMC,gBAAgBZ,eAAe;QACnCI;QACAS,MAAMJ;QACNC;IACF;IAEA,MAAMI,WAAW,MAAMC,MAAMH,eAAe;QAC1CI,MAAMC,KAAKC,SAAS,CAAC;YACnBZ;YACAC;YACAY,UAAUX,KAAKY,IAAI;YACnBC,UAAUb,KAAKc,IAAI;QACrB;QACAC,aAAa;QACbC,QAAQ;IACV;IAEA,IAAI,CAACV,SAASW,EAAE,EAAE;QAChB,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAM,EACJnB,WAAWoB,kBAAkB,EAC7BR,UAAUS,iBAAiB,EAC3BC,GAAG,EACJ,GAAI,MAAMf,SAASgB,IAAI;IAMxB,IAAIF,qBAAqBA,sBAAsBpB,KAAKY,IAAI,EAAE;QACxDT,eAAeiB;IACjB;IAEA,IAAIvB,iBAAiB;QACnB,gFAAgF;QAChF,gFAAgF;QAChF,MAAM,EAAE0B,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC;QAEzC,MAAMC,kBAAkB,IAAID,gBAAgBF;QAE5C,MAAMG,gBAAgBC,UAAU,CAACzB,MAAM;YACrC0B,iBAAiB;gBAAEC,iBAAiB3B,KAAKc,IAAI;YAAC;YAC9Cc,WAAWnC;YACXoC,aAAanC;QACf;IACF,OAAO;QACL,MAAMa,MAAMc,KAAK;YACfb,MAAMR;YACN8B,SAAS;gBACP,kBAAkB9B,KAAK+B,IAAI,CAACC,QAAQ;gBACpC,gBAAgBhC,KAAKc,IAAI;gBACzB,qBAAqB;gBACrB,kBAAkB;YACpB;YACAE,QAAQ;QACV;IACF;IAEA,OAAO;QAAEiB,QAAQd;IAAmB;AACtC,EAAC"}
1
+ {"version":3,"sources":["../../src/client/handleAzureUpload.ts"],"sourcesContent":["'use client'\nimport type { ClientUploadContext } from '@payloadcms/plugin-cloud-storage/types'\n\nimport { formatAdminURL } from 'payload/shared'\n\n// Balances upload parallelism against browser memory: `blockSize * concurrency`\n// bytes are buffered in flight (64 MiB * 4 = 256 MiB). Only used on the SDK path.\nconst BLOCK_SIZE = 64 * 1024 * 1024\nconst CONCURRENCY = 4\n\ntype HandleAzureUploadArgs = {\n apiRoute: string\n /**\n * When true, upload through the Azure Blob SDK, which splits the file into\n * blocks (Put Block + Put Block List) and lifts the ~5 GB single-request\n * limit. When false, use the legacy single Put Blob request.\n */\n chunkLargeFiles: boolean\n collectionSlug: string\n docPrefix?: string\n file: File\n serverHandlerPath: `/${string}`\n serverURL: string\n updateFilename: (value: string) => void\n}\n\nexport const handleAzureUpload = async ({\n apiRoute,\n chunkLargeFiles,\n collectionSlug,\n docPrefix,\n file,\n serverHandlerPath,\n serverURL,\n updateFilename,\n}: HandleAzureUploadArgs): Promise<ClientUploadContext> => {\n const endpointRoute = formatAdminURL({\n apiRoute,\n path: serverHandlerPath,\n serverURL,\n })\n\n const response = await fetch(endpointRoute, {\n body: JSON.stringify({\n collectionSlug,\n docPrefix,\n filename: file.name,\n mimeType: file.type,\n }),\n credentials: 'include',\n method: 'POST',\n })\n\n if (!response.ok) {\n throw new Error('Failed to retrieve signed URL for Azure client upload')\n }\n\n const {\n clientUploadContext,\n filename: sanitizedFilename,\n url,\n } = (await response.json()) as {\n clientUploadContext?: unknown\n filename?: string\n url: string\n }\n\n if (\n typeof clientUploadContext !== 'object' ||\n clientUploadContext === null ||\n !('prefix' in clientUploadContext) ||\n typeof clientUploadContext.prefix !== 'string' ||\n !('signedReceipt' in clientUploadContext) ||\n typeof clientUploadContext.signedReceipt !== 'string'\n ) {\n throw new Error('Invalid Azure client upload reference')\n }\n\n if (sanitizedFilename && sanitizedFilename !== file.name) {\n updateFilename(sanitizedFilename)\n }\n\n if (chunkLargeFiles) {\n // Loaded on demand so @azure/storage-blob is only pulled into the client bundle\n // when a chunked client upload actually runs, rather than for every Azure user.\n const { BlockBlobClient } = await import('@azure/storage-blob')\n\n const blockBlobClient = new BlockBlobClient(url)\n\n await blockBlobClient.uploadData(file, {\n blobHTTPHeaders: { blobContentType: file.type },\n blockSize: BLOCK_SIZE,\n concurrency: CONCURRENCY,\n })\n } else {\n const upload = await fetch(url, {\n body: file,\n headers: {\n 'Content-Length': file.size.toString(),\n 'Content-Type': file.type,\n // Required for azure\n 'x-ms-blob-type': 'BlockBlob',\n },\n method: 'PUT',\n })\n\n if (!upload.ok) {\n throw new Error('Failed to upload file to Azure Blob Storage')\n }\n }\n\n return {\n prefix: clientUploadContext.prefix,\n // Validated as a string above; re-typed to its branded receipt form for the return contract.\n signedReceipt: clientUploadContext.signedReceipt as ClientUploadContext['signedReceipt'],\n }\n}\n"],"names":["formatAdminURL","BLOCK_SIZE","CONCURRENCY","handleAzureUpload","apiRoute","chunkLargeFiles","collectionSlug","docPrefix","file","serverHandlerPath","serverURL","updateFilename","endpointRoute","path","response","fetch","body","JSON","stringify","filename","name","mimeType","type","credentials","method","ok","Error","clientUploadContext","sanitizedFilename","url","json","prefix","signedReceipt","BlockBlobClient","blockBlobClient","uploadData","blobHTTPHeaders","blobContentType","blockSize","concurrency","upload","headers","size","toString"],"mappings":"AAAA;AAGA,SAASA,cAAc,QAAQ,iBAAgB;AAE/C,gFAAgF;AAChF,kFAAkF;AAClF,MAAMC,aAAa,KAAK,OAAO;AAC/B,MAAMC,cAAc;AAkBpB,OAAO,MAAMC,oBAAoB,OAAO,EACtCC,QAAQ,EACRC,eAAe,EACfC,cAAc,EACdC,SAAS,EACTC,IAAI,EACJC,iBAAiB,EACjBC,SAAS,EACTC,cAAc,EACQ;IACtB,MAAMC,gBAAgBZ,eAAe;QACnCI;QACAS,MAAMJ;QACNC;IACF;IAEA,MAAMI,WAAW,MAAMC,MAAMH,eAAe;QAC1CI,MAAMC,KAAKC,SAAS,CAAC;YACnBZ;YACAC;YACAY,UAAUX,KAAKY,IAAI;YACnBC,UAAUb,KAAKc,IAAI;QACrB;QACAC,aAAa;QACbC,QAAQ;IACV;IAEA,IAAI,CAACV,SAASW,EAAE,EAAE;QAChB,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAM,EACJC,mBAAmB,EACnBR,UAAUS,iBAAiB,EAC3BC,GAAG,EACJ,GAAI,MAAMf,SAASgB,IAAI;IAMxB,IACE,OAAOH,wBAAwB,YAC/BA,wBAAwB,QACxB,CAAE,CAAA,YAAYA,mBAAkB,KAChC,OAAOA,oBAAoBI,MAAM,KAAK,YACtC,CAAE,CAAA,mBAAmBJ,mBAAkB,KACvC,OAAOA,oBAAoBK,aAAa,KAAK,UAC7C;QACA,MAAM,IAAIN,MAAM;IAClB;IAEA,IAAIE,qBAAqBA,sBAAsBpB,KAAKY,IAAI,EAAE;QACxDT,eAAeiB;IACjB;IAEA,IAAIvB,iBAAiB;QACnB,gFAAgF;QAChF,gFAAgF;QAChF,MAAM,EAAE4B,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC;QAEzC,MAAMC,kBAAkB,IAAID,gBAAgBJ;QAE5C,MAAMK,gBAAgBC,UAAU,CAAC3B,MAAM;YACrC4B,iBAAiB;gBAAEC,iBAAiB7B,KAAKc,IAAI;YAAC;YAC9CgB,WAAWrC;YACXsC,aAAarC;QACf;IACF,OAAO;QACL,MAAMsC,SAAS,MAAMzB,MAAMc,KAAK;YAC9Bb,MAAMR;YACNiC,SAAS;gBACP,kBAAkBjC,KAAKkC,IAAI,CAACC,QAAQ;gBACpC,gBAAgBnC,KAAKc,IAAI;gBACzB,qBAAqB;gBACrB,kBAAkB;YACpB;YACAE,QAAQ;QACV;QAEA,IAAI,CAACgB,OAAOf,EAAE,EAAE;YACd,MAAM,IAAIC,MAAM;QAClB;IACF;IAEA,OAAO;QACLK,QAAQJ,oBAAoBI,MAAM;QAClC,6FAA6F;QAC7FC,eAAeL,oBAAoBK,aAAa;IAClD;AACF,EAAC"}
@@ -13,6 +13,10 @@ const serverURL = 'https://example.com';
13
13
  const apiRoute = '/api';
14
14
  const serverHandlerPath = '/storage-azure-generate-signed-url';
15
15
  const signedURL = 'https://account.blob.core.windows.net/container/file.png?sig=abc';
16
+ const clientUploadContext = {
17
+ prefix: 'docs',
18
+ signedReceipt: 'receipt'
19
+ };
16
20
  const createFile = ()=>new File([
17
21
  new Uint8Array([
18
22
  1,
@@ -24,7 +28,10 @@ const createFile = ()=>new File([
24
28
  });
25
29
  const mockSignedURLResponse = (body, ok = true)=>{
26
30
  const fetchMock = vi.fn().mockResolvedValue({
27
- json: ()=>Promise.resolve(body),
31
+ json: ()=>Promise.resolve({
32
+ clientUploadContext,
33
+ ...body
34
+ }),
28
35
  ok
29
36
  });
30
37
  vi.stubGlobal('fetch', fetchMock);
@@ -89,15 +96,15 @@ describe('handleAzureUpload', ()=>{
89
96
  });
90
97
  expect(updateFilename).not.toHaveBeenCalled();
91
98
  });
92
- it('should return the sanitized doc prefix', async ()=>{
99
+ it('should reject a missing upload reference before uploading', async ()=>{
93
100
  mockSignedURLResponse({
94
- docPrefix: 'sanitized-docs',
101
+ clientUploadContext: undefined,
102
+ docPrefix: 'docs',
95
103
  url: signedURL
96
104
  });
97
- const result = await invoke();
98
- expect(result).toEqual({
99
- prefix: 'sanitized-docs'
100
- });
105
+ await expect(invoke()).rejects.toThrow('Invalid Azure client upload reference');
106
+ expect(uploadDataMock).not.toHaveBeenCalled();
107
+ expect(blockBlobClientMock).not.toHaveBeenCalled();
101
108
  });
102
109
  it('should throw when the signed URL request fails, before any upload', async ()=>{
103
110
  mockSignedURLResponse({}, false);
@@ -181,6 +188,26 @@ describe('handleAzureUpload', ()=>{
181
188
  expect(blockBlobClientMock).not.toHaveBeenCalled();
182
189
  expect(uploadDataMock).not.toHaveBeenCalled();
183
190
  });
191
+ it('should throw when the raw PUT is rejected', async ()=>{
192
+ const fetchMock = mockSignedURLResponse({
193
+ docPrefix: 'docs',
194
+ url: signedURL
195
+ });
196
+ fetchMock.mockResolvedValueOnce({
197
+ json: ()=>Promise.resolve({
198
+ clientUploadContext,
199
+ docPrefix: 'docs',
200
+ url: signedURL
201
+ }),
202
+ ok: true
203
+ });
204
+ fetchMock.mockResolvedValueOnce({
205
+ ok: false
206
+ });
207
+ await expect(invoke({
208
+ chunkLargeFiles: false
209
+ })).rejects.toThrow('Failed to upload file to Azure Blob Storage');
210
+ });
184
211
  });
185
212
  });
186
213
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/handleAzureUpload.spec.ts"],"sourcesContent":["import { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst uploadDataMock = vi.fn()\nconst blockBlobClientMock = vi.fn(function () {\n return { uploadData: uploadDataMock }\n})\n\nvi.mock('@azure/storage-blob', () => ({\n BlockBlobClient: blockBlobClientMock,\n}))\n\nimport { handleAzureUpload } from './handleAzureUpload.js'\n\nconst serverURL = 'https://example.com'\nconst apiRoute = '/api'\nconst serverHandlerPath = '/storage-azure-generate-signed-url' as const\nconst signedURL = 'https://account.blob.core.windows.net/container/file.png?sig=abc'\n\nconst createFile = () => new File([new Uint8Array([1, 2, 3])], 'file.png', { type: 'image/png' })\n\nconst mockSignedURLResponse = (body: Record<string, unknown>, ok = true) => {\n const fetchMock = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(body),\n ok,\n })\n vi.stubGlobal('fetch', fetchMock)\n return fetchMock\n}\n\nconst invoke = (overrides: Partial<Parameters<typeof handleAzureUpload>[0]> = {}) =>\n handleAzureUpload({\n apiRoute,\n chunkLargeFiles: true,\n collectionSlug: 'media',\n file: createFile(),\n serverHandlerPath,\n serverURL,\n updateFilename: vi.fn(),\n ...overrides,\n })\n\ndescribe('handleAzureUpload', () => {\n beforeEach(() => {\n vi.clearAllMocks()\n vi.unstubAllGlobals()\n uploadDataMock.mockResolvedValue(undefined)\n })\n\n describe('shared behavior', () => {\n it('should request a signed URL from the server handler', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n\n await invoke({ docPrefix: 'docs' })\n\n const [calledURL, init] = fetchMock.mock.calls[0]!\n expect(calledURL).toBe(`${serverURL}${apiRoute}${serverHandlerPath}`)\n expect(init.method).toBe('POST')\n expect(JSON.parse(init.body)).toEqual({\n collectionSlug: 'media',\n docPrefix: 'docs',\n filename: 'file.png',\n mimeType: 'image/png',\n })\n })\n\n it('should call updateFilename when the server returns a sanitized filename', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', filename: 'file-1.png', url: signedURL })\n const updateFilename = vi.fn()\n\n await invoke({ updateFilename })\n\n expect(updateFilename).toHaveBeenCalledWith('file-1.png')\n })\n\n it('should not call updateFilename when the filename is unchanged', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', filename: 'file.png', url: signedURL })\n const updateFilename = vi.fn()\n\n await invoke({ updateFilename })\n\n expect(updateFilename).not.toHaveBeenCalled()\n })\n\n it('should return the sanitized doc prefix', async () => {\n mockSignedURLResponse({ docPrefix: 'sanitized-docs', url: signedURL })\n\n const result = await invoke()\n\n expect(result).toEqual({ prefix: 'sanitized-docs' })\n })\n\n it('should throw when the signed URL request fails, before any upload', async () => {\n mockSignedURLResponse({}, false)\n\n await expect(invoke()).rejects.toThrow()\n\n expect(uploadDataMock).not.toHaveBeenCalled()\n expect(blockBlobClientMock).not.toHaveBeenCalled()\n })\n })\n\n describe('chunkLargeFiles: true (SDK block upload)', () => {\n it('should upload via BlockBlobClient.uploadData rather than a raw PUT', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n const file = createFile()\n\n await invoke({ chunkLargeFiles: true, file })\n\n expect(blockBlobClientMock).toHaveBeenCalledWith(signedURL)\n expect(uploadDataMock).toHaveBeenCalledTimes(1)\n\n const [uploadedFile, options] = uploadDataMock.mock.calls[0]!\n expect(uploadedFile).toBe(file)\n expect(options.blockSize).toBeGreaterThan(0)\n expect(options.concurrency).toBeGreaterThan(0)\n\n // Only the signed-URL request should hit fetch; no raw PUT to the blob URL.\n expect(fetchMock).toHaveBeenCalledTimes(1)\n })\n\n it('should pass the file content type to the blob headers', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n\n await invoke({ chunkLargeFiles: true })\n\n const [, options] = uploadDataMock.mock.calls[0]!\n expect(options.blobHTTPHeaders).toEqual({ blobContentType: 'image/png' })\n })\n\n it('should propagate errors from uploadData', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n uploadDataMock.mockRejectedValue(new Error('block upload failed'))\n\n await expect(invoke({ chunkLargeFiles: true })).rejects.toThrow('block upload failed')\n })\n })\n\n describe('chunkLargeFiles: false (single PUT, default/legacy)', () => {\n it('should upload via a single raw PUT with the BlockBlob header', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n const file = createFile()\n\n await invoke({ chunkLargeFiles: false, file })\n\n // 1st fetch = signed-URL POST, 2nd = the raw PUT to the blob URL\n expect(fetchMock).toHaveBeenCalledTimes(2)\n const [putURL, putInit] = fetchMock.mock.calls[1]!\n expect(putURL).toBe(signedURL)\n expect(putInit.method).toBe('PUT')\n expect(putInit.body).toBe(file)\n expect(putInit.headers['x-ms-blob-type']).toBe('BlockBlob')\n expect(putInit.headers['Content-Type']).toBe('image/png')\n })\n\n it('should not use the Azure SDK', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n\n await invoke({ chunkLargeFiles: false })\n\n expect(blockBlobClientMock).not.toHaveBeenCalled()\n expect(uploadDataMock).not.toHaveBeenCalled()\n })\n })\n})\n"],"names":["beforeEach","describe","expect","it","vi","uploadDataMock","fn","blockBlobClientMock","uploadData","mock","BlockBlobClient","handleAzureUpload","serverURL","apiRoute","serverHandlerPath","signedURL","createFile","File","Uint8Array","type","mockSignedURLResponse","body","ok","fetchMock","mockResolvedValue","json","Promise","resolve","stubGlobal","invoke","overrides","chunkLargeFiles","collectionSlug","file","updateFilename","clearAllMocks","unstubAllGlobals","undefined","docPrefix","url","calledURL","init","calls","toBe","method","JSON","parse","toEqual","filename","mimeType","toHaveBeenCalledWith","not","toHaveBeenCalled","result","prefix","rejects","toThrow","toHaveBeenCalledTimes","uploadedFile","options","blockSize","toBeGreaterThan","concurrency","blobHTTPHeaders","blobContentType","mockRejectedValue","Error","putURL","putInit","headers"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/client/handleAzureUpload.spec.ts"],"sourcesContent":["import { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst uploadDataMock = vi.fn()\nconst blockBlobClientMock = vi.fn(function () {\n return { uploadData: uploadDataMock }\n})\n\nvi.mock('@azure/storage-blob', () => ({\n BlockBlobClient: blockBlobClientMock,\n}))\n\nimport { handleAzureUpload } from './handleAzureUpload.js'\n\nconst serverURL = 'https://example.com'\nconst apiRoute = '/api'\nconst serverHandlerPath = '/storage-azure-generate-signed-url' as const\nconst signedURL = 'https://account.blob.core.windows.net/container/file.png?sig=abc'\nconst clientUploadContext = { prefix: 'docs', signedReceipt: 'receipt' }\n\nconst createFile = () => new File([new Uint8Array([1, 2, 3])], 'file.png', { type: 'image/png' })\n\nconst mockSignedURLResponse = (body: Record<string, unknown>, ok = true) => {\n const fetchMock = vi.fn().mockResolvedValue({\n json: () => Promise.resolve({ clientUploadContext, ...body }),\n ok,\n })\n vi.stubGlobal('fetch', fetchMock)\n return fetchMock\n}\n\nconst invoke = (overrides: Partial<Parameters<typeof handleAzureUpload>[0]> = {}) =>\n handleAzureUpload({\n apiRoute,\n chunkLargeFiles: true,\n collectionSlug: 'media',\n file: createFile(),\n serverHandlerPath,\n serverURL,\n updateFilename: vi.fn(),\n ...overrides,\n })\n\ndescribe('handleAzureUpload', () => {\n beforeEach(() => {\n vi.clearAllMocks()\n vi.unstubAllGlobals()\n uploadDataMock.mockResolvedValue(undefined)\n })\n\n describe('shared behavior', () => {\n it('should request a signed URL from the server handler', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n\n await invoke({ docPrefix: 'docs' })\n\n const [calledURL, init] = fetchMock.mock.calls[0]!\n expect(calledURL).toBe(`${serverURL}${apiRoute}${serverHandlerPath}`)\n expect(init.method).toBe('POST')\n expect(JSON.parse(init.body)).toEqual({\n collectionSlug: 'media',\n docPrefix: 'docs',\n filename: 'file.png',\n mimeType: 'image/png',\n })\n })\n\n it('should call updateFilename when the server returns a sanitized filename', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', filename: 'file-1.png', url: signedURL })\n const updateFilename = vi.fn()\n\n await invoke({ updateFilename })\n\n expect(updateFilename).toHaveBeenCalledWith('file-1.png')\n })\n\n it('should not call updateFilename when the filename is unchanged', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', filename: 'file.png', url: signedURL })\n const updateFilename = vi.fn()\n\n await invoke({ updateFilename })\n\n expect(updateFilename).not.toHaveBeenCalled()\n })\n\n it('should reject a missing upload reference before uploading', async () => {\n mockSignedURLResponse({ clientUploadContext: undefined, docPrefix: 'docs', url: signedURL })\n\n await expect(invoke()).rejects.toThrow('Invalid Azure client upload reference')\n\n expect(uploadDataMock).not.toHaveBeenCalled()\n expect(blockBlobClientMock).not.toHaveBeenCalled()\n })\n\n it('should throw when the signed URL request fails, before any upload', async () => {\n mockSignedURLResponse({}, false)\n\n await expect(invoke()).rejects.toThrow()\n\n expect(uploadDataMock).not.toHaveBeenCalled()\n expect(blockBlobClientMock).not.toHaveBeenCalled()\n })\n })\n\n describe('chunkLargeFiles: true (SDK block upload)', () => {\n it('should upload via BlockBlobClient.uploadData rather than a raw PUT', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n const file = createFile()\n\n await invoke({ chunkLargeFiles: true, file })\n\n expect(blockBlobClientMock).toHaveBeenCalledWith(signedURL)\n expect(uploadDataMock).toHaveBeenCalledTimes(1)\n\n const [uploadedFile, options] = uploadDataMock.mock.calls[0]!\n expect(uploadedFile).toBe(file)\n expect(options.blockSize).toBeGreaterThan(0)\n expect(options.concurrency).toBeGreaterThan(0)\n\n // Only the signed-URL request should hit fetch; no raw PUT to the blob URL.\n expect(fetchMock).toHaveBeenCalledTimes(1)\n })\n\n it('should pass the file content type to the blob headers', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n\n await invoke({ chunkLargeFiles: true })\n\n const [, options] = uploadDataMock.mock.calls[0]!\n expect(options.blobHTTPHeaders).toEqual({ blobContentType: 'image/png' })\n })\n\n it('should propagate errors from uploadData', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n uploadDataMock.mockRejectedValue(new Error('block upload failed'))\n\n await expect(invoke({ chunkLargeFiles: true })).rejects.toThrow('block upload failed')\n })\n })\n\n describe('chunkLargeFiles: false (single PUT, default/legacy)', () => {\n it('should upload via a single raw PUT with the BlockBlob header', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n const file = createFile()\n\n await invoke({ chunkLargeFiles: false, file })\n\n // 1st fetch = signed-URL POST, 2nd = the raw PUT to the blob URL\n expect(fetchMock).toHaveBeenCalledTimes(2)\n const [putURL, putInit] = fetchMock.mock.calls[1]!\n expect(putURL).toBe(signedURL)\n expect(putInit.method).toBe('PUT')\n expect(putInit.body).toBe(file)\n expect(putInit.headers['x-ms-blob-type']).toBe('BlockBlob')\n expect(putInit.headers['Content-Type']).toBe('image/png')\n })\n\n it('should not use the Azure SDK', async () => {\n mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n\n await invoke({ chunkLargeFiles: false })\n\n expect(blockBlobClientMock).not.toHaveBeenCalled()\n expect(uploadDataMock).not.toHaveBeenCalled()\n })\n\n it('should throw when the raw PUT is rejected', async () => {\n const fetchMock = mockSignedURLResponse({ docPrefix: 'docs', url: signedURL })\n fetchMock.mockResolvedValueOnce({\n json: () => Promise.resolve({ clientUploadContext, docPrefix: 'docs', url: signedURL }),\n ok: true,\n })\n fetchMock.mockResolvedValueOnce({ ok: false })\n\n await expect(invoke({ chunkLargeFiles: false })).rejects.toThrow(\n 'Failed to upload file to Azure Blob Storage',\n )\n })\n })\n})\n"],"names":["beforeEach","describe","expect","it","vi","uploadDataMock","fn","blockBlobClientMock","uploadData","mock","BlockBlobClient","handleAzureUpload","serverURL","apiRoute","serverHandlerPath","signedURL","clientUploadContext","prefix","signedReceipt","createFile","File","Uint8Array","type","mockSignedURLResponse","body","ok","fetchMock","mockResolvedValue","json","Promise","resolve","stubGlobal","invoke","overrides","chunkLargeFiles","collectionSlug","file","updateFilename","clearAllMocks","unstubAllGlobals","undefined","docPrefix","url","calledURL","init","calls","toBe","method","JSON","parse","toEqual","filename","mimeType","toHaveBeenCalledWith","not","toHaveBeenCalled","rejects","toThrow","toHaveBeenCalledTimes","uploadedFile","options","blockSize","toBeGreaterThan","concurrency","blobHTTPHeaders","blobContentType","mockRejectedValue","Error","putURL","putInit","headers","mockResolvedValueOnce"],"mappings":"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"}
@@ -1,11 +1,8 @@
1
1
  import type { ContainerClient } from '@azure/storage-blob';
2
2
  interface DeleteArgs {
3
3
  client: ContainerClient;
4
- collectionPrefix?: string;
5
- docPrefix: string;
6
- filename: string;
7
- useCompositePrefixes?: boolean;
4
+ storageFilePath: string;
8
5
  }
9
- export declare function deleteFile({ client, collectionPrefix, docPrefix, filename, useCompositePrefixes, }: DeleteArgs): Promise<void>;
6
+ export declare function deleteFile({ client, storageFilePath }: DeleteArgs): Promise<void>;
10
7
  export {};
11
8
  //# sourceMappingURL=deleteFile.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deleteFile.d.ts","sourceRoot":"","sources":["../src/deleteFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAI1D,UAAU,UAAU;IAClB,MAAM,EAAE,eAAe,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,wBAAsB,UAAU,CAAC,EAC/B,MAAM,EACN,gBAAqB,EACrB,SAAS,EACT,QAAQ,EACR,oBAA4B,GAC7B,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5B"}
1
+ {"version":3,"file":"deleteFile.d.ts","sourceRoot":"","sources":["../src/deleteFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAE1D,UAAU,UAAU;IAClB,MAAM,EAAE,eAAe,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,wBAAsB,UAAU,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvF"}
@@ -1,12 +1,5 @@
1
- import { getFileKey } from '@payloadcms/plugin-cloud-storage/utilities';
2
- export async function deleteFile({ client, collectionPrefix = '', docPrefix, filename, useCompositePrefixes = false }) {
3
- const { fileKey } = getFileKey({
4
- collectionPrefix,
5
- docPrefix,
6
- filename,
7
- useCompositePrefixes
8
- });
9
- const blockBlobClient = client.getBlockBlobClient(fileKey);
1
+ export async function deleteFile({ client, storageFilePath }) {
2
+ const blockBlobClient = client.getBlockBlobClient(storageFilePath);
10
3
  await blockBlobClient.deleteIfExists();
11
4
  }
12
5
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/deleteFile.ts"],"sourcesContent":["import type { ContainerClient } from '@azure/storage-blob'\n\nimport { getFileKey } from '@payloadcms/plugin-cloud-storage/utilities'\n\ninterface DeleteArgs {\n client: ContainerClient\n collectionPrefix?: string\n docPrefix: string\n filename: string\n useCompositePrefixes?: boolean\n}\n\nexport async function deleteFile({\n client,\n collectionPrefix = '',\n docPrefix,\n filename,\n useCompositePrefixes = false,\n}: DeleteArgs): Promise<void> {\n const { fileKey } = getFileKey({\n collectionPrefix,\n docPrefix,\n filename,\n useCompositePrefixes,\n })\n\n const blockBlobClient = client.getBlockBlobClient(fileKey)\n\n await blockBlobClient.deleteIfExists()\n}\n"],"names":["getFileKey","deleteFile","client","collectionPrefix","docPrefix","filename","useCompositePrefixes","fileKey","blockBlobClient","getBlockBlobClient","deleteIfExists"],"mappings":"AAEA,SAASA,UAAU,QAAQ,6CAA4C;AAUvE,OAAO,eAAeC,WAAW,EAC/BC,MAAM,EACNC,mBAAmB,EAAE,EACrBC,SAAS,EACTC,QAAQ,EACRC,uBAAuB,KAAK,EACjB;IACX,MAAM,EAAEC,OAAO,EAAE,GAAGP,WAAW;QAC7BG;QACAC;QACAC;QACAC;IACF;IAEA,MAAME,kBAAkBN,OAAOO,kBAAkB,CAACF;IAElD,MAAMC,gBAAgBE,cAAc;AACtC"}
1
+ {"version":3,"sources":["../src/deleteFile.ts"],"sourcesContent":["import type { ContainerClient } from '@azure/storage-blob'\n\ninterface DeleteArgs {\n client: ContainerClient\n storageFilePath: string\n}\n\nexport async function deleteFile({ client, storageFilePath }: DeleteArgs): Promise<void> {\n const blockBlobClient = client.getBlockBlobClient(storageFilePath)\n\n await blockBlobClient.deleteIfExists()\n}\n"],"names":["deleteFile","client","storageFilePath","blockBlobClient","getBlockBlobClient","deleteIfExists"],"mappings":"AAOA,OAAO,eAAeA,WAAW,EAAEC,MAAM,EAAEC,eAAe,EAAc;IACtE,MAAMC,kBAAkBF,OAAOG,kBAAkB,CAACF;IAElD,MAAMC,gBAAgBE,cAAc;AACtC"}
@@ -1 +1 @@
1
- {"version":3,"file":"generateSignedURL.d.ts","sourceRoot":"","sources":["../src/generateSignedURL.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,MAAM,qBAAqB,CAAA;AACtF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAM7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,UAAU,IAAI;IACZ,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAA;IAC/C,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,eAAe,CAAA;IACvC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAID,eAAO,MAAM,2BAA2B,oFAMrC,IAAI,KAAG,cAsDT,CAAA"}
1
+ {"version":3,"file":"generateSignedURL.d.ts","sourceRoot":"","sources":["../src/generateSignedURL.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,MAAM,qBAAqB,CAAA;AACtF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAO7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAIrD,UAAU,IAAI;IACZ,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAA;IAC/C,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,eAAe,CAAA;IACvC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAID,eAAO,MAAM,2BAA2B,oFAMrC,IAAI,KAAG,cAqET,CAAA"}
@@ -1,6 +1,8 @@
1
1
  import { BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob';
2
2
  import { resolveSignedURLKey } from '@payloadcms/plugin-cloud-storage/utilities';
3
3
  import { APIError, Forbidden } from 'payload';
4
+ import { assertClientUploadAccess, assertClientUploadAllowed } from 'payload/internal';
5
+ import { isAzureClientUploadAllowed } from './isClientUploadAllowed.js';
4
6
  const defaultAccess = ({ req })=>!!req.user;
5
7
  export const getGenerateSignedURLHandler = ({ access = defaultAccess, collections, containerName, getStorageClient, useCompositePrefixes = false })=>{
6
8
  return async (req)=>{
@@ -8,10 +10,17 @@ export const getGenerateSignedURLHandler = ({ access = defaultAccess, collection
8
10
  throw new APIError('Unreachable');
9
11
  }
10
12
  const { collectionSlug, docPrefix, filename, mimeType } = await req.json();
13
+ await assertClientUploadAccess({
14
+ collectionSlug,
15
+ req
16
+ });
11
17
  const collectionStorageConfig = collections[collectionSlug];
12
18
  if (!collectionStorageConfig) {
13
19
  throw new APIError(`Collection ${collectionSlug} was not found in Azure storage options`);
14
20
  }
21
+ if (!isAzureClientUploadAllowed(req.payload.collections[collectionSlug]?.config)) {
22
+ throw new APIError('Azure client uploads require allowRestrictedFileTypes.', 400);
23
+ }
15
24
  const collectionPrefix = typeof collectionStorageConfig === 'object' && collectionStorageConfig.prefix || '';
16
25
  if (!await access({
17
26
  collectionSlug,
@@ -19,7 +28,12 @@ export const getGenerateSignedURLHandler = ({ access = defaultAccess, collection
19
28
  })) {
20
29
  throw new Forbidden();
21
30
  }
22
- const { fileKey, sanitizedDocPrefix, sanitizedFilename } = await resolveSignedURLKey({
31
+ assertClientUploadAllowed({
32
+ collection: req.payload.collections[collectionSlug]?.config,
33
+ filename,
34
+ mimeType
35
+ });
36
+ const { clientUploadContext, fileKey, sanitizedDocPrefix, sanitizedFilename } = await resolveSignedURLKey({
23
37
  collectionPrefix,
24
38
  collectionSlug,
25
39
  docPrefix,
@@ -33,10 +47,12 @@ export const getGenerateSignedURLHandler = ({ access = defaultAccess, collection
33
47
  containerName,
34
48
  contentType: mimeType,
35
49
  expiresOn: new Date(Date.now() + 60 * 60 * 1000),
36
- permissions: BlobSASPermissions.parse('w'),
37
- startsOn: new Date()
50
+ permissions: BlobSASPermissions.parse('c'),
51
+ startsOn: new Date(),
52
+ version: '2026-04-06'
38
53
  }, getStorageClient().credential);
39
54
  return Response.json({
55
+ clientUploadContext,
40
56
  docPrefix: sanitizedDocPrefix,
41
57
  filename: sanitizedFilename,
42
58
  url: `${blobClient.url}?${sasToken.toString()}`
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/generateSignedURL.ts"],"sourcesContent":["import type { ContainerClient, StorageSharedKeyCredential } from '@azure/storage-blob'\nimport type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types'\nimport type { PayloadHandler } from 'payload'\n\nimport { BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'\nimport { resolveSignedURLKey } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { APIError, Forbidden } from 'payload'\n\nimport type { AzureStorageOptions } from './index.js'\n\ninterface Args {\n access?: ClientUploadsAccess\n collections: AzureStorageOptions['collections']\n containerName: string\n getStorageClient: () => ContainerClient\n useCompositePrefixes?: boolean\n}\n\nconst defaultAccess: Args['access'] = ({ req }) => !!req.user\n\nexport const getGenerateSignedURLHandler = ({\n access = defaultAccess,\n collections,\n containerName,\n getStorageClient,\n useCompositePrefixes = false,\n}: Args): PayloadHandler => {\n return async (req) => {\n if (!req.json) {\n throw new APIError('Unreachable')\n }\n\n const { collectionSlug, docPrefix, filename, mimeType } = (await req.json()) as {\n collectionSlug: string\n docPrefix?: string\n filename: string\n mimeType: string\n }\n\n const collectionStorageConfig = collections[collectionSlug]\n if (!collectionStorageConfig) {\n throw new APIError(`Collection ${collectionSlug} was not found in Azure storage options`)\n }\n\n const collectionPrefix =\n (typeof collectionStorageConfig === 'object' && collectionStorageConfig.prefix) || ''\n\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden()\n }\n\n const { fileKey, sanitizedDocPrefix, sanitizedFilename } = await resolveSignedURLKey({\n collectionPrefix,\n collectionSlug,\n docPrefix,\n filename,\n req,\n useCompositePrefixes,\n })\n\n const blobClient = getStorageClient().getBlobClient(fileKey)\n\n const sasToken = generateBlobSASQueryParameters(\n {\n blobName: fileKey,\n containerName,\n contentType: mimeType,\n expiresOn: new Date(Date.now() + 60 * 60 * 1000),\n permissions: BlobSASPermissions.parse('w'),\n startsOn: new Date(),\n },\n getStorageClient().credential as StorageSharedKeyCredential,\n )\n\n return Response.json({\n docPrefix: sanitizedDocPrefix,\n filename: sanitizedFilename,\n url: `${blobClient.url}?${sasToken.toString()}`,\n })\n }\n}\n"],"names":["BlobSASPermissions","generateBlobSASQueryParameters","resolveSignedURLKey","APIError","Forbidden","defaultAccess","req","user","getGenerateSignedURLHandler","access","collections","containerName","getStorageClient","useCompositePrefixes","json","collectionSlug","docPrefix","filename","mimeType","collectionStorageConfig","collectionPrefix","prefix","fileKey","sanitizedDocPrefix","sanitizedFilename","blobClient","getBlobClient","sasToken","blobName","contentType","expiresOn","Date","now","permissions","parse","startsOn","credential","Response","url","toString"],"mappings":"AAIA,SAASA,kBAAkB,EAAEC,8BAA8B,QAAQ,sBAAqB;AACxF,SAASC,mBAAmB,QAAQ,6CAA4C;AAChF,SAASC,QAAQ,EAAEC,SAAS,QAAQ,UAAS;AAY7C,MAAMC,gBAAgC,CAAC,EAAEC,GAAG,EAAE,GAAK,CAAC,CAACA,IAAIC,IAAI;AAE7D,OAAO,MAAMC,8BAA8B,CAAC,EAC1CC,SAASJ,aAAa,EACtBK,WAAW,EACXC,aAAa,EACbC,gBAAgB,EAChBC,uBAAuB,KAAK,EACvB;IACL,OAAO,OAAOP;QACZ,IAAI,CAACA,IAAIQ,IAAI,EAAE;YACb,MAAM,IAAIX,SAAS;QACrB;QAEA,MAAM,EAAEY,cAAc,EAAEC,SAAS,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAI,MAAMZ,IAAIQ,IAAI;QAOzE,MAAMK,0BAA0BT,WAAW,CAACK,eAAe;QAC3D,IAAI,CAACI,yBAAyB;YAC5B,MAAM,IAAIhB,SAAS,CAAC,WAAW,EAAEY,eAAe,uCAAuC,CAAC;QAC1F;QAEA,MAAMK,mBACJ,AAAC,OAAOD,4BAA4B,YAAYA,wBAAwBE,MAAM,IAAK;QAErF,IAAI,CAAE,MAAMZ,OAAO;YAAEM;YAAgBT;QAAI,IAAK;YAC5C,MAAM,IAAIF;QACZ;QAEA,MAAM,EAAEkB,OAAO,EAAEC,kBAAkB,EAAEC,iBAAiB,EAAE,GAAG,MAAMtB,oBAAoB;YACnFkB;YACAL;YACAC;YACAC;YACAX;YACAO;QACF;QAEA,MAAMY,aAAab,mBAAmBc,aAAa,CAACJ;QAEpD,MAAMK,WAAW1B,+BACf;YACE2B,UAAUN;YACVX;YACAkB,aAAaX;YACbY,WAAW,IAAIC,KAAKA,KAAKC,GAAG,KAAK,KAAK,KAAK;YAC3CC,aAAajC,mBAAmBkC,KAAK,CAAC;YACtCC,UAAU,IAAIJ;QAChB,GACAnB,mBAAmBwB,UAAU;QAG/B,OAAOC,SAASvB,IAAI,CAAC;YACnBE,WAAWO;YACXN,UAAUO;YACVc,KAAK,GAAGb,WAAWa,GAAG,CAAC,CAAC,EAAEX,SAASY,QAAQ,IAAI;QACjD;IACF;AACF,EAAC"}
1
+ {"version":3,"sources":["../src/generateSignedURL.ts"],"sourcesContent":["import type { ContainerClient, StorageSharedKeyCredential } from '@azure/storage-blob'\nimport type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types'\nimport type { PayloadHandler } from 'payload'\n\nimport { BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'\nimport { resolveSignedURLKey } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { APIError, Forbidden } from 'payload'\nimport { assertClientUploadAccess, assertClientUploadAllowed } from 'payload/internal'\n\nimport type { AzureStorageOptions } from './index.js'\n\nimport { isAzureClientUploadAllowed } from './isClientUploadAllowed.js'\n\ninterface Args {\n access?: ClientUploadsAccess\n collections: AzureStorageOptions['collections']\n containerName: string\n getStorageClient: () => ContainerClient\n useCompositePrefixes?: boolean\n}\n\nconst defaultAccess: Args['access'] = ({ req }) => !!req.user\n\nexport const getGenerateSignedURLHandler = ({\n access = defaultAccess,\n collections,\n containerName,\n getStorageClient,\n useCompositePrefixes = false,\n}: Args): PayloadHandler => {\n return async (req) => {\n if (!req.json) {\n throw new APIError('Unreachable')\n }\n\n const { collectionSlug, docPrefix, filename, mimeType } = (await req.json()) as {\n collectionSlug: string\n docPrefix?: string\n filename: string\n mimeType: string\n }\n\n await assertClientUploadAccess({ collectionSlug, req })\n\n const collectionStorageConfig = collections[collectionSlug]\n if (!collectionStorageConfig) {\n throw new APIError(`Collection ${collectionSlug} was not found in Azure storage options`)\n }\n\n if (!isAzureClientUploadAllowed(req.payload.collections[collectionSlug]?.config)) {\n throw new APIError('Azure client uploads require allowRestrictedFileTypes.', 400)\n }\n\n const collectionPrefix =\n (typeof collectionStorageConfig === 'object' && collectionStorageConfig.prefix) || ''\n\n if (!(await access({ collectionSlug, req }))) {\n throw new Forbidden()\n }\n\n assertClientUploadAllowed({\n collection: req.payload.collections[collectionSlug]?.config,\n filename,\n mimeType,\n })\n\n const { clientUploadContext, fileKey, sanitizedDocPrefix, sanitizedFilename } =\n await resolveSignedURLKey({\n collectionPrefix,\n collectionSlug,\n docPrefix,\n filename,\n req,\n useCompositePrefixes,\n })\n\n const blobClient = getStorageClient().getBlobClient(fileKey)\n\n const sasToken = generateBlobSASQueryParameters(\n {\n blobName: fileKey,\n containerName,\n contentType: mimeType,\n expiresOn: new Date(Date.now() + 60 * 60 * 1000),\n permissions: BlobSASPermissions.parse('c'),\n startsOn: new Date(),\n version: '2026-04-06',\n },\n getStorageClient().credential as StorageSharedKeyCredential,\n )\n\n return Response.json({\n clientUploadContext,\n docPrefix: sanitizedDocPrefix,\n filename: sanitizedFilename,\n url: `${blobClient.url}?${sasToken.toString()}`,\n })\n }\n}\n"],"names":["BlobSASPermissions","generateBlobSASQueryParameters","resolveSignedURLKey","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","isAzureClientUploadAllowed","defaultAccess","req","user","getGenerateSignedURLHandler","access","collections","containerName","getStorageClient","useCompositePrefixes","json","collectionSlug","docPrefix","filename","mimeType","collectionStorageConfig","payload","config","collectionPrefix","prefix","collection","clientUploadContext","fileKey","sanitizedDocPrefix","sanitizedFilename","blobClient","getBlobClient","sasToken","blobName","contentType","expiresOn","Date","now","permissions","parse","startsOn","version","credential","Response","url","toString"],"mappings":"AAIA,SAASA,kBAAkB,EAAEC,8BAA8B,QAAQ,sBAAqB;AACxF,SAASC,mBAAmB,QAAQ,6CAA4C;AAChF,SAASC,QAAQ,EAAEC,SAAS,QAAQ,UAAS;AAC7C,SAASC,wBAAwB,EAAEC,yBAAyB,QAAQ,mBAAkB;AAItF,SAASC,0BAA0B,QAAQ,6BAA4B;AAUvE,MAAMC,gBAAgC,CAAC,EAAEC,GAAG,EAAE,GAAK,CAAC,CAACA,IAAIC,IAAI;AAE7D,OAAO,MAAMC,8BAA8B,CAAC,EAC1CC,SAASJ,aAAa,EACtBK,WAAW,EACXC,aAAa,EACbC,gBAAgB,EAChBC,uBAAuB,KAAK,EACvB;IACL,OAAO,OAAOP;QACZ,IAAI,CAACA,IAAIQ,IAAI,EAAE;YACb,MAAM,IAAId,SAAS;QACrB;QAEA,MAAM,EAAEe,cAAc,EAAEC,SAAS,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAI,MAAMZ,IAAIQ,IAAI;QAOzE,MAAMZ,yBAAyB;YAAEa;YAAgBT;QAAI;QAErD,MAAMa,0BAA0BT,WAAW,CAACK,eAAe;QAC3D,IAAI,CAACI,yBAAyB;YAC5B,MAAM,IAAInB,SAAS,CAAC,WAAW,EAAEe,eAAe,uCAAuC,CAAC;QAC1F;QAEA,IAAI,CAACX,2BAA2BE,IAAIc,OAAO,CAACV,WAAW,CAACK,eAAe,EAAEM,SAAS;YAChF,MAAM,IAAIrB,SAAS,0DAA0D;QAC/E;QAEA,MAAMsB,mBACJ,AAAC,OAAOH,4BAA4B,YAAYA,wBAAwBI,MAAM,IAAK;QAErF,IAAI,CAAE,MAAMd,OAAO;YAAEM;YAAgBT;QAAI,IAAK;YAC5C,MAAM,IAAIL;QACZ;QAEAE,0BAA0B;YACxBqB,YAAYlB,IAAIc,OAAO,CAACV,WAAW,CAACK,eAAe,EAAEM;YACrDJ;YACAC;QACF;QAEA,MAAM,EAAEO,mBAAmB,EAAEC,OAAO,EAAEC,kBAAkB,EAAEC,iBAAiB,EAAE,GAC3E,MAAM7B,oBAAoB;YACxBuB;YACAP;YACAC;YACAC;YACAX;YACAO;QACF;QAEF,MAAMgB,aAAajB,mBAAmBkB,aAAa,CAACJ;QAEpD,MAAMK,WAAWjC,+BACf;YACEkC,UAAUN;YACVf;YACAsB,aAAaf;YACbgB,WAAW,IAAIC,KAAKA,KAAKC,GAAG,KAAK,KAAK,KAAK;YAC3CC,aAAaxC,mBAAmByC,KAAK,CAAC;YACtCC,UAAU,IAAIJ;YACdK,SAAS;QACX,GACA5B,mBAAmB6B,UAAU;QAG/B,OAAOC,SAAS5B,IAAI,CAAC;YACnBW;YACAT,WAAWW;YACXV,UAAUW;YACVe,KAAK,GAAGd,WAAWc,GAAG,CAAC,CAAC,EAAEZ,SAASa,QAAQ,IAAI;QACjD;IACF;AACF,EAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generateSignedURL.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateSignedURL.spec.d.ts","sourceRoot":"","sources":["../src/generateSignedURL.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,65 @@
1
+ import { StorageSharedKeyCredential } from '@azure/storage-blob';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { getGenerateSignedURLHandler } from './generateSignedURL.js';
4
+ const createRequest = (allowRestrictedFileTypes, mimeType, filename = 'reference.png')=>({
5
+ json: ()=>Promise.resolve({
6
+ collectionSlug: 'media',
7
+ filename,
8
+ mimeType
9
+ }),
10
+ payload: {
11
+ collections: {
12
+ media: {
13
+ config: {
14
+ slug: 'media',
15
+ access: {},
16
+ upload: {
17
+ allowRestrictedFileTypes
18
+ }
19
+ }
20
+ }
21
+ },
22
+ db: {
23
+ findOne: ()=>Promise.resolve(null)
24
+ }
25
+ },
26
+ user: {
27
+ id: 'user-id'
28
+ }
29
+ });
30
+ describe('Azure signed upload URLs', ()=>{
31
+ it('should use Payload uploads under secure defaults', async ()=>{
32
+ const getStorageClient = vi.fn();
33
+ const handler = getGenerateSignedURLHandler({
34
+ collections: {
35
+ media: true
36
+ },
37
+ containerName: 'media',
38
+ getStorageClient
39
+ });
40
+ await expect(handler(createRequest(false, 'image/png'))).rejects.toThrow('Azure client uploads require allowRestrictedFileTypes.');
41
+ expect(getStorageClient).not.toHaveBeenCalled();
42
+ });
43
+ it('should generate direct instructions for the explicit opt-out', async ()=>{
44
+ const credential = new StorageSharedKeyCredential('account', Buffer.alloc(32, 'a').toString('base64'));
45
+ const getStorageClient = ()=>({
46
+ credential,
47
+ getBlobClient: (key)=>({
48
+ url: `https://account.blob.core.windows.net/media/${key}`
49
+ })
50
+ });
51
+ const handler = getGenerateSignedURLHandler({
52
+ collections: {
53
+ media: true
54
+ },
55
+ containerName: 'media',
56
+ getStorageClient
57
+ });
58
+ const response = await handler(createRequest(true, 'image/png', 'folder\\nested/reference.png'));
59
+ const result = await response.json();
60
+ expect(result.filename).toBe('reference.png');
61
+ expect(result.url).toMatch(/https:\/\/account\.blob\.core\.windows\.net\/media\/[0-9a-f-]+\/reference\.png\?/);
62
+ });
63
+ });
64
+
65
+ //# sourceMappingURL=generateSignedURL.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generateSignedURL.spec.ts"],"sourcesContent":["import type { ContainerClient } from '@azure/storage-blob'\nimport type { PayloadRequest } from 'payload'\n\nimport { StorageSharedKeyCredential } from '@azure/storage-blob'\nimport { describe, expect, it, vi } from 'vitest'\n\nimport { getGenerateSignedURLHandler } from './generateSignedURL.js'\n\nconst createRequest = (\n allowRestrictedFileTypes: boolean,\n mimeType: unknown,\n filename = 'reference.png',\n): PayloadRequest =>\n ({\n json: () =>\n Promise.resolve({\n collectionSlug: 'media',\n filename,\n mimeType,\n }),\n payload: {\n collections: {\n media: {\n config: {\n slug: 'media',\n access: {},\n upload: { allowRestrictedFileTypes },\n },\n },\n },\n db: { findOne: () => Promise.resolve(null) },\n },\n user: { id: 'user-id' },\n }) as unknown as PayloadRequest\n\ndescribe('Azure signed upload URLs', () => {\n it('should use Payload uploads under secure defaults', async () => {\n const getStorageClient = vi.fn()\n const handler = getGenerateSignedURLHandler({\n collections: { media: true },\n containerName: 'media',\n getStorageClient,\n })\n\n await expect(handler(createRequest(false, 'image/png'))).rejects.toThrow(\n 'Azure client uploads require allowRestrictedFileTypes.',\n )\n expect(getStorageClient).not.toHaveBeenCalled()\n })\n\n it('should generate direct instructions for the explicit opt-out', async () => {\n const credential = new StorageSharedKeyCredential(\n 'account',\n Buffer.alloc(32, 'a').toString('base64'),\n )\n const getStorageClient = () =>\n ({\n credential,\n getBlobClient: (key: string) => ({\n url: `https://account.blob.core.windows.net/media/${key}`,\n }),\n }) as unknown as ContainerClient\n const handler = getGenerateSignedURLHandler({\n collections: { media: true },\n containerName: 'media',\n getStorageClient,\n })\n\n const response = await handler(createRequest(true, 'image/png', 'folder\\\\nested/reference.png'))\n const result = (await response.json()) as { filename: string; url: string }\n\n expect(result.filename).toBe('reference.png')\n expect(result.url).toMatch(\n /https:\\/\\/account\\.blob\\.core\\.windows\\.net\\/media\\/[0-9a-f-]+\\/reference\\.png\\?/,\n )\n })\n})\n"],"names":["StorageSharedKeyCredential","describe","expect","it","vi","getGenerateSignedURLHandler","createRequest","allowRestrictedFileTypes","mimeType","filename","json","Promise","resolve","collectionSlug","payload","collections","media","config","slug","access","upload","db","findOne","user","id","getStorageClient","fn","handler","containerName","rejects","toThrow","not","toHaveBeenCalled","credential","Buffer","alloc","toString","getBlobClient","key","url","response","result","toBe","toMatch"],"mappings":"AAGA,SAASA,0BAA0B,QAAQ,sBAAqB;AAChE,SAASC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAEjD,SAASC,2BAA2B,QAAQ,yBAAwB;AAEpE,MAAMC,gBAAgB,CACpBC,0BACAC,UACAC,WAAW,eAAe,GAEzB,CAAA;QACCC,MAAM,IACJC,QAAQC,OAAO,CAAC;gBACdC,gBAAgB;gBAChBJ;gBACAD;YACF;QACFM,SAAS;YACPC,aAAa;gBACXC,OAAO;oBACLC,QAAQ;wBACNC,MAAM;wBACNC,QAAQ,CAAC;wBACTC,QAAQ;4BAAEb;wBAAyB;oBACrC;gBACF;YACF;YACAc,IAAI;gBAAEC,SAAS,IAAMX,QAAQC,OAAO,CAAC;YAAM;QAC7C;QACAW,MAAM;YAAEC,IAAI;QAAU;IACxB,CAAA;AAEFvB,SAAS,4BAA4B;IACnCE,GAAG,oDAAoD;QACrD,MAAMsB,mBAAmBrB,GAAGsB,EAAE;QAC9B,MAAMC,UAAUtB,4BAA4B;YAC1CU,aAAa;gBAAEC,OAAO;YAAK;YAC3BY,eAAe;YACfH;QACF;QAEA,MAAMvB,OAAOyB,QAAQrB,cAAc,OAAO,eAAeuB,OAAO,CAACC,OAAO,CACtE;QAEF5B,OAAOuB,kBAAkBM,GAAG,CAACC,gBAAgB;IAC/C;IAEA7B,GAAG,gEAAgE;QACjE,MAAM8B,aAAa,IAAIjC,2BACrB,WACAkC,OAAOC,KAAK,CAAC,IAAI,KAAKC,QAAQ,CAAC;QAEjC,MAAMX,mBAAmB,IACtB,CAAA;gBACCQ;gBACAI,eAAe,CAACC,MAAiB,CAAA;wBAC/BC,KAAK,CAAC,4CAA4C,EAAED,KAAK;oBAC3D,CAAA;YACF,CAAA;QACF,MAAMX,UAAUtB,4BAA4B;YAC1CU,aAAa;gBAAEC,OAAO;YAAK;YAC3BY,eAAe;YACfH;QACF;QAEA,MAAMe,WAAW,MAAMb,QAAQrB,cAAc,MAAM,aAAa;QAChE,MAAMmC,SAAU,MAAMD,SAAS9B,IAAI;QAEnCR,OAAOuC,OAAOhC,QAAQ,EAAEiC,IAAI,CAAC;QAC7BxC,OAAOuC,OAAOF,GAAG,EAAEI,OAAO,CACxB;IAEJ;AACF"}