payload 3.80.0-internal.cee0ccf → 3.80.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 (135) hide show
  1. package/dist/auth/endpoints/forgotPassword.d.ts.map +1 -1
  2. package/dist/auth/endpoints/forgotPassword.js +0 -2
  3. package/dist/auth/endpoints/forgotPassword.js.map +1 -1
  4. package/dist/auth/extractJWT.d.ts.map +1 -1
  5. package/dist/auth/extractJWT.js +18 -4
  6. package/dist/auth/extractJWT.js.map +1 -1
  7. package/dist/auth/operations/forgotPassword.d.ts.map +1 -1
  8. package/dist/auth/operations/forgotPassword.js +5 -4
  9. package/dist/auth/operations/forgotPassword.js.map +1 -1
  10. package/dist/auth/operations/me.js +5 -5
  11. package/dist/auth/operations/me.js.map +1 -1
  12. package/dist/auth/sendVerificationEmail.d.ts.map +1 -1
  13. package/dist/auth/sendVerificationEmail.js +5 -4
  14. package/dist/auth/sendVerificationEmail.js.map +1 -1
  15. package/dist/collections/operations/update.js +1 -1
  16. package/dist/collections/operations/update.js.map +1 -1
  17. package/dist/collections/operations/utilities/update.d.ts.map +1 -1
  18. package/dist/collections/operations/utilities/update.js +2 -1
  19. package/dist/collections/operations/utilities/update.js.map +1 -1
  20. package/dist/config/orderable/index.d.ts +2 -2
  21. package/dist/config/orderable/index.d.ts.map +1 -1
  22. package/dist/config/orderable/index.js +60 -34
  23. package/dist/config/orderable/index.js.map +1 -1
  24. package/dist/config/orderable/utils/buildJoinScopeWhere.d.ts +10 -0
  25. package/dist/config/orderable/utils/buildJoinScopeWhere.d.ts.map +1 -0
  26. package/dist/config/orderable/utils/buildJoinScopeWhere.js +43 -0
  27. package/dist/config/orderable/utils/buildJoinScopeWhere.js.map +1 -0
  28. package/dist/config/orderable/utils/getJoinScopeContext.d.ts +16 -0
  29. package/dist/config/orderable/utils/getJoinScopeContext.d.ts.map +1 -0
  30. package/dist/config/orderable/utils/getJoinScopeContext.js +42 -0
  31. package/dist/config/orderable/utils/getJoinScopeContext.js.map +1 -0
  32. package/dist/config/orderable/utils/getJoinScopeWhereFromDocData.d.ts +12 -0
  33. package/dist/config/orderable/utils/getJoinScopeWhereFromDocData.d.ts.map +1 -0
  34. package/dist/config/orderable/utils/getJoinScopeWhereFromDocData.js +18 -0
  35. package/dist/config/orderable/utils/getJoinScopeWhereFromDocData.js.map +1 -0
  36. package/dist/config/orderable/utils/getValueAtPath.d.ts +5 -0
  37. package/dist/config/orderable/utils/getValueAtPath.d.ts.map +1 -0
  38. package/dist/config/orderable/utils/getValueAtPath.js +18 -0
  39. package/dist/config/orderable/utils/getValueAtPath.js.map +1 -0
  40. package/dist/config/orderable/utils/resolvePendingTargetKey.d.ts +13 -0
  41. package/dist/config/orderable/utils/resolvePendingTargetKey.d.ts.map +1 -0
  42. package/dist/config/orderable/utils/resolvePendingTargetKey.js +24 -0
  43. package/dist/config/orderable/utils/resolvePendingTargetKey.js.map +1 -0
  44. package/dist/config/types.d.ts +1 -1
  45. package/dist/config/types.js.map +1 -1
  46. package/dist/database/getLocalizedPaths.d.ts.map +1 -1
  47. package/dist/database/getLocalizedPaths.js +2 -1
  48. package/dist/database/getLocalizedPaths.js.map +1 -1
  49. package/dist/database/queryValidation/validateQueryPaths.js +1 -1
  50. package/dist/database/queryValidation/validateQueryPaths.js.map +1 -1
  51. package/dist/database/queryValidation/validateSearchParams.d.ts.map +1 -1
  52. package/dist/database/queryValidation/validateSearchParams.js +2 -1
  53. package/dist/database/queryValidation/validateSearchParams.js.map +1 -1
  54. package/dist/database/sanitizeJoinQuery.d.ts.map +1 -1
  55. package/dist/database/sanitizeJoinQuery.js +6 -0
  56. package/dist/database/sanitizeJoinQuery.js.map +1 -1
  57. package/dist/database/types.d.ts +0 -1
  58. package/dist/database/types.d.ts.map +1 -1
  59. package/dist/database/types.js.map +1 -1
  60. package/dist/exports/shared.d.ts +1 -0
  61. package/dist/exports/shared.d.ts.map +1 -1
  62. package/dist/exports/shared.js +1 -0
  63. package/dist/exports/shared.js.map +1 -1
  64. package/dist/fields/baseFields/slug/index.d.ts +7 -0
  65. package/dist/fields/baseFields/slug/index.d.ts.map +1 -1
  66. package/dist/fields/baseFields/slug/index.js +2 -2
  67. package/dist/fields/baseFields/slug/index.js.map +1 -1
  68. package/dist/fields/validations.js +1 -1
  69. package/dist/fields/validations.js.map +1 -1
  70. package/dist/fields/validations.spec.js +25 -0
  71. package/dist/fields/validations.spec.js.map +1 -1
  72. package/dist/globals/operations/update.d.ts.map +1 -1
  73. package/dist/globals/operations/update.js +2 -1
  74. package/dist/globals/operations/update.js.map +1 -1
  75. package/dist/index.bundled.d.ts +16 -13
  76. package/dist/index.d.ts +1 -7
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -10
  79. package/dist/index.js.map +1 -1
  80. package/dist/queues/localAPI.d.ts +0 -1
  81. package/dist/queues/localAPI.d.ts.map +1 -1
  82. package/dist/queues/localAPI.js +0 -4
  83. package/dist/queues/localAPI.js.map +1 -1
  84. package/dist/queues/operations/runJobs/index.d.ts +0 -1
  85. package/dist/queues/operations/runJobs/index.d.ts.map +1 -1
  86. package/dist/queues/operations/runJobs/index.js +1 -96
  87. package/dist/queues/operations/runJobs/index.js.map +1 -1
  88. package/dist/queues/utilities/updateJob.d.ts +1 -2
  89. package/dist/queues/utilities/updateJob.d.ts.map +1 -1
  90. package/dist/queues/utilities/updateJob.js +31 -92
  91. package/dist/queues/utilities/updateJob.js.map +1 -1
  92. package/dist/types/constants.d.ts +5 -0
  93. package/dist/types/constants.d.ts.map +1 -1
  94. package/dist/types/constants.js +4 -0
  95. package/dist/types/constants.js.map +1 -1
  96. package/dist/uploads/endpoints/getFile.d.ts.map +1 -1
  97. package/dist/uploads/endpoints/getFile.js +7 -1
  98. package/dist/uploads/endpoints/getFile.js.map +1 -1
  99. package/dist/uploads/endpoints/getFileFromURL.d.ts.map +1 -1
  100. package/dist/uploads/endpoints/getFileFromURL.js +67 -28
  101. package/dist/uploads/endpoints/getFileFromURL.js.map +1 -1
  102. package/dist/uploads/getExternalFile.d.ts.map +1 -1
  103. package/dist/uploads/getExternalFile.js +3 -0
  104. package/dist/uploads/getExternalFile.js.map +1 -1
  105. package/dist/uploads/safeFetch.d.ts +1 -1
  106. package/dist/uploads/safeFetch.d.ts.map +1 -1
  107. package/dist/uploads/safeFetch.js.map +1 -1
  108. package/dist/utilities/addDataAndFileToRequest.d.ts.map +1 -1
  109. package/dist/utilities/addDataAndFileToRequest.js +7 -1
  110. package/dist/utilities/addDataAndFileToRequest.js.map +1 -1
  111. package/dist/utilities/configToJSONSchema.d.ts +7 -3
  112. package/dist/utilities/configToJSONSchema.d.ts.map +1 -1
  113. package/dist/utilities/configToJSONSchema.js +23 -33
  114. package/dist/utilities/configToJSONSchema.js.map +1 -1
  115. package/dist/utilities/configToJSONSchema.spec.js +75 -1
  116. package/dist/utilities/configToJSONSchema.spec.js.map +1 -1
  117. package/dist/utilities/getRequestOrigin.d.ts +10 -0
  118. package/dist/utilities/getRequestOrigin.d.ts.map +1 -0
  119. package/dist/utilities/getRequestOrigin.js +50 -0
  120. package/dist/utilities/getRequestOrigin.js.map +1 -0
  121. package/dist/utilities/getRequestOrigin.spec.js +151 -0
  122. package/dist/utilities/getRequestOrigin.spec.js.map +1 -0
  123. package/dist/utilities/sanitizeUrl.d.ts +7 -0
  124. package/dist/utilities/sanitizeUrl.d.ts.map +1 -0
  125. package/dist/utilities/sanitizeUrl.js +28 -0
  126. package/dist/utilities/sanitizeUrl.js.map +1 -0
  127. package/dist/versions/saveVersion.d.ts +1 -0
  128. package/dist/versions/saveVersion.d.ts.map +1 -1
  129. package/dist/versions/saveVersion.js +16 -66
  130. package/dist/versions/saveVersion.js.map +1 -1
  131. package/dist/versions/updateLatestVersion.d.ts +24 -0
  132. package/dist/versions/updateLatestVersion.d.ts.map +1 -0
  133. package/dist/versions/updateLatestVersion.js +64 -0
  134. package/dist/versions/updateLatestVersion.js.map +1 -0
  135. package/package.json +4 -4
@@ -11,28 +11,14 @@ import { jobAfterRead, jobsCollectionSlug } from '../config/collection.js';
11
11
  * Helper for updating jobs in the most performant way possible.
12
12
  * Handles deciding whether it can used direct db methods or not, and if so,
13
13
  * manually runs the afterRead hook that populates the `taskStatus` property.
14
- */ export async function updateJobs({ id, data, debugID, depth, disableTransaction, limit: limitArg, req, returning, sort, where: whereArg }) {
14
+ */ export async function updateJobs({ id, data, depth, disableTransaction, limit: limitArg, req, returning, sort, where: whereArg }) {
15
15
  const limit = id ? 1 : limitArg;
16
16
  const where = id ? {
17
17
  id: {
18
18
  equals: id
19
19
  }
20
20
  } : whereArg;
21
- const prefix = debugID ? `[${debugID}] updateJobs` : null;
22
- if (prefix) {
23
- console.log(`${prefix} - enter`, {
24
- id,
25
- depth,
26
- disableTransaction,
27
- limit,
28
- returning,
29
- sort
30
- });
31
- }
32
21
  if (depth || req.payload.config?.jobs?.runHooks) {
33
- if (prefix) {
34
- console.log(`${prefix} - using payload.update (depth or runHooks)`);
35
- }
36
22
  const result = await req.payload.update({
37
23
  id,
38
24
  collection: jobsCollectionSlug,
@@ -43,91 +29,44 @@ import { jobAfterRead, jobsCollectionSlug } from '../config/collection.js';
43
29
  req,
44
30
  where
45
31
  });
46
- if (prefix) {
47
- console.log(`${prefix} - payload.update done`, {
48
- docCount: result?.docs?.length ?? 0
49
- });
50
- }
51
32
  if (returning === false || !result) {
52
33
  return null;
53
34
  }
54
35
  return result.docs;
55
36
  }
56
- if (prefix) {
57
- console.log(`${prefix} - using direct db path`);
37
+ const jobReq = {
38
+ transactionID: req.payload.db.name !== 'mongoose' ? await req.payload.db.beginTransaction() : undefined
39
+ };
40
+ if (typeof data.updatedAt === 'undefined') {
41
+ // Ensure updatedAt date is always updated
42
+ data.updatedAt = new Date().toISOString();
58
43
  }
59
- const UPDATE_JOBS_TIMEOUT_MS = 60_000;
60
- const doDirectDBUpdate = async ()=>{
61
- const txStart = Date.now();
62
- const jobReq = {
63
- transactionID: req.payload.db.name !== 'mongoose' ? await req.payload.db.beginTransaction() : undefined
64
- };
65
- if (prefix) {
66
- console.log(`${prefix} - beginTransaction took ${Date.now() - txStart}ms, txID: ${jobReq.transactionID}`);
67
- }
68
- if (typeof data.updatedAt === 'undefined') {
69
- data.updatedAt = new Date().toISOString();
70
- }
71
- const args = id ? {
72
- id,
73
- data,
74
- debugID,
75
- req: jobReq,
76
- returning
77
- } : {
78
- data,
79
- debugID,
80
- limit,
81
- req: jobReq,
82
- returning,
83
- sort,
84
- where: where
85
- };
86
- const dbStart = Date.now();
87
- if (prefix) {
88
- console.log(`${prefix} - calling db.updateJobs`);
89
- }
90
- const updatedJobs = await req.payload.db.updateJobs(args);
91
- if (prefix) {
92
- console.log(`${prefix} - db.updateJobs took ${Date.now() - dbStart}ms, returned ${updatedJobs?.length ?? 0} jobs`);
93
- }
94
- if (req.payload.db.name !== 'mongoose' && jobReq.transactionID) {
95
- const commitStart = Date.now();
96
- await req.payload.db.commitTransaction(jobReq.transactionID);
97
- if (prefix) {
98
- console.log(`${prefix} - commitTransaction took ${Date.now() - commitStart}ms`);
99
- }
100
- }
101
- if (prefix) {
102
- console.log(`${prefix} - total elapsed ${Date.now() - txStart}ms`);
103
- }
104
- if (returning === false || !updatedJobs?.length) {
105
- return null;
106
- }
107
- return updatedJobs.map((updatedJob)=>{
108
- return jobAfterRead({
109
- config: req.payload.config,
110
- doc: updatedJob
111
- });
112
- });
44
+ const args = id ? {
45
+ id,
46
+ data,
47
+ req: jobReq,
48
+ returning
49
+ } : {
50
+ data,
51
+ limit,
52
+ req: jobReq,
53
+ returning,
54
+ sort,
55
+ where: where
113
56
  };
114
- let timeoutId;
115
- try {
116
- const result = await Promise.race([
117
- doDirectDBUpdate(),
118
- new Promise((_, reject)=>{
119
- timeoutId = setTimeout(()=>{
120
- reject(new Error(`[${debugID ?? 'unknown'}] updateJobs timed out after ${UPDATE_JOBS_TIMEOUT_MS}ms`));
121
- }, UPDATE_JOBS_TIMEOUT_MS);
122
- })
123
- ]);
124
- clearTimeout(timeoutId);
125
- return result;
126
- } catch (err) {
127
- clearTimeout(timeoutId);
128
- console.error(`[${debugID ?? 'unknown'}] updateJobs failed/timed out:`, err instanceof Error ? err.message : String(err));
129
- throw err;
57
+ const updatedJobs = await req.payload.db.updateJobs(args);
58
+ if (req.payload.db.name !== 'mongoose' && jobReq.transactionID) {
59
+ await req.payload.db.commitTransaction(jobReq.transactionID);
60
+ }
61
+ if (returning === false || !updatedJobs?.length) {
62
+ return null;
130
63
  }
64
+ return updatedJobs.map((updatedJob)=>{
65
+ return jobAfterRead({
66
+ config: req.payload.config,
67
+ doc: updatedJob
68
+ });
69
+ });
131
70
  }
132
71
 
133
72
  //# sourceMappingURL=updateJob.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/queues/utilities/updateJob.ts"],"sourcesContent":["import type { ManyOptions } from '../../collections/operations/local/update.js'\nimport type { UpdateJobsArgs } from '../../database/types.js'\nimport type { Job } from '../../index.js'\nimport type { PayloadRequest, Sort, Where } from '../../types/index.js'\n\nimport { jobAfterRead, jobsCollectionSlug } from '../config/collection.js'\n\ntype BaseArgs = {\n data: Partial<Job>\n debugID?: string\n depth?: number\n disableTransaction?: boolean\n limit?: number\n req: PayloadRequest\n returning?: boolean\n}\n\ntype ArgsByID = {\n id: number | string\n limit?: never\n sort?: never\n where?: never\n}\n\ntype ArgsWhere = {\n id?: never\n limit?: number\n sort?: Sort\n where: Where\n}\n\ntype RunJobsArgs = (ArgsByID | ArgsWhere) & BaseArgs\n\n/**\n * Convenience method for updateJobs by id\n */\nexport async function updateJob(args: ArgsByID & BaseArgs) {\n const result = await updateJobs(args)\n if (result) {\n return result[0]\n }\n}\n\n/**\n * Helper for updating jobs in the most performant way possible.\n * Handles deciding whether it can used direct db methods or not, and if so,\n * manually runs the afterRead hook that populates the `taskStatus` property.\n */\nexport async function updateJobs({\n id,\n data,\n debugID,\n depth,\n disableTransaction,\n limit: limitArg,\n req,\n returning,\n sort,\n where: whereArg,\n}: RunJobsArgs): Promise<Job[] | null> {\n const limit = id ? 1 : limitArg\n const where = id ? { id: { equals: id } } : whereArg\n const prefix = debugID ? `[${debugID}] updateJobs` : null\n\n if (prefix) {\n console.log(`${prefix} - enter`, { id, depth, disableTransaction, limit, returning, sort })\n }\n\n if (depth || req.payload.config?.jobs?.runHooks) {\n if (prefix) {\n console.log(`${prefix} - using payload.update (depth or runHooks)`)\n }\n const result = await req.payload.update({\n id,\n collection: jobsCollectionSlug,\n data,\n depth,\n disableTransaction,\n limit,\n req,\n where,\n } as ManyOptions<any, any>)\n if (prefix) {\n console.log(`${prefix} - payload.update done`, { docCount: result?.docs?.length ?? 0 })\n }\n if (returning === false || !result) {\n return null\n }\n return result.docs as Job[]\n }\n\n if (prefix) {\n console.log(`${prefix} - using direct db path`)\n }\n\n const UPDATE_JOBS_TIMEOUT_MS = 60_000\n\n const doDirectDBUpdate = async (): Promise<Job[] | null> => {\n const txStart = Date.now()\n const jobReq = {\n transactionID:\n req.payload.db.name !== 'mongoose'\n ? ((await req.payload.db.beginTransaction()) as string)\n : undefined,\n }\n if (prefix) {\n console.log(\n `${prefix} - beginTransaction took ${Date.now() - txStart}ms, txID: ${jobReq.transactionID}`,\n )\n }\n\n if (typeof data.updatedAt === 'undefined') {\n data.updatedAt = new Date().toISOString()\n }\n\n const args: UpdateJobsArgs = id\n ? {\n id,\n data,\n debugID,\n req: jobReq,\n returning,\n }\n : {\n data,\n debugID,\n limit,\n req: jobReq,\n returning,\n sort,\n where: where as Where,\n }\n\n const dbStart = Date.now()\n if (prefix) {\n console.log(`${prefix} - calling db.updateJobs`)\n }\n const updatedJobs: Job[] | null = await req.payload.db.updateJobs(args)\n if (prefix) {\n console.log(\n `${prefix} - db.updateJobs took ${Date.now() - dbStart}ms, returned ${updatedJobs?.length ?? 0} jobs`,\n )\n }\n\n if (req.payload.db.name !== 'mongoose' && jobReq.transactionID) {\n const commitStart = Date.now()\n await req.payload.db.commitTransaction(jobReq.transactionID)\n if (prefix) {\n console.log(`${prefix} - commitTransaction took ${Date.now() - commitStart}ms`)\n }\n }\n\n if (prefix) {\n console.log(`${prefix} - total elapsed ${Date.now() - txStart}ms`)\n }\n\n if (returning === false || !updatedJobs?.length) {\n return null\n }\n\n return updatedJobs.map((updatedJob) => {\n return jobAfterRead({\n config: req.payload.config,\n doc: updatedJob,\n })\n })\n }\n\n let timeoutId: ReturnType<typeof setTimeout>\n try {\n const result = await Promise.race([\n doDirectDBUpdate(),\n new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(\n new Error(\n `[${debugID ?? 'unknown'}] updateJobs timed out after ${UPDATE_JOBS_TIMEOUT_MS}ms`,\n ),\n )\n }, UPDATE_JOBS_TIMEOUT_MS)\n }),\n ])\n clearTimeout(timeoutId!)\n return result\n } catch (err) {\n clearTimeout(timeoutId!)\n console.error(\n `[${debugID ?? 'unknown'}] updateJobs failed/timed out:`,\n err instanceof Error ? err.message : String(err),\n )\n throw err\n }\n}\n"],"names":["jobAfterRead","jobsCollectionSlug","updateJob","args","result","updateJobs","id","data","debugID","depth","disableTransaction","limit","limitArg","req","returning","sort","where","whereArg","equals","prefix","console","log","payload","config","jobs","runHooks","update","collection","docCount","docs","length","UPDATE_JOBS_TIMEOUT_MS","doDirectDBUpdate","txStart","Date","now","jobReq","transactionID","db","name","beginTransaction","undefined","updatedAt","toISOString","dbStart","updatedJobs","commitStart","commitTransaction","map","updatedJob","doc","timeoutId","Promise","race","_","reject","setTimeout","Error","clearTimeout","err","error","message","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/queues/utilities/updateJob.ts"],"sourcesContent":["import type { ManyOptions } from '../../collections/operations/local/update.js'\nimport type { UpdateJobsArgs } from '../../database/types.js'\nimport type { Job } from '../../index.js'\nimport type { PayloadRequest, Sort, Where } from '../../types/index.js'\n\nimport { jobAfterRead, jobsCollectionSlug } from '../config/collection.js'\n\ntype BaseArgs = {\n data: Partial<Job>\n depth?: number\n disableTransaction?: boolean\n limit?: number\n req: PayloadRequest\n returning?: boolean\n}\n\ntype ArgsByID = {\n id: number | string\n limit?: never\n sort?: never\n where?: never\n}\n\ntype ArgsWhere = {\n id?: never\n limit?: number\n sort?: Sort\n where: Where\n}\n\ntype RunJobsArgs = (ArgsByID | ArgsWhere) & BaseArgs\n\n/**\n * Convenience method for updateJobs by id\n */\nexport async function updateJob(args: ArgsByID & BaseArgs) {\n const result = await updateJobs(args)\n if (result) {\n return result[0]\n }\n}\n\n/**\n * Helper for updating jobs in the most performant way possible.\n * Handles deciding whether it can used direct db methods or not, and if so,\n * manually runs the afterRead hook that populates the `taskStatus` property.\n */\nexport async function updateJobs({\n id,\n data,\n depth,\n disableTransaction,\n limit: limitArg,\n req,\n returning,\n sort,\n where: whereArg,\n}: RunJobsArgs): Promise<Job[] | null> {\n const limit = id ? 1 : limitArg\n const where = id ? { id: { equals: id } } : whereArg\n\n if (depth || req.payload.config?.jobs?.runHooks) {\n const result = await req.payload.update({\n id,\n collection: jobsCollectionSlug,\n data,\n depth,\n disableTransaction,\n limit,\n req,\n where,\n } as ManyOptions<any, any>)\n if (returning === false || !result) {\n return null\n }\n return result.docs as Job[]\n }\n\n const jobReq = {\n transactionID:\n req.payload.db.name !== 'mongoose'\n ? ((await req.payload.db.beginTransaction()) as string)\n : undefined,\n }\n\n if (typeof data.updatedAt === 'undefined') {\n // Ensure updatedAt date is always updated\n data.updatedAt = new Date().toISOString()\n }\n\n const args: UpdateJobsArgs = id\n ? {\n id,\n data,\n req: jobReq,\n returning,\n }\n : {\n data,\n limit,\n req: jobReq,\n returning,\n sort,\n where: where as Where,\n }\n\n const updatedJobs: Job[] | null = await req.payload.db.updateJobs(args)\n\n if (req.payload.db.name !== 'mongoose' && jobReq.transactionID) {\n await req.payload.db.commitTransaction(jobReq.transactionID)\n }\n\n if (returning === false || !updatedJobs?.length) {\n return null\n }\n\n return updatedJobs.map((updatedJob) => {\n return jobAfterRead({\n config: req.payload.config,\n doc: updatedJob,\n })\n })\n}\n"],"names":["jobAfterRead","jobsCollectionSlug","updateJob","args","result","updateJobs","id","data","depth","disableTransaction","limit","limitArg","req","returning","sort","where","whereArg","equals","payload","config","jobs","runHooks","update","collection","docs","jobReq","transactionID","db","name","beginTransaction","undefined","updatedAt","Date","toISOString","updatedJobs","commitTransaction","length","map","updatedJob","doc"],"mappings":"AAKA,SAASA,YAAY,EAAEC,kBAAkB,QAAQ,0BAAyB;AA2B1E;;CAEC,GACD,OAAO,eAAeC,UAAUC,IAAyB;IACvD,MAAMC,SAAS,MAAMC,WAAWF;IAChC,IAAIC,QAAQ;QACV,OAAOA,MAAM,CAAC,EAAE;IAClB;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,WAAW,EAC/BC,EAAE,EACFC,IAAI,EACJC,KAAK,EACLC,kBAAkB,EAClBC,OAAOC,QAAQ,EACfC,GAAG,EACHC,SAAS,EACTC,IAAI,EACJC,OAAOC,QAAQ,EACH;IACZ,MAAMN,QAAQJ,KAAK,IAAIK;IACvB,MAAMI,QAAQT,KAAK;QAAEA,IAAI;YAAEW,QAAQX;QAAG;IAAE,IAAIU;IAE5C,IAAIR,SAASI,IAAIM,OAAO,CAACC,MAAM,EAAEC,MAAMC,UAAU;QAC/C,MAAMjB,SAAS,MAAMQ,IAAIM,OAAO,CAACI,MAAM,CAAC;YACtChB;YACAiB,YAAYtB;YACZM;YACAC;YACAC;YACAC;YACAE;YACAG;QACF;QACA,IAAIF,cAAc,SAAS,CAACT,QAAQ;YAClC,OAAO;QACT;QACA,OAAOA,OAAOoB,IAAI;IACpB;IAEA,MAAMC,SAAS;QACbC,eACEd,IAAIM,OAAO,CAACS,EAAE,CAACC,IAAI,KAAK,aAClB,MAAMhB,IAAIM,OAAO,CAACS,EAAE,CAACE,gBAAgB,KACvCC;IACR;IAEA,IAAI,OAAOvB,KAAKwB,SAAS,KAAK,aAAa;QACzC,0CAA0C;QAC1CxB,KAAKwB,SAAS,GAAG,IAAIC,OAAOC,WAAW;IACzC;IAEA,MAAM9B,OAAuBG,KACzB;QACEA;QACAC;QACAK,KAAKa;QACLZ;IACF,IACA;QACEN;QACAG;QACAE,KAAKa;QACLZ;QACAC;QACAC,OAAOA;IACT;IAEJ,MAAMmB,cAA4B,MAAMtB,IAAIM,OAAO,CAACS,EAAE,CAACtB,UAAU,CAACF;IAElE,IAAIS,IAAIM,OAAO,CAACS,EAAE,CAACC,IAAI,KAAK,cAAcH,OAAOC,aAAa,EAAE;QAC9D,MAAMd,IAAIM,OAAO,CAACS,EAAE,CAACQ,iBAAiB,CAACV,OAAOC,aAAa;IAC7D;IAEA,IAAIb,cAAc,SAAS,CAACqB,aAAaE,QAAQ;QAC/C,OAAO;IACT;IAEA,OAAOF,YAAYG,GAAG,CAAC,CAACC;QACtB,OAAOtC,aAAa;YAClBmB,QAAQP,IAAIM,OAAO,CAACC,MAAM;YAC1BoB,KAAKD;QACP;IACF;AACF"}
@@ -1,4 +1,9 @@
1
1
  export declare const validOperators: readonly ["equals", "contains", "not_equals", "in", "all", "not_in", "exists", "greater_than", "greater_than_equal", "less_than", "less_than_equal", "like", "not_like", "within", "intersects", "near"];
2
2
  export type Operator = (typeof validOperators)[number];
3
3
  export declare const validOperatorSet: Set<"all" | "contains" | "equals" | "exists" | "intersects" | "near" | "within" | "not_equals" | "in" | "not_in" | "greater_than" | "greater_than_equal" | "less_than" | "less_than_equal" | "like" | "not_like">;
4
+ /**
5
+ * Matches a dot-separated path where each segment is a word character (a-zA-Z0-9_).
6
+ * Used to validate field paths before they are processed by query builders.
7
+ */
8
+ export declare const SAFE_FIELD_PATH_REGEX: RegExp;
4
9
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/types/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,0MAiBjB,CAAA;AAEV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD,eAAO,MAAM,gBAAgB,mNAAoC,CAAA"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/types/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,0MAiBjB,CAAA;AAEV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD,eAAO,MAAM,gBAAgB,mNAAoC,CAAA;AAEjE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAoB,CAAA"}
@@ -17,5 +17,9 @@ export const validOperators = [
17
17
  'near'
18
18
  ];
19
19
  export const validOperatorSet = new Set(validOperators);
20
+ /**
21
+ * Matches a dot-separated path where each segment is a word character (a-zA-Z0-9_).
22
+ * Used to validate field paths before they are processed by query builders.
23
+ */ export const SAFE_FIELD_PATH_REGEX = /^\w+(?:\.\w+)*$/;
20
24
 
21
25
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/constants.ts"],"sourcesContent":["export const validOperators = [\n 'equals',\n 'contains',\n 'not_equals',\n 'in',\n 'all',\n 'not_in',\n 'exists',\n 'greater_than',\n 'greater_than_equal',\n 'less_than',\n 'less_than_equal',\n 'like',\n 'not_like',\n 'within',\n 'intersects',\n 'near',\n] as const\n\nexport type Operator = (typeof validOperators)[number]\n\nexport const validOperatorSet = new Set<Operator>(validOperators)\n"],"names":["validOperators","validOperatorSet","Set"],"mappings":"AAAA,OAAO,MAAMA,iBAAiB;IAC5B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV,OAAO,MAAMC,mBAAmB,IAAIC,IAAcF,gBAAe"}
1
+ {"version":3,"sources":["../../src/types/constants.ts"],"sourcesContent":["export const validOperators = [\n 'equals',\n 'contains',\n 'not_equals',\n 'in',\n 'all',\n 'not_in',\n 'exists',\n 'greater_than',\n 'greater_than_equal',\n 'less_than',\n 'less_than_equal',\n 'like',\n 'not_like',\n 'within',\n 'intersects',\n 'near',\n] as const\n\nexport type Operator = (typeof validOperators)[number]\n\nexport const validOperatorSet = new Set<Operator>(validOperators)\n\n/**\n * Matches a dot-separated path where each segment is a word character (a-zA-Z0-9_).\n * Used to validate field paths before they are processed by query builders.\n */\nexport const SAFE_FIELD_PATH_REGEX = /^\\w+(?:\\.\\w+)*$/\n"],"names":["validOperators","validOperatorSet","Set","SAFE_FIELD_PATH_REGEX"],"mappings":"AAAA,OAAO,MAAMA,iBAAiB;IAC5B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV,OAAO,MAAMC,mBAAmB,IAAIC,IAAcF,gBAAe;AAEjE;;;CAGC,GACD,OAAO,MAAMG,wBAAwB,kBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"getFile.d.ts","sourceRoot":"","sources":["../../../src/uploads/endpoints/getFile.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAU3D,eAAO,MAAM,cAAc,EAAE,cAkJ5B,CAAA"}
1
+ {"version":3,"file":"getFile.d.ts","sourceRoot":"","sources":["../../../src/uploads/endpoints/getFile.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAU3D,eAAO,MAAM,cAAc,EAAE,cA0J5B,CAAA"}
@@ -43,8 +43,14 @@ export const getFileHandler = async (req)=>{
43
43
  return customResponse;
44
44
  }
45
45
  }
46
+ // Local filesystem fallback — cloud storage handlers return a Response above
47
+ // and have their own filename validation via sanitizeFilename.
46
48
  const fileDir = collection.config.upload?.staticDir || collection.config.slug;
47
- const filePath = path.resolve(`${fileDir}/${filename}`);
49
+ const resolvedDir = path.resolve(fileDir);
50
+ const filePath = path.resolve(resolvedDir, filename);
51
+ if (!filePath.startsWith(resolvedDir + path.sep)) {
52
+ throw new APIError('Invalid filename.', httpStatus.BAD_REQUEST);
53
+ }
48
54
  let stats;
49
55
  try {
50
56
  stats = await fsPromises.stat(filePath);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/uploads/endpoints/getFile.ts"],"sourcesContent":["import type { Stats } from 'fs'\n\nimport { fileTypeFromFile } from 'file-type'\nimport fsPromises from 'fs/promises'\nimport { status as httpStatus } from 'http-status'\nimport path from 'path'\n\nimport type { PayloadHandler } from '../../config/types.js'\n\nimport { APIError } from '../../errors/APIError.js'\nimport { checkFileAccess } from '../../uploads/checkFileAccess.js'\nimport { streamFile } from '../../uploads/fetchAPI-stream-file/index.js'\nimport { getFileTypeFallback } from '../../uploads/getFileTypeFallback.js'\nimport { parseRangeHeader } from '../../uploads/parseRangeHeader.js'\nimport { getRequestCollection } from '../../utilities/getRequestEntity.js'\nimport { headersWithCors } from '../../utilities/headersWithCors.js'\n\nexport const getFileHandler: PayloadHandler = async (req) => {\n const collection = getRequestCollection(req)\n\n const filename = req.routeParams?.filename as string\n\n if (!collection.config.upload) {\n throw new APIError(\n `This collection is not an upload collection: ${collection.config.slug}`,\n httpStatus.BAD_REQUEST,\n )\n }\n\n const accessResult = (await checkFileAccess({\n collection,\n filename,\n req,\n }))!\n\n if (accessResult instanceof Response) {\n return accessResult\n }\n\n if (collection.config.upload.handlers?.length) {\n let customResponse: null | Response | void = null\n const headers = new Headers()\n\n for (const handler of collection.config.upload.handlers) {\n customResponse = await handler(req, {\n doc: accessResult,\n headers,\n params: {\n collection: collection.config.slug,\n filename,\n },\n })\n if (customResponse && customResponse instanceof Response) {\n break\n }\n }\n\n if (customResponse instanceof Response) {\n return customResponse\n }\n }\n\n const fileDir = collection.config.upload?.staticDir || collection.config.slug\n const filePath = path.resolve(`${fileDir}/${filename}`)\n let stats: Stats\n\n try {\n stats = await fsPromises.stat(filePath)\n } catch (err) {\n if ((err as { code?: string }).code === 'ENOENT') {\n req.payload.logger.error(\n `File ${filename} for collection ${collection.config.slug} is missing on the disk. Expected path: ${filePath}`,\n )\n\n // Omit going to the routeError handler by returning response instead of\n // throwing an error to cut down log noise. The response still matches what you get with APIError to not leak details to the user.\n return Response.json(\n {\n errors: [\n {\n message: 'Something went wrong.',\n },\n ],\n },\n {\n headers: headersWithCors({\n headers: new Headers(),\n req,\n }),\n status: 500,\n },\n )\n }\n\n throw err\n }\n\n const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath)\n let mimeType = fileTypeResult.mime\n\n if (filePath.endsWith('.svg') && fileTypeResult.mime === 'application/xml') {\n mimeType = 'image/svg+xml'\n }\n\n // Parse Range header for byte range requests\n const rangeHeader = req.headers.get('range')\n const rangeResult = parseRangeHeader({\n fileSize: stats.size,\n rangeHeader,\n })\n\n if (rangeResult.type === 'invalid') {\n let headers = new Headers()\n headers.set('Content-Range', `bytes */${stats.size}`)\n headers = collection.config.upload?.modifyResponseHeaders\n ? collection.config.upload.modifyResponseHeaders({ headers }) || headers\n : headers\n\n return new Response(null, {\n headers: headersWithCors({\n headers,\n req,\n }),\n status: httpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n })\n }\n\n let headers = new Headers()\n headers.set('Content-Type', mimeType)\n headers.set('Accept-Ranges', 'bytes')\n\n if (mimeType === 'image/svg+xml') {\n headers.set('Content-Security-Policy', \"script-src 'none'\")\n }\n\n let data: ReadableStream\n let status: number\n const isPartial = rangeResult.type === 'partial'\n const range = rangeResult.range\n\n if (isPartial && range) {\n const contentLength = range.end - range.start + 1\n headers.set('Content-Length', String(contentLength))\n headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`)\n data = streamFile({ filePath, options: { end: range.end, start: range.start } })\n status = httpStatus.PARTIAL_CONTENT\n } else {\n headers.set('Content-Length', String(stats.size))\n data = streamFile({ filePath })\n status = httpStatus.OK\n }\n\n headers = collection.config.upload?.modifyResponseHeaders\n ? collection.config.upload.modifyResponseHeaders({ headers }) || headers\n : headers\n\n return new Response(data, {\n headers: headersWithCors({\n headers,\n req,\n }),\n status,\n })\n}\n"],"names":["fileTypeFromFile","fsPromises","status","httpStatus","path","APIError","checkFileAccess","streamFile","getFileTypeFallback","parseRangeHeader","getRequestCollection","headersWithCors","getFileHandler","req","collection","filename","routeParams","config","upload","slug","BAD_REQUEST","accessResult","Response","handlers","length","customResponse","headers","Headers","handler","doc","params","fileDir","staticDir","filePath","resolve","stats","stat","err","code","payload","logger","error","json","errors","message","fileTypeResult","mimeType","mime","endsWith","rangeHeader","get","rangeResult","fileSize","size","type","set","modifyResponseHeaders","REQUESTED_RANGE_NOT_SATISFIABLE","data","isPartial","range","contentLength","end","start","String","options","PARTIAL_CONTENT","OK"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/uploads/endpoints/getFile.ts"],"sourcesContent":["import type { Stats } from 'fs'\n\nimport { fileTypeFromFile } from 'file-type'\nimport fsPromises from 'fs/promises'\nimport { status as httpStatus } from 'http-status'\nimport path from 'path'\n\nimport type { PayloadHandler } from '../../config/types.js'\n\nimport { APIError } from '../../errors/APIError.js'\nimport { checkFileAccess } from '../../uploads/checkFileAccess.js'\nimport { streamFile } from '../../uploads/fetchAPI-stream-file/index.js'\nimport { getFileTypeFallback } from '../../uploads/getFileTypeFallback.js'\nimport { parseRangeHeader } from '../../uploads/parseRangeHeader.js'\nimport { getRequestCollection } from '../../utilities/getRequestEntity.js'\nimport { headersWithCors } from '../../utilities/headersWithCors.js'\n\nexport const getFileHandler: PayloadHandler = async (req) => {\n const collection = getRequestCollection(req)\n\n const filename = req.routeParams?.filename as string\n\n if (!collection.config.upload) {\n throw new APIError(\n `This collection is not an upload collection: ${collection.config.slug}`,\n httpStatus.BAD_REQUEST,\n )\n }\n\n const accessResult = (await checkFileAccess({\n collection,\n filename,\n req,\n }))!\n\n if (accessResult instanceof Response) {\n return accessResult\n }\n\n if (collection.config.upload.handlers?.length) {\n let customResponse: null | Response | void = null\n const headers = new Headers()\n\n for (const handler of collection.config.upload.handlers) {\n customResponse = await handler(req, {\n doc: accessResult,\n headers,\n params: {\n collection: collection.config.slug,\n filename,\n },\n })\n if (customResponse && customResponse instanceof Response) {\n break\n }\n }\n\n if (customResponse instanceof Response) {\n return customResponse\n }\n }\n\n // Local filesystem fallback — cloud storage handlers return a Response above\n // and have their own filename validation via sanitizeFilename.\n const fileDir = collection.config.upload?.staticDir || collection.config.slug\n const resolvedDir = path.resolve(fileDir)\n const filePath = path.resolve(resolvedDir, filename)\n\n if (!filePath.startsWith(resolvedDir + path.sep)) {\n throw new APIError('Invalid filename.', httpStatus.BAD_REQUEST)\n }\n\n let stats: Stats\n\n try {\n stats = await fsPromises.stat(filePath)\n } catch (err) {\n if ((err as { code?: string }).code === 'ENOENT') {\n req.payload.logger.error(\n `File ${filename} for collection ${collection.config.slug} is missing on the disk. Expected path: ${filePath}`,\n )\n\n // Omit going to the routeError handler by returning response instead of\n // throwing an error to cut down log noise. The response still matches what you get with APIError to not leak details to the user.\n return Response.json(\n {\n errors: [\n {\n message: 'Something went wrong.',\n },\n ],\n },\n {\n headers: headersWithCors({\n headers: new Headers(),\n req,\n }),\n status: 500,\n },\n )\n }\n\n throw err\n }\n\n const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath)\n let mimeType = fileTypeResult.mime\n\n if (filePath.endsWith('.svg') && fileTypeResult.mime === 'application/xml') {\n mimeType = 'image/svg+xml'\n }\n\n // Parse Range header for byte range requests\n const rangeHeader = req.headers.get('range')\n const rangeResult = parseRangeHeader({\n fileSize: stats.size,\n rangeHeader,\n })\n\n if (rangeResult.type === 'invalid') {\n let headers = new Headers()\n headers.set('Content-Range', `bytes */${stats.size}`)\n headers = collection.config.upload?.modifyResponseHeaders\n ? collection.config.upload.modifyResponseHeaders({ headers }) || headers\n : headers\n\n return new Response(null, {\n headers: headersWithCors({\n headers,\n req,\n }),\n status: httpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n })\n }\n\n let headers = new Headers()\n headers.set('Content-Type', mimeType)\n headers.set('Accept-Ranges', 'bytes')\n\n if (mimeType === 'image/svg+xml') {\n headers.set('Content-Security-Policy', \"script-src 'none'\")\n }\n\n let data: ReadableStream\n let status: number\n const isPartial = rangeResult.type === 'partial'\n const range = rangeResult.range\n\n if (isPartial && range) {\n const contentLength = range.end - range.start + 1\n headers.set('Content-Length', String(contentLength))\n headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`)\n data = streamFile({ filePath, options: { end: range.end, start: range.start } })\n status = httpStatus.PARTIAL_CONTENT\n } else {\n headers.set('Content-Length', String(stats.size))\n data = streamFile({ filePath })\n status = httpStatus.OK\n }\n\n headers = collection.config.upload?.modifyResponseHeaders\n ? collection.config.upload.modifyResponseHeaders({ headers }) || headers\n : headers\n\n return new Response(data, {\n headers: headersWithCors({\n headers,\n req,\n }),\n status,\n })\n}\n"],"names":["fileTypeFromFile","fsPromises","status","httpStatus","path","APIError","checkFileAccess","streamFile","getFileTypeFallback","parseRangeHeader","getRequestCollection","headersWithCors","getFileHandler","req","collection","filename","routeParams","config","upload","slug","BAD_REQUEST","accessResult","Response","handlers","length","customResponse","headers","Headers","handler","doc","params","fileDir","staticDir","resolvedDir","resolve","filePath","startsWith","sep","stats","stat","err","code","payload","logger","error","json","errors","message","fileTypeResult","mimeType","mime","endsWith","rangeHeader","get","rangeResult","fileSize","size","type","set","modifyResponseHeaders","REQUESTED_RANGE_NOT_SATISFIABLE","data","isPartial","range","contentLength","end","start","String","options","PARTIAL_CONTENT","OK"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"getFileFromURL.d.ts","sourceRoot":"","sources":["../../../src/uploads/endpoints/getFileFromURL.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAc3D,eAAO,MAAM,qBAAqB,EAAE,cAsEnC,CAAA"}
1
+ {"version":3,"file":"getFileFromURL.d.ts","sourceRoot":"","sources":["../../../src/uploads/endpoints/getFileFromURL.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAgB3D,eAAO,MAAM,qBAAqB,EAAE,cA+GnC,CAAA"}
@@ -3,6 +3,8 @@ import { APIError } from '../../errors/APIError.js';
3
3
  import { Forbidden } from '../../errors/Forbidden.js';
4
4
  import { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js';
5
5
  import { isURLAllowed } from '../../utilities/isURLAllowed.js';
6
+ import { sanitizeFilename } from '../../utilities/sanitizeFilename.js';
7
+ import { safeFetch } from '../safeFetch.js';
6
8
  // If doc id is provided, it means we are updating the doc
7
9
  // /:collectionSlug/paste-url/:doc-id?src=:fileUrl
8
10
  // If doc id is not provided, it means we are creating a new doc
@@ -35,39 +37,76 @@ export const getFileFromURLHandler = async (req)=>{
35
37
  throw new Forbidden(req.t);
36
38
  }
37
39
  }
40
+ if (!req.url) {
41
+ throw new APIError('Request URL is missing.', 400);
42
+ }
43
+ const { searchParams } = new URL(req.url);
44
+ const src = searchParams.get('src');
45
+ if (!src || typeof src !== 'string') {
46
+ throw new APIError('A valid URL string is required.', 400);
47
+ }
48
+ const hasAllowList = typeof config.upload.pasteURL === 'object' && Array.isArray(config.upload.pasteURL.allowList);
49
+ let fileURL;
38
50
  try {
39
- if (!req.url) {
40
- throw new APIError('Request URL is missing.', 400);
41
- }
42
- const { searchParams } = new URL(req.url);
43
- const src = searchParams.get('src');
44
- if (!src || typeof src !== 'string') {
45
- throw new APIError('A valid URL string is required.', 400);
46
- }
47
- const validatedUrl = new URL(src);
48
- if (!isURLAllowed(validatedUrl.href, config.upload.pasteURL.allowList)) {
49
- throw new APIError(`The provided URL (${validatedUrl.href}) is not allowed.`, 400);
51
+ fileURL = new URL(src).href;
52
+ } catch {
53
+ throw new APIError('A valid URL string is required.', 400);
54
+ }
55
+ if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {
56
+ throw new APIError('The provided URL is not allowed.', 400);
57
+ }
58
+ let redirectCount = 0;
59
+ const maxRedirects = 3;
60
+ let response;
61
+ while(true){
62
+ if (hasAllowList && isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {
63
+ // Allow-listed URLs bypass SSRF filtering (e.g. internal/localhost CDNs)
64
+ response = await fetch(fileURL, {
65
+ headers: {
66
+ 'Accept-Encoding': 'identity'
67
+ },
68
+ redirect: 'manual',
69
+ signal: AbortSignal.timeout(30_000)
70
+ });
71
+ } else {
72
+ response = await safeFetch(fileURL, {
73
+ headers: {
74
+ 'Accept-Encoding': 'identity'
75
+ },
76
+ signal: AbortSignal.timeout(30_000)
77
+ });
50
78
  }
51
- // Fetch the file with no compression
52
- const response = await fetch(validatedUrl.href, {
53
- headers: {
54
- 'Accept-Encoding': 'identity'
79
+ if (response.status >= 300 && response.status < 400) {
80
+ redirectCount++;
81
+ if (redirectCount > maxRedirects) {
82
+ throw new APIError('Too many redirects.', 403);
55
83
  }
56
- });
57
- if (!response.ok) {
58
- throw new APIError(`Failed to fetch file from ${validatedUrl.href}`, response.status);
59
- }
60
- const decodedFileName = decodeURIComponent(validatedUrl.pathname.split('/').pop() || '');
61
- return new Response(response.body, {
62
- headers: {
63
- 'Content-Disposition': `attachment; filename="${decodedFileName}"`,
64
- 'Content-Length': response.headers.get('content-length') || '',
65
- 'Content-Type': response.headers.get('content-type') || 'application/octet-stream'
84
+ const location = response.headers.get('location');
85
+ if (location) {
86
+ fileURL = new URL(location, fileURL).href;
87
+ if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {
88
+ throw new APIError('The provided URL is not allowed.', 400);
89
+ }
90
+ continue;
66
91
  }
67
- });
68
- } catch (err) {
69
- throw new APIError(`Error fetching file: ${err instanceof Error ? err.message : 'Unknown error'}`, 500);
92
+ }
93
+ break;
94
+ }
95
+ if (!response.ok) {
96
+ throw new APIError('Failed to fetch the file from the provided URL.', response.status);
70
97
  }
98
+ const rawFileName = decodeURIComponent(new URL(fileURL).pathname.split('/').pop() || '');
99
+ const safeFileName = sanitizeFilename(rawFileName);
100
+ const encodedFileName = encodeURIComponent(safeFileName).replace(/['()]/g, (c)=>`%${c.charCodeAt(0).toString(16).toUpperCase()}`);
101
+ // Strip quotes, backslashes, and control chars from the ASCII fallback
102
+ const asciiFileName = safeFileName.replace(/["\\\r\n]/g, '_');
103
+ return new Response(response.body, {
104
+ headers: {
105
+ 'Content-Disposition': `attachment; filename="${asciiFileName}"; filename*=UTF-8''${encodedFileName}`,
106
+ 'Content-Length': response.headers.get('content-length') || '',
107
+ 'Content-Type': response.headers.get('content-type') || 'application/octet-stream'
108
+ }
109
+ });
71
110
  };
72
111
 
73
112
  //# sourceMappingURL=getFileFromURL.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/uploads/endpoints/getFileFromURL.ts"],"sourcesContent":["import type { PayloadHandler } from '../../config/types.js'\n\nimport { executeAccess } from '../../auth/executeAccess.js'\nimport { APIError } from '../../errors/APIError.js'\nimport { Forbidden } from '../../errors/Forbidden.js'\nimport { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'\nimport { isURLAllowed } from '../../utilities/isURLAllowed.js'\n\n// If doc id is provided, it means we are updating the doc\n// /:collectionSlug/paste-url/:doc-id?src=:fileUrl\n\n// If doc id is not provided, it means we are creating a new doc\n// /:collectionSlug/paste-url?src=:fileUrl\n\nexport const getFileFromURLHandler: PayloadHandler = async (req) => {\n const { id, collection } = getRequestCollectionWithID(req, { optionalID: true })\n\n if (!req.user) {\n throw new Forbidden(req.t)\n }\n\n const config = collection?.config\n\n if (!config.upload?.pasteURL) {\n throw new APIError('Pasting from URL is not enabled for this collection.', 400)\n }\n\n if (id) {\n // updating doc\n const accessResult = await executeAccess({ req }, config.access.update)\n if (!accessResult) {\n throw new Forbidden(req.t)\n }\n } else {\n // creating doc\n const accessResult = await executeAccess({ req }, config.access?.create)\n if (!accessResult) {\n throw new Forbidden(req.t)\n }\n }\n try {\n if (!req.url) {\n throw new APIError('Request URL is missing.', 400)\n }\n\n const { searchParams } = new URL(req.url)\n const src = searchParams.get('src')\n\n if (!src || typeof src !== 'string') {\n throw new APIError('A valid URL string is required.', 400)\n }\n\n const validatedUrl = new URL(src)\n\n if (!isURLAllowed(validatedUrl.href, config.upload.pasteURL.allowList)) {\n throw new APIError(`The provided URL (${validatedUrl.href}) is not allowed.`, 400)\n }\n\n // Fetch the file with no compression\n const response = await fetch(validatedUrl.href, {\n headers: {\n 'Accept-Encoding': 'identity',\n },\n })\n\n if (!response.ok) {\n throw new APIError(`Failed to fetch file from ${validatedUrl.href}`, response.status)\n }\n\n const decodedFileName = decodeURIComponent(validatedUrl.pathname.split('/').pop() || '')\n\n return new Response(response.body, {\n headers: {\n 'Content-Disposition': `attachment; filename=\"${decodedFileName}\"`,\n 'Content-Length': response.headers.get('content-length') || '',\n 'Content-Type': response.headers.get('content-type') || 'application/octet-stream',\n },\n })\n } catch (err) {\n throw new APIError(\n `Error fetching file: ${err instanceof Error ? err.message : 'Unknown error'}`,\n 500,\n )\n }\n}\n"],"names":["executeAccess","APIError","Forbidden","getRequestCollectionWithID","isURLAllowed","getFileFromURLHandler","req","id","collection","optionalID","user","t","config","upload","pasteURL","accessResult","access","update","create","url","searchParams","URL","src","get","validatedUrl","href","allowList","response","fetch","headers","ok","status","decodedFileName","decodeURIComponent","pathname","split","pop","Response","body","err","Error","message"],"mappings":"AAEA,SAASA,aAAa,QAAQ,8BAA6B;AAC3D,SAASC,QAAQ,QAAQ,2BAA0B;AACnD,SAASC,SAAS,QAAQ,4BAA2B;AACrD,SAASC,0BAA0B,QAAQ,sCAAqC;AAChF,SAASC,YAAY,QAAQ,kCAAiC;AAE9D,0DAA0D;AAC1D,kDAAkD;AAElD,gEAAgE;AAChE,0CAA0C;AAE1C,OAAO,MAAMC,wBAAwC,OAAOC;IAC1D,MAAM,EAAEC,EAAE,EAAEC,UAAU,EAAE,GAAGL,2BAA2BG,KAAK;QAAEG,YAAY;IAAK;IAE9E,IAAI,CAACH,IAAII,IAAI,EAAE;QACb,MAAM,IAAIR,UAAUI,IAAIK,CAAC;IAC3B;IAEA,MAAMC,SAASJ,YAAYI;IAE3B,IAAI,CAACA,OAAOC,MAAM,EAAEC,UAAU;QAC5B,MAAM,IAAIb,SAAS,wDAAwD;IAC7E;IAEA,IAAIM,IAAI;QACN,eAAe;QACf,MAAMQ,eAAe,MAAMf,cAAc;YAAEM;QAAI,GAAGM,OAAOI,MAAM,CAACC,MAAM;QACtE,IAAI,CAACF,cAAc;YACjB,MAAM,IAAIb,UAAUI,IAAIK,CAAC;QAC3B;IACF,OAAO;QACL,eAAe;QACf,MAAMI,eAAe,MAAMf,cAAc;YAAEM;QAAI,GAAGM,OAAOI,MAAM,EAAEE;QACjE,IAAI,CAACH,cAAc;YACjB,MAAM,IAAIb,UAAUI,IAAIK,CAAC;QAC3B;IACF;IACA,IAAI;QACF,IAAI,CAACL,IAAIa,GAAG,EAAE;YACZ,MAAM,IAAIlB,SAAS,2BAA2B;QAChD;QAEA,MAAM,EAAEmB,YAAY,EAAE,GAAG,IAAIC,IAAIf,IAAIa,GAAG;QACxC,MAAMG,MAAMF,aAAaG,GAAG,CAAC;QAE7B,IAAI,CAACD,OAAO,OAAOA,QAAQ,UAAU;YACnC,MAAM,IAAIrB,SAAS,mCAAmC;QACxD;QAEA,MAAMuB,eAAe,IAAIH,IAAIC;QAE7B,IAAI,CAAClB,aAAaoB,aAAaC,IAAI,EAAEb,OAAOC,MAAM,CAACC,QAAQ,CAACY,SAAS,GAAG;YACtE,MAAM,IAAIzB,SAAS,CAAC,kBAAkB,EAAEuB,aAAaC,IAAI,CAAC,iBAAiB,CAAC,EAAE;QAChF;QAEA,qCAAqC;QACrC,MAAME,WAAW,MAAMC,MAAMJ,aAAaC,IAAI,EAAE;YAC9CI,SAAS;gBACP,mBAAmB;YACrB;QACF;QAEA,IAAI,CAACF,SAASG,EAAE,EAAE;YAChB,MAAM,IAAI7B,SAAS,CAAC,0BAA0B,EAAEuB,aAAaC,IAAI,EAAE,EAAEE,SAASI,MAAM;QACtF;QAEA,MAAMC,kBAAkBC,mBAAmBT,aAAaU,QAAQ,CAACC,KAAK,CAAC,KAAKC,GAAG,MAAM;QAErF,OAAO,IAAIC,SAASV,SAASW,IAAI,EAAE;YACjCT,SAAS;gBACP,uBAAuB,CAAC,sBAAsB,EAAEG,gBAAgB,CAAC,CAAC;gBAClE,kBAAkBL,SAASE,OAAO,CAACN,GAAG,CAAC,qBAAqB;gBAC5D,gBAAgBI,SAASE,OAAO,CAACN,GAAG,CAAC,mBAAmB;YAC1D;QACF;IACF,EAAE,OAAOgB,KAAK;QACZ,MAAM,IAAItC,SACR,CAAC,qBAAqB,EAAEsC,eAAeC,QAAQD,IAAIE,OAAO,GAAG,iBAAiB,EAC9E;IAEJ;AACF,EAAC"}
1
+ {"version":3,"sources":["../../../src/uploads/endpoints/getFileFromURL.ts"],"sourcesContent":["import type { PayloadHandler } from '../../config/types.js'\n\nimport { executeAccess } from '../../auth/executeAccess.js'\nimport { APIError } from '../../errors/APIError.js'\nimport { Forbidden } from '../../errors/Forbidden.js'\nimport { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'\nimport { isURLAllowed } from '../../utilities/isURLAllowed.js'\nimport { sanitizeFilename } from '../../utilities/sanitizeFilename.js'\nimport { safeFetch } from '../safeFetch.js'\n\n// If doc id is provided, it means we are updating the doc\n// /:collectionSlug/paste-url/:doc-id?src=:fileUrl\n\n// If doc id is not provided, it means we are creating a new doc\n// /:collectionSlug/paste-url?src=:fileUrl\n\nexport const getFileFromURLHandler: PayloadHandler = async (req) => {\n const { id, collection } = getRequestCollectionWithID(req, { optionalID: true })\n\n if (!req.user) {\n throw new Forbidden(req.t)\n }\n\n const config = collection?.config\n\n if (!config.upload?.pasteURL) {\n throw new APIError('Pasting from URL is not enabled for this collection.', 400)\n }\n\n if (id) {\n // updating doc\n const accessResult = await executeAccess({ req }, config.access.update)\n if (!accessResult) {\n throw new Forbidden(req.t)\n }\n } else {\n // creating doc\n const accessResult = await executeAccess({ req }, config.access?.create)\n if (!accessResult) {\n throw new Forbidden(req.t)\n }\n }\n\n if (!req.url) {\n throw new APIError('Request URL is missing.', 400)\n }\n\n const { searchParams } = new URL(req.url)\n const src = searchParams.get('src')\n\n if (!src || typeof src !== 'string') {\n throw new APIError('A valid URL string is required.', 400)\n }\n\n const hasAllowList =\n typeof config.upload.pasteURL === 'object' && Array.isArray(config.upload.pasteURL.allowList)\n\n let fileURL: string\n try {\n fileURL = new URL(src).href\n } catch {\n throw new APIError('A valid URL string is required.', 400)\n }\n\n if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {\n throw new APIError('The provided URL is not allowed.', 400)\n }\n\n let redirectCount = 0\n const maxRedirects = 3\n let response!: Response\n\n while (true) {\n if (hasAllowList && isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {\n // Allow-listed URLs bypass SSRF filtering (e.g. internal/localhost CDNs)\n response = await fetch(fileURL, {\n headers: { 'Accept-Encoding': 'identity' },\n redirect: 'manual',\n signal: AbortSignal.timeout(30_000),\n })\n } else {\n response = await safeFetch(fileURL, {\n headers: {\n 'Accept-Encoding': 'identity',\n },\n signal: AbortSignal.timeout(30_000),\n })\n }\n\n if (response.status >= 300 && response.status < 400) {\n redirectCount++\n if (redirectCount > maxRedirects) {\n throw new APIError('Too many redirects.', 403)\n }\n const location = response.headers.get('location')\n if (location) {\n fileURL = new URL(location, fileURL).href\n if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {\n throw new APIError('The provided URL is not allowed.', 400)\n }\n continue\n }\n }\n\n break\n }\n\n if (!response.ok) {\n throw new APIError('Failed to fetch the file from the provided URL.', response.status)\n }\n\n const rawFileName = decodeURIComponent(new URL(fileURL).pathname.split('/').pop() || '')\n const safeFileName = sanitizeFilename(rawFileName)\n const encodedFileName = encodeURIComponent(safeFileName).replace(\n /['()]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n )\n // Strip quotes, backslashes, and control chars from the ASCII fallback\n const asciiFileName = safeFileName.replace(/[\"\\\\\\r\\n]/g, '_')\n\n return new Response(response.body, {\n headers: {\n 'Content-Disposition': `attachment; filename=\"${asciiFileName}\"; filename*=UTF-8''${encodedFileName}`,\n 'Content-Length': response.headers.get('content-length') || '',\n 'Content-Type': response.headers.get('content-type') || 'application/octet-stream',\n },\n })\n}\n"],"names":["executeAccess","APIError","Forbidden","getRequestCollectionWithID","isURLAllowed","sanitizeFilename","safeFetch","getFileFromURLHandler","req","id","collection","optionalID","user","t","config","upload","pasteURL","accessResult","access","update","create","url","searchParams","URL","src","get","hasAllowList","Array","isArray","allowList","fileURL","href","redirectCount","maxRedirects","response","fetch","headers","redirect","signal","AbortSignal","timeout","status","location","ok","rawFileName","decodeURIComponent","pathname","split","pop","safeFileName","encodedFileName","encodeURIComponent","replace","c","charCodeAt","toString","toUpperCase","asciiFileName","Response","body"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"getExternalFile.d.ts","sourceRoot":"","sources":["../../src/uploads/getExternalFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAM9D,KAAK,IAAI,GAAG;IACV,IAAI,EAAE,QAAQ,CAAA;IACd,GAAG,EAAE,cAAc,CAAA;IACnB,YAAY,EAAE,YAAY,CAAA;CAC3B,CAAA;AACD,eAAO,MAAM,eAAe,gCAAuC,IAAI,KAAG,OAAO,CAAC,IAAI,CAyFrF,CAAA"}
1
+ {"version":3,"file":"getExternalFile.d.ts","sourceRoot":"","sources":["../../src/uploads/getExternalFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAM9D,KAAK,IAAI,GAAG;IACV,IAAI,EAAE,QAAQ,CAAA;IACd,GAAG,EAAE,cAAc,CAAA;IACnB,YAAY,EAAE,YAAY,CAAA;CAC3B,CAAA;AACD,eAAO,MAAM,eAAe,gCAAuC,IAAI,KAAG,OAAO,CAAC,IAAI,CAgGrF,CAAA"}
@@ -48,6 +48,9 @@ export const getExternalFile = async ({ data, req, uploadConfig })=>{
48
48
  const location = res.headers.get('location');
49
49
  if (location) {
50
50
  fileURL = new URL(location, fileURL).toString();
51
+ if (uploadConfig.pasteURL && uploadConfig.pasteURL.allowList && !isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)) {
52
+ throw new APIError('Redirect target is not allowed.', 400);
53
+ }
51
54
  continue;
52
55
  }
53
56
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/uploads/getExternalFile.ts"],"sourcesContent":["import type { PayloadRequest } from '../types/index.js'\nimport type { File, FileData, UploadConfig } from './types.js'\n\nimport { APIError } from '../errors/index.js'\nimport { isURLAllowed } from '../utilities/isURLAllowed.js'\nimport { safeFetch } from './safeFetch.js'\n\ntype Args = {\n data: FileData\n req: PayloadRequest\n uploadConfig: UploadConfig\n}\nexport const getExternalFile = async ({ data, req, uploadConfig }: Args): Promise<File> => {\n const { filename, url } = data\n\n let trimAuthCookies = true\n if (typeof url === 'string') {\n let fileURL = url\n if (!url.startsWith('http')) {\n // URL points to the same server - we can send any cookies safely to our server.\n trimAuthCookies = false\n const baseUrl = req.headers.get('origin') || `${req.protocol}://${req.headers.get('host')}`\n fileURL = `${baseUrl}${url}`\n }\n\n let cookies = (req.headers.get('cookie') ?? '').split(';')\n\n if (trimAuthCookies) {\n cookies = cookies.filter(\n (cookie) => !cookie.trim().startsWith(req.payload.config.cookiePrefix),\n )\n }\n\n const headers = uploadConfig.externalFileHeaderFilter\n ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers)))\n : {\n cookie: cookies.join(';'),\n }\n\n let res\n let redirectCount = 0\n const maxRedirects = 3\n\n while (redirectCount <= maxRedirects) {\n const skipSafeFetch: boolean =\n uploadConfig.skipSafeFetch === true\n ? uploadConfig.skipSafeFetch\n : Array.isArray(uploadConfig.skipSafeFetch) &&\n isURLAllowed(fileURL, uploadConfig.skipSafeFetch)\n\n const isAllowedPasteUrl: boolean | undefined =\n uploadConfig.pasteURL &&\n uploadConfig.pasteURL.allowList &&\n isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)\n\n if (skipSafeFetch || isAllowedPasteUrl) {\n res = await fetch(fileURL, {\n credentials: 'include',\n headers,\n method: 'GET',\n redirect: 'manual',\n })\n } else {\n // Default\n res = await safeFetch(fileURL, {\n credentials: 'include',\n headers,\n method: 'GET',\n })\n }\n\n if (res.status >= 300 && res.status < 400) {\n redirectCount++\n if (redirectCount > maxRedirects) {\n throw new APIError(`Too many redirects (max ${maxRedirects})`, 403)\n }\n const location = res.headers.get('location')\n if (location) {\n fileURL = new URL(location, fileURL).toString()\n continue\n }\n }\n\n break\n }\n\n if (!res || !res.ok) {\n throw new APIError(`Failed to fetch file from ${fileURL}`, res?.status)\n }\n\n const data = await res.arrayBuffer()\n\n return {\n name: filename,\n data: Buffer.from(data),\n mimetype: res.headers.get('content-type') || undefined!,\n size: Number(res.headers.get('content-length')) || 0,\n }\n }\n\n throw new APIError('Invalid file url', 400)\n}\n"],"names":["APIError","isURLAllowed","safeFetch","getExternalFile","data","req","uploadConfig","filename","url","trimAuthCookies","fileURL","startsWith","baseUrl","headers","get","protocol","cookies","split","filter","cookie","trim","payload","config","cookiePrefix","externalFileHeaderFilter","Object","fromEntries","Headers","join","res","redirectCount","maxRedirects","skipSafeFetch","Array","isArray","isAllowedPasteUrl","pasteURL","allowList","fetch","credentials","method","redirect","status","location","URL","toString","ok","arrayBuffer","name","Buffer","from","mimetype","undefined","size","Number"],"mappings":"AAGA,SAASA,QAAQ,QAAQ,qBAAoB;AAC7C,SAASC,YAAY,QAAQ,+BAA8B;AAC3D,SAASC,SAAS,QAAQ,iBAAgB;AAO1C,OAAO,MAAMC,kBAAkB,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,YAAY,EAAQ;IACrE,MAAM,EAAEC,QAAQ,EAAEC,GAAG,EAAE,GAAGJ;IAE1B,IAAIK,kBAAkB;IACtB,IAAI,OAAOD,QAAQ,UAAU;QAC3B,IAAIE,UAAUF;QACd,IAAI,CAACA,IAAIG,UAAU,CAAC,SAAS;YAC3B,gFAAgF;YAChFF,kBAAkB;YAClB,MAAMG,UAAUP,IAAIQ,OAAO,CAACC,GAAG,CAAC,aAAa,GAAGT,IAAIU,QAAQ,CAAC,GAAG,EAAEV,IAAIQ,OAAO,CAACC,GAAG,CAAC,SAAS;YAC3FJ,UAAU,GAAGE,UAAUJ,KAAK;QAC9B;QAEA,IAAIQ,UAAU,AAACX,CAAAA,IAAIQ,OAAO,CAACC,GAAG,CAAC,aAAa,EAAC,EAAGG,KAAK,CAAC;QAEtD,IAAIR,iBAAiB;YACnBO,UAAUA,QAAQE,MAAM,CACtB,CAACC,SAAW,CAACA,OAAOC,IAAI,GAAGT,UAAU,CAACN,IAAIgB,OAAO,CAACC,MAAM,CAACC,YAAY;QAEzE;QAEA,MAAMV,UAAUP,aAAakB,wBAAwB,GACjDlB,aAAakB,wBAAwB,CAACC,OAAOC,WAAW,CAAC,IAAIC,QAAQtB,IAAIQ,OAAO,MAChF;YACEM,QAAQH,QAAQY,IAAI,CAAC;QACvB;QAEJ,IAAIC;QACJ,IAAIC,gBAAgB;QACpB,MAAMC,eAAe;QAErB,MAAOD,iBAAiBC,aAAc;YACpC,MAAMC,gBACJ1B,aAAa0B,aAAa,KAAK,OAC3B1B,aAAa0B,aAAa,GAC1BC,MAAMC,OAAO,CAAC5B,aAAa0B,aAAa,KACxC/B,aAAaS,SAASJ,aAAa0B,aAAa;YAEtD,MAAMG,oBACJ7B,aAAa8B,QAAQ,IACrB9B,aAAa8B,QAAQ,CAACC,SAAS,IAC/BpC,aAAaS,SAASJ,aAAa8B,QAAQ,CAACC,SAAS;YAEvD,IAAIL,iBAAiBG,mBAAmB;gBACtCN,MAAM,MAAMS,MAAM5B,SAAS;oBACzB6B,aAAa;oBACb1B;oBACA2B,QAAQ;oBACRC,UAAU;gBACZ;YACF,OAAO;gBACL,UAAU;gBACVZ,MAAM,MAAM3B,UAAUQ,SAAS;oBAC7B6B,aAAa;oBACb1B;oBACA2B,QAAQ;gBACV;YACF;YAEA,IAAIX,IAAIa,MAAM,IAAI,OAAOb,IAAIa,MAAM,GAAG,KAAK;gBACzCZ;gBACA,IAAIA,gBAAgBC,cAAc;oBAChC,MAAM,IAAI/B,SAAS,CAAC,wBAAwB,EAAE+B,aAAa,CAAC,CAAC,EAAE;gBACjE;gBACA,MAAMY,WAAWd,IAAIhB,OAAO,CAACC,GAAG,CAAC;gBACjC,IAAI6B,UAAU;oBACZjC,UAAU,IAAIkC,IAAID,UAAUjC,SAASmC,QAAQ;oBAC7C;gBACF;YACF;YAEA;QACF;QAEA,IAAI,CAAChB,OAAO,CAACA,IAAIiB,EAAE,EAAE;YACnB,MAAM,IAAI9C,SAAS,CAAC,0BAA0B,EAAEU,SAAS,EAAEmB,KAAKa;QAClE;QAEA,MAAMtC,OAAO,MAAMyB,IAAIkB,WAAW;QAElC,OAAO;YACLC,MAAMzC;YACNH,MAAM6C,OAAOC,IAAI,CAAC9C;YAClB+C,UAAUtB,IAAIhB,OAAO,CAACC,GAAG,CAAC,mBAAmBsC;YAC7CC,MAAMC,OAAOzB,IAAIhB,OAAO,CAACC,GAAG,CAAC,sBAAsB;QACrD;IACF;IAEA,MAAM,IAAId,SAAS,oBAAoB;AACzC,EAAC"}
1
+ {"version":3,"sources":["../../src/uploads/getExternalFile.ts"],"sourcesContent":["import type { PayloadRequest } from '../types/index.js'\nimport type { File, FileData, UploadConfig } from './types.js'\n\nimport { APIError } from '../errors/index.js'\nimport { isURLAllowed } from '../utilities/isURLAllowed.js'\nimport { safeFetch } from './safeFetch.js'\n\ntype Args = {\n data: FileData\n req: PayloadRequest\n uploadConfig: UploadConfig\n}\nexport const getExternalFile = async ({ data, req, uploadConfig }: Args): Promise<File> => {\n const { filename, url } = data\n\n let trimAuthCookies = true\n if (typeof url === 'string') {\n let fileURL = url\n if (!url.startsWith('http')) {\n // URL points to the same server - we can send any cookies safely to our server.\n trimAuthCookies = false\n const baseUrl = req.headers.get('origin') || `${req.protocol}://${req.headers.get('host')}`\n fileURL = `${baseUrl}${url}`\n }\n\n let cookies = (req.headers.get('cookie') ?? '').split(';')\n\n if (trimAuthCookies) {\n cookies = cookies.filter(\n (cookie) => !cookie.trim().startsWith(req.payload.config.cookiePrefix),\n )\n }\n\n const headers = uploadConfig.externalFileHeaderFilter\n ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers)))\n : {\n cookie: cookies.join(';'),\n }\n\n let res\n let redirectCount = 0\n const maxRedirects = 3\n\n while (redirectCount <= maxRedirects) {\n const skipSafeFetch: boolean =\n uploadConfig.skipSafeFetch === true\n ? uploadConfig.skipSafeFetch\n : Array.isArray(uploadConfig.skipSafeFetch) &&\n isURLAllowed(fileURL, uploadConfig.skipSafeFetch)\n\n const isAllowedPasteUrl: boolean | undefined =\n uploadConfig.pasteURL &&\n uploadConfig.pasteURL.allowList &&\n isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)\n\n if (skipSafeFetch || isAllowedPasteUrl) {\n res = await fetch(fileURL, {\n credentials: 'include',\n headers,\n method: 'GET',\n redirect: 'manual',\n })\n } else {\n // Default\n res = await safeFetch(fileURL, {\n credentials: 'include',\n headers,\n method: 'GET',\n })\n }\n\n if (res.status >= 300 && res.status < 400) {\n redirectCount++\n if (redirectCount > maxRedirects) {\n throw new APIError(`Too many redirects (max ${maxRedirects})`, 403)\n }\n const location = res.headers.get('location')\n if (location) {\n fileURL = new URL(location, fileURL).toString()\n if (\n uploadConfig.pasteURL &&\n uploadConfig.pasteURL.allowList &&\n !isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)\n ) {\n throw new APIError('Redirect target is not allowed.', 400)\n }\n continue\n }\n }\n\n break\n }\n\n if (!res || !res.ok) {\n throw new APIError(`Failed to fetch file from ${fileURL}`, res?.status)\n }\n\n const data = await res.arrayBuffer()\n\n return {\n name: filename,\n data: Buffer.from(data),\n mimetype: res.headers.get('content-type') || undefined!,\n size: Number(res.headers.get('content-length')) || 0,\n }\n }\n\n throw new APIError('Invalid file url', 400)\n}\n"],"names":["APIError","isURLAllowed","safeFetch","getExternalFile","data","req","uploadConfig","filename","url","trimAuthCookies","fileURL","startsWith","baseUrl","headers","get","protocol","cookies","split","filter","cookie","trim","payload","config","cookiePrefix","externalFileHeaderFilter","Object","fromEntries","Headers","join","res","redirectCount","maxRedirects","skipSafeFetch","Array","isArray","isAllowedPasteUrl","pasteURL","allowList","fetch","credentials","method","redirect","status","location","URL","toString","ok","arrayBuffer","name","Buffer","from","mimetype","undefined","size","Number"],"mappings":"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"}
@@ -13,5 +13,5 @@ export declare const _internal_safeFetchGlobal: {
13
13
  * - Validates domain names by resolving them to IP addresses and checking if they're safe.
14
14
  * - Undici was used because it supported interceptors as well as "credentials: include". Native fetch
15
15
  */
16
- export declare const safeFetch: (input: import("undici").RequestInfo, init?: import("undici").RequestInit | undefined) => Promise<import("undici").Response>;
16
+ export declare const safeFetch: (input: import("undici").RequestInfo, init?: import("undici").RequestInit | undefined) => Promise<Response>;
17
17
  //# sourceMappingURL=safeFetch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safeFetch.d.ts","sourceRoot":"","sources":["../../src/uploads/safeFetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,EAAS,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAA;AAEpD;;GAEG;AACH,eAAO,MAAM,yBAAyB;;CAErC,CAAA;AAgDD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,8HA4CrB,CAAA"}
1
+ {"version":3,"file":"safeFetch.d.ts","sourceRoot":"","sources":["../../src/uploads/safeFetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,EAAS,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAA;AAEpD;;GAEG;AACH,eAAO,MAAM,yBAAyB;;CAErC,CAAA;AAgDD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,4FAAoD,OAAO,CAAC,QAAQ,CA4CzF,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/uploads/safeFetch.ts"],"sourcesContent":["import type { LookupFunction } from 'net'\n\nimport { lookup } from 'dns'\nimport ipaddr from 'ipaddr.js'\nimport { Agent, fetch as undiciFetch } from 'undici'\n\n/**\n * @internal this is used to mock the IP `lookup` function in integration tests\n */\nexport const _internal_safeFetchGlobal = {\n lookup,\n}\n\nconst isSafeIp = (ip: string) => {\n try {\n if (!ip) {\n return false\n }\n\n if (!ipaddr.isValid(ip)) {\n return false\n }\n\n const parsedIpAddress = ipaddr.parse(ip)\n const range = parsedIpAddress.range()\n if (range !== 'unicast') {\n return false // Private IP Range\n }\n } catch (ignore) {\n return false\n }\n return true\n}\n\nconst ssrfFilterInterceptor: LookupFunction = (hostname, options, callback) => {\n _internal_safeFetchGlobal.lookup(hostname, options, (err, address, family) => {\n if (err) {\n callback(err, address, family)\n } else {\n let ips = [] as string[]\n if (Array.isArray(address)) {\n ips = address.map((a) => a.address)\n } else {\n ips = [address]\n }\n\n if (ips.some((ip) => !isSafeIp(ip))) {\n callback(new Error(`Blocked unsafe attempt to ${hostname}`), address, family)\n return\n }\n\n callback(null, address, family)\n }\n })\n}\n\nconst safeDispatcher = new Agent({\n connect: { lookup: ssrfFilterInterceptor },\n})\n/**\n * A \"safe\" version of undici's fetch that prevents SSRF attacks.\n *\n * - Utilizes a custom dispatcher that filters out requests to unsafe IP addresses.\n * - Validates domain names by resolving them to IP addresses and checking if they're safe.\n * - Undici was used because it supported interceptors as well as \"credentials: include\". Native fetch\n */\nexport const safeFetch = async (...args: Parameters<typeof undiciFetch>) => {\n const [unverifiedUrl, options] = args\n\n try {\n const url = new URL(unverifiedUrl)\n\n let hostname = url.hostname\n\n // Strip brackets from IPv6 addresses (e.g., \"[::1]\" => \"::1\")\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n hostname = hostname.slice(1, -1)\n }\n\n if (ipaddr.isValid(hostname)) {\n if (!isSafeIp(hostname)) {\n throw new Error(`Blocked unsafe attempt to ${hostname}`)\n }\n }\n return await undiciFetch(url, {\n ...options,\n dispatcher: safeDispatcher,\n redirect: 'manual', // Prevent automatic redirects\n })\n } catch (error) {\n if (error instanceof Error) {\n if (error.cause instanceof Error && error.cause.message.includes('unsafe')) {\n // Errors thrown from within interceptors always have 'fetch error' as the message\n // The desired message we want to bubble up is in the cause\n throw new Error(error.cause.message)\n } else {\n let stringifiedUrl: string | undefined = undefined\n if (typeof unverifiedUrl === 'string') {\n stringifiedUrl = unverifiedUrl\n } else if (unverifiedUrl instanceof URL) {\n stringifiedUrl = unverifiedUrl.toString()\n } else if (unverifiedUrl instanceof Request) {\n stringifiedUrl = unverifiedUrl.url\n }\n\n throw new Error(`Failed to fetch from ${stringifiedUrl}, ${error.message}`)\n }\n }\n throw error\n }\n}\n"],"names":["lookup","ipaddr","Agent","fetch","undiciFetch","_internal_safeFetchGlobal","isSafeIp","ip","isValid","parsedIpAddress","parse","range","ignore","ssrfFilterInterceptor","hostname","options","callback","err","address","family","ips","Array","isArray","map","a","some","Error","safeDispatcher","connect","safeFetch","args","unverifiedUrl","url","URL","startsWith","endsWith","slice","dispatcher","redirect","error","cause","message","includes","stringifiedUrl","undefined","toString","Request"],"mappings":"AAEA,SAASA,MAAM,QAAQ,MAAK;AAC5B,OAAOC,YAAY,YAAW;AAC9B,SAASC,KAAK,EAAEC,SAASC,WAAW,QAAQ,SAAQ;AAEpD;;CAEC,GACD,OAAO,MAAMC,4BAA4B;IACvCL;AACF,EAAC;AAED,MAAMM,WAAW,CAACC;IAChB,IAAI;QACF,IAAI,CAACA,IAAI;YACP,OAAO;QACT;QAEA,IAAI,CAACN,OAAOO,OAAO,CAACD,KAAK;YACvB,OAAO;QACT;QAEA,MAAME,kBAAkBR,OAAOS,KAAK,CAACH;QACrC,MAAMI,QAAQF,gBAAgBE,KAAK;QACnC,IAAIA,UAAU,WAAW;YACvB,OAAO,MAAM,mBAAmB;;QAClC;IACF,EAAE,OAAOC,QAAQ;QACf,OAAO;IACT;IACA,OAAO;AACT;AAEA,MAAMC,wBAAwC,CAACC,UAAUC,SAASC;IAChEX,0BAA0BL,MAAM,CAACc,UAAUC,SAAS,CAACE,KAAKC,SAASC;QACjE,IAAIF,KAAK;YACPD,SAASC,KAAKC,SAASC;QACzB,OAAO;YACL,IAAIC,MAAM,EAAE;YACZ,IAAIC,MAAMC,OAAO,CAACJ,UAAU;gBAC1BE,MAAMF,QAAQK,GAAG,CAAC,CAACC,IAAMA,EAAEN,OAAO;YACpC,OAAO;gBACLE,MAAM;oBAACF;iBAAQ;YACjB;YAEA,IAAIE,IAAIK,IAAI,CAAC,CAAClB,KAAO,CAACD,SAASC,MAAM;gBACnCS,SAAS,IAAIU,MAAM,CAAC,0BAA0B,EAAEZ,UAAU,GAAGI,SAASC;gBACtE;YACF;YAEAH,SAAS,MAAME,SAASC;QAC1B;IACF;AACF;AAEA,MAAMQ,iBAAiB,IAAIzB,MAAM;IAC/B0B,SAAS;QAAE5B,QAAQa;IAAsB;AAC3C;AACA;;;;;;CAMC,GACD,OAAO,MAAMgB,YAAY,OAAO,GAAGC;IACjC,MAAM,CAACC,eAAehB,QAAQ,GAAGe;IAEjC,IAAI;QACF,MAAME,MAAM,IAAIC,IAAIF;QAEpB,IAAIjB,WAAWkB,IAAIlB,QAAQ;QAE3B,8DAA8D;QAC9D,IAAIA,SAASoB,UAAU,CAAC,QAAQpB,SAASqB,QAAQ,CAAC,MAAM;YACtDrB,WAAWA,SAASsB,KAAK,CAAC,GAAG,CAAC;QAChC;QAEA,IAAInC,OAAOO,OAAO,CAACM,WAAW;YAC5B,IAAI,CAACR,SAASQ,WAAW;gBACvB,MAAM,IAAIY,MAAM,CAAC,0BAA0B,EAAEZ,UAAU;YACzD;QACF;QACA,OAAO,MAAMV,YAAY4B,KAAK;YAC5B,GAAGjB,OAAO;YACVsB,YAAYV;YACZW,UAAU;QACZ;IACF,EAAE,OAAOC,OAAO;QACd,IAAIA,iBAAiBb,OAAO;YAC1B,IAAIa,MAAMC,KAAK,YAAYd,SAASa,MAAMC,KAAK,CAACC,OAAO,CAACC,QAAQ,CAAC,WAAW;gBAC1E,kFAAkF;gBAClF,2DAA2D;gBAC3D,MAAM,IAAIhB,MAAMa,MAAMC,KAAK,CAACC,OAAO;YACrC,OAAO;gBACL,IAAIE,iBAAqCC;gBACzC,IAAI,OAAOb,kBAAkB,UAAU;oBACrCY,iBAAiBZ;gBACnB,OAAO,IAAIA,yBAAyBE,KAAK;oBACvCU,iBAAiBZ,cAAcc,QAAQ;gBACzC,OAAO,IAAId,yBAAyBe,SAAS;oBAC3CH,iBAAiBZ,cAAcC,GAAG;gBACpC;gBAEA,MAAM,IAAIN,MAAM,CAAC,qBAAqB,EAAEiB,eAAe,EAAE,EAAEJ,MAAME,OAAO,EAAE;YAC5E;QACF;QACA,MAAMF;IACR;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/uploads/safeFetch.ts"],"sourcesContent":["import type { LookupFunction } from 'net'\n\nimport { lookup } from 'dns'\nimport ipaddr from 'ipaddr.js'\nimport { Agent, fetch as undiciFetch } from 'undici'\n\n/**\n * @internal this is used to mock the IP `lookup` function in integration tests\n */\nexport const _internal_safeFetchGlobal = {\n lookup,\n}\n\nconst isSafeIp = (ip: string) => {\n try {\n if (!ip) {\n return false\n }\n\n if (!ipaddr.isValid(ip)) {\n return false\n }\n\n const parsedIpAddress = ipaddr.parse(ip)\n const range = parsedIpAddress.range()\n if (range !== 'unicast') {\n return false // Private IP Range\n }\n } catch (ignore) {\n return false\n }\n return true\n}\n\nconst ssrfFilterInterceptor: LookupFunction = (hostname, options, callback) => {\n _internal_safeFetchGlobal.lookup(hostname, options, (err, address, family) => {\n if (err) {\n callback(err, address, family)\n } else {\n let ips = [] as string[]\n if (Array.isArray(address)) {\n ips = address.map((a) => a.address)\n } else {\n ips = [address]\n }\n\n if (ips.some((ip) => !isSafeIp(ip))) {\n callback(new Error(`Blocked unsafe attempt to ${hostname}`), address, family)\n return\n }\n\n callback(null, address, family)\n }\n })\n}\n\nconst safeDispatcher = new Agent({\n connect: { lookup: ssrfFilterInterceptor },\n})\n/**\n * A \"safe\" version of undici's fetch that prevents SSRF attacks.\n *\n * - Utilizes a custom dispatcher that filters out requests to unsafe IP addresses.\n * - Validates domain names by resolving them to IP addresses and checking if they're safe.\n * - Undici was used because it supported interceptors as well as \"credentials: include\". Native fetch\n */\nexport const safeFetch = async (...args: Parameters<typeof undiciFetch>): Promise<Response> => {\n const [unverifiedUrl, options] = args\n\n try {\n const url = new URL(unverifiedUrl)\n\n let hostname = url.hostname\n\n // Strip brackets from IPv6 addresses (e.g., \"[::1]\" => \"::1\")\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n hostname = hostname.slice(1, -1)\n }\n\n if (ipaddr.isValid(hostname)) {\n if (!isSafeIp(hostname)) {\n throw new Error(`Blocked unsafe attempt to ${hostname}`)\n }\n }\n return (await undiciFetch(url, {\n ...options,\n dispatcher: safeDispatcher,\n redirect: 'manual', // Prevent automatic redirects\n })) as unknown as Response\n } catch (error) {\n if (error instanceof Error) {\n if (error.cause instanceof Error && error.cause.message.includes('unsafe')) {\n // Errors thrown from within interceptors always have 'fetch error' as the message\n // The desired message we want to bubble up is in the cause\n throw new Error(error.cause.message)\n } else {\n let stringifiedUrl: string | undefined = undefined\n if (typeof unverifiedUrl === 'string') {\n stringifiedUrl = unverifiedUrl\n } else if (unverifiedUrl instanceof URL) {\n stringifiedUrl = unverifiedUrl.toString()\n } else if (unverifiedUrl instanceof Request) {\n stringifiedUrl = unverifiedUrl.url\n }\n\n throw new Error(`Failed to fetch from ${stringifiedUrl}, ${error.message}`)\n }\n }\n throw error\n }\n}\n"],"names":["lookup","ipaddr","Agent","fetch","undiciFetch","_internal_safeFetchGlobal","isSafeIp","ip","isValid","parsedIpAddress","parse","range","ignore","ssrfFilterInterceptor","hostname","options","callback","err","address","family","ips","Array","isArray","map","a","some","Error","safeDispatcher","connect","safeFetch","args","unverifiedUrl","url","URL","startsWith","endsWith","slice","dispatcher","redirect","error","cause","message","includes","stringifiedUrl","undefined","toString","Request"],"mappings":"AAEA,SAASA,MAAM,QAAQ,MAAK;AAC5B,OAAOC,YAAY,YAAW;AAC9B,SAASC,KAAK,EAAEC,SAASC,WAAW,QAAQ,SAAQ;AAEpD;;CAEC,GACD,OAAO,MAAMC,4BAA4B;IACvCL;AACF,EAAC;AAED,MAAMM,WAAW,CAACC;IAChB,IAAI;QACF,IAAI,CAACA,IAAI;YACP,OAAO;QACT;QAEA,IAAI,CAACN,OAAOO,OAAO,CAACD,KAAK;YACvB,OAAO;QACT;QAEA,MAAME,kBAAkBR,OAAOS,KAAK,CAACH;QACrC,MAAMI,QAAQF,gBAAgBE,KAAK;QACnC,IAAIA,UAAU,WAAW;YACvB,OAAO,MAAM,mBAAmB;;QAClC;IACF,EAAE,OAAOC,QAAQ;QACf,OAAO;IACT;IACA,OAAO;AACT;AAEA,MAAMC,wBAAwC,CAACC,UAAUC,SAASC;IAChEX,0BAA0BL,MAAM,CAACc,UAAUC,SAAS,CAACE,KAAKC,SAASC;QACjE,IAAIF,KAAK;YACPD,SAASC,KAAKC,SAASC;QACzB,OAAO;YACL,IAAIC,MAAM,EAAE;YACZ,IAAIC,MAAMC,OAAO,CAACJ,UAAU;gBAC1BE,MAAMF,QAAQK,GAAG,CAAC,CAACC,IAAMA,EAAEN,OAAO;YACpC,OAAO;gBACLE,MAAM;oBAACF;iBAAQ;YACjB;YAEA,IAAIE,IAAIK,IAAI,CAAC,CAAClB,KAAO,CAACD,SAASC,MAAM;gBACnCS,SAAS,IAAIU,MAAM,CAAC,0BAA0B,EAAEZ,UAAU,GAAGI,SAASC;gBACtE;YACF;YAEAH,SAAS,MAAME,SAASC;QAC1B;IACF;AACF;AAEA,MAAMQ,iBAAiB,IAAIzB,MAAM;IAC/B0B,SAAS;QAAE5B,QAAQa;IAAsB;AAC3C;AACA;;;;;;CAMC,GACD,OAAO,MAAMgB,YAAY,OAAO,GAAGC;IACjC,MAAM,CAACC,eAAehB,QAAQ,GAAGe;IAEjC,IAAI;QACF,MAAME,MAAM,IAAIC,IAAIF;QAEpB,IAAIjB,WAAWkB,IAAIlB,QAAQ;QAE3B,8DAA8D;QAC9D,IAAIA,SAASoB,UAAU,CAAC,QAAQpB,SAASqB,QAAQ,CAAC,MAAM;YACtDrB,WAAWA,SAASsB,KAAK,CAAC,GAAG,CAAC;QAChC;QAEA,IAAInC,OAAOO,OAAO,CAACM,WAAW;YAC5B,IAAI,CAACR,SAASQ,WAAW;gBACvB,MAAM,IAAIY,MAAM,CAAC,0BAA0B,EAAEZ,UAAU;YACzD;QACF;QACA,OAAQ,MAAMV,YAAY4B,KAAK;YAC7B,GAAGjB,OAAO;YACVsB,YAAYV;YACZW,UAAU;QACZ;IACF,EAAE,OAAOC,OAAO;QACd,IAAIA,iBAAiBb,OAAO;YAC1B,IAAIa,MAAMC,KAAK,YAAYd,SAASa,MAAMC,KAAK,CAACC,OAAO,CAACC,QAAQ,CAAC,WAAW;gBAC1E,kFAAkF;gBAClF,2DAA2D;gBAC3D,MAAM,IAAIhB,MAAMa,MAAMC,KAAK,CAACC,OAAO;YACrC,OAAO;gBACL,IAAIE,iBAAqCC;gBACzC,IAAI,OAAOb,kBAAkB,UAAU;oBACrCY,iBAAiBZ;gBACnB,OAAO,IAAIA,yBAAyBE,KAAK;oBACvCU,iBAAiBZ,cAAcc,QAAQ;gBACzC,OAAO,IAAId,yBAAyBe,SAAS;oBAC3CH,iBAAiBZ,cAAcC,GAAG;gBACpC;gBAEA,MAAM,IAAIN,MAAM,CAAC,qBAAqB,EAAEiB,eAAe,EAAE,EAAEJ,MAAME,OAAO,EAAE;YAC5E;QACF;QACA,MAAMF;IACR;AACF,EAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"addDataAndFileToRequest.d.ts","sourceRoot":"","sources":["../../src/utilities/addDataAndFileToRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAKvD,KAAK,uBAAuB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAErE;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,uBA4FrC,CAAA"}
1
+ {"version":3,"file":"addDataAndFileToRequest.d.ts","sourceRoot":"","sources":["../../src/utilities/addDataAndFileToRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAKvD,KAAK,uBAAuB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAErE;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,uBAiGrC,CAAA"}
@@ -43,7 +43,13 @@ import { processMultipartFormdata } from '../uploads/fetchAPI-multipart/index.js
43
43
  req.data = JSON.parse(fields._payload);
44
44
  }
45
45
  if (!req.file && fields?.file && typeof fields?.file === 'string') {
46
- const { clientUploadContext, collectionSlug, filename, mimeType, size } = JSON.parse(fields.file);
46
+ let clientUploadContext, collectionSlug, filename, mimeType, size;
47
+ try {
48
+ ;
49
+ ({ clientUploadContext, collectionSlug, filename, mimeType, size } = JSON.parse(fields.file));
50
+ } catch {
51
+ throw new APIError('A file name is required.', 400);
52
+ }
47
53
  const uploadConfig = req.payload.collections[collectionSlug].config.upload;
48
54
  if (!uploadConfig.handlers) {
49
55
  throw new APIError('uploadConfig.handlers is not present for ' + collectionSlug);