payload 3.87.1 → 3.88.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 (41) hide show
  1. package/dist/collections/operations/find.d.ts.map +1 -1
  2. package/dist/collections/operations/find.js +8 -1
  3. package/dist/collections/operations/find.js.map +1 -1
  4. package/dist/collections/operations/findDistinct.d.ts.map +1 -1
  5. package/dist/collections/operations/findDistinct.js +7 -0
  6. package/dist/collections/operations/findDistinct.js.map +1 -1
  7. package/dist/collections/operations/findVersions.d.ts.map +1 -1
  8. package/dist/collections/operations/findVersions.js +8 -0
  9. package/dist/collections/operations/findVersions.js.map +1 -1
  10. package/dist/collections/operations/update.d.ts.map +1 -1
  11. package/dist/collections/operations/update.js +8 -1
  12. package/dist/collections/operations/update.js.map +1 -1
  13. package/dist/database/queryValidation/validateQueryPaths.d.ts.map +1 -1
  14. package/dist/database/queryValidation/validateQueryPaths.js +8 -1
  15. package/dist/database/queryValidation/validateQueryPaths.js.map +1 -1
  16. package/dist/database/queryValidation/validateSortQuery.d.ts +23 -0
  17. package/dist/database/queryValidation/validateSortQuery.d.ts.map +1 -0
  18. package/dist/database/queryValidation/validateSortQuery.js +54 -0
  19. package/dist/database/queryValidation/validateSortQuery.js.map +1 -0
  20. package/dist/database/sanitizeJoinQuery.d.ts.map +1 -1
  21. package/dist/database/sanitizeJoinQuery.js +6 -0
  22. package/dist/database/sanitizeJoinQuery.js.map +1 -1
  23. package/dist/globals/operations/findVersions.d.ts.map +1 -1
  24. package/dist/globals/operations/findVersions.js +8 -0
  25. package/dist/globals/operations/findVersions.js.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.js +1 -2
  29. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.js.map +1 -1
  30. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.spec.js +27 -0
  31. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.spec.js.map +1 -1
  32. package/dist/utilities/getSafeRedirect.d.ts.map +1 -1
  33. package/dist/utilities/getSafeRedirect.js +16 -9
  34. package/dist/utilities/getSafeRedirect.js.map +1 -1
  35. package/dist/utilities/getSafeRedirect.spec.js +76 -2
  36. package/dist/utilities/getSafeRedirect.spec.js.map +1 -1
  37. package/dist/versions/drafts/appendVersionToQueryKey.js +1 -3
  38. package/dist/versions/drafts/appendVersionToQueryKey.js.map +1 -1
  39. package/dist/versions/drafts/appendVersionToQueryKey.spec.js +28 -0
  40. package/dist/versions/drafts/appendVersionToQueryKey.spec.js.map +1 -0
  41. package/package.json +2 -2
@@ -2,23 +2,30 @@ export const getSafeRedirect = ({ allowAbsoluteUrls = false, fallbackTo = '/', r
2
2
  if (typeof redirectTo !== 'string') {
3
3
  return fallbackTo;
4
4
  }
5
- // Normalize and decode the path
6
- let redirectPath;
5
+ const redirectPath = redirectTo.trim();
6
+ const hasControlCharacters = [
7
+ ...redirectPath
8
+ ].some((character)=>{
9
+ const code = character.charCodeAt(0);
10
+ return code <= 31 || code === 127;
11
+ });
12
+ const hasAmbiguousPathPrefix = /^\/%(?:25)*(?:09|0a|0d|2f|5c)/i.test(redirectPath);
13
+ let parsedRedirect;
7
14
  try {
8
- redirectPath = decodeURIComponent(redirectTo.trim());
15
+ parsedRedirect = new URL(redirectPath, 'http://localhost');
9
16
  } catch {
10
- return fallbackTo // invalid encoding
11
- ;
17
+ return fallbackTo;
12
18
  }
13
- const isSafeRedirect = // Must start with a single forward slash (e.g., "/admin")
14
- redirectPath.startsWith('/') && // Prevent protocol-relative URLs (e.g., "//example.com")
19
+ const isSafeRedirect = !hasControlCharacters && !hasAmbiguousPathPrefix && // Must start with a single forward slash (e.g., "/admin")
20
+ redirectPath.startsWith('/') && // Must resolve to the same origin after URL parser normalization
21
+ parsedRedirect.origin === 'http://localhost' && // Prevent protocol-relative URLs (e.g., "//example.com")
15
22
  !redirectPath.startsWith('//') && // Prevent encoded slashes that could resolve to protocol-relative
16
23
  !redirectPath.startsWith('/%2F') && // Prevent backslash-based escape attempts (e.g., "/\\/example.com", "/\\\\example.com", "/\\example.com")
17
24
  !redirectPath.startsWith('/\\/') && !redirectPath.startsWith('/\\\\') && !redirectPath.startsWith('/\\') && // Prevent javascript-based schemes (e.g., "/javascript:alert(1)")
18
25
  !redirectPath.toLowerCase().startsWith('/javascript:') && // Prevent attempts to redirect to full URLs using "/http:" or "/https:"
19
26
  !redirectPath.toLowerCase().startsWith('/http');
20
- const isAbsoluteSafeRedirect = allowAbsoluteUrls && // Must be a valid absolute URL with http or https
21
- /^https?:\/\/\S+$/i.test(redirectPath);
27
+ const isAbsoluteSafeRedirect = allowAbsoluteUrls && !hasControlCharacters && // Must be a valid absolute URL with http or https
28
+ /^https?:\/\/\S+$/i.test(redirectPath) && (parsedRedirect.protocol === 'http:' || parsedRedirect.protocol === 'https:');
22
29
  return isSafeRedirect || isAbsoluteSafeRedirect ? redirectPath : fallbackTo;
23
30
  };
24
31
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/getSafeRedirect.ts"],"sourcesContent":["export const getSafeRedirect = ({\n allowAbsoluteUrls = false,\n fallbackTo = '/',\n redirectTo,\n}: {\n allowAbsoluteUrls?: boolean\n fallbackTo?: string\n redirectTo: string | string[]\n}): string => {\n if (typeof redirectTo !== 'string') {\n return fallbackTo\n }\n\n // Normalize and decode the path\n let redirectPath: string\n try {\n redirectPath = decodeURIComponent(redirectTo.trim())\n } catch {\n return fallbackTo // invalid encoding\n }\n\n const isSafeRedirect =\n // Must start with a single forward slash (e.g., \"/admin\")\n redirectPath.startsWith('/') &&\n // Prevent protocol-relative URLs (e.g., \"//example.com\")\n !redirectPath.startsWith('//') &&\n // Prevent encoded slashes that could resolve to protocol-relative\n !redirectPath.startsWith('/%2F') &&\n // Prevent backslash-based escape attempts (e.g., \"/\\\\/example.com\", \"/\\\\\\\\example.com\", \"/\\\\example.com\")\n !redirectPath.startsWith('/\\\\/') &&\n !redirectPath.startsWith('/\\\\\\\\') &&\n !redirectPath.startsWith('/\\\\') &&\n // Prevent javascript-based schemes (e.g., \"/javascript:alert(1)\")\n !redirectPath.toLowerCase().startsWith('/javascript:') &&\n // Prevent attempts to redirect to full URLs using \"/http:\" or \"/https:\"\n !redirectPath.toLowerCase().startsWith('/http')\n\n const isAbsoluteSafeRedirect =\n allowAbsoluteUrls &&\n // Must be a valid absolute URL with http or https\n /^https?:\\/\\/\\S+$/i.test(redirectPath)\n\n return isSafeRedirect || isAbsoluteSafeRedirect ? redirectPath : fallbackTo\n}\n"],"names":["getSafeRedirect","allowAbsoluteUrls","fallbackTo","redirectTo","redirectPath","decodeURIComponent","trim","isSafeRedirect","startsWith","toLowerCase","isAbsoluteSafeRedirect","test"],"mappings":"AAAA,OAAO,MAAMA,kBAAkB,CAAC,EAC9BC,oBAAoB,KAAK,EACzBC,aAAa,GAAG,EAChBC,UAAU,EAKX;IACC,IAAI,OAAOA,eAAe,UAAU;QAClC,OAAOD;IACT;IAEA,gCAAgC;IAChC,IAAIE;IACJ,IAAI;QACFA,eAAeC,mBAAmBF,WAAWG,IAAI;IACnD,EAAE,OAAM;QACN,OAAOJ,WAAW,mBAAmB;;IACvC;IAEA,MAAMK,iBACJ,0DAA0D;IAC1DH,aAAaI,UAAU,CAAC,QACxB,yDAAyD;IACzD,CAACJ,aAAaI,UAAU,CAAC,SACzB,kEAAkE;IAClE,CAACJ,aAAaI,UAAU,CAAC,WACzB,0GAA0G;IAC1G,CAACJ,aAAaI,UAAU,CAAC,WACzB,CAACJ,aAAaI,UAAU,CAAC,YACzB,CAACJ,aAAaI,UAAU,CAAC,UACzB,kEAAkE;IAClE,CAACJ,aAAaK,WAAW,GAAGD,UAAU,CAAC,mBACvC,wEAAwE;IACxE,CAACJ,aAAaK,WAAW,GAAGD,UAAU,CAAC;IAEzC,MAAME,yBACJT,qBACA,kDAAkD;IAClD,oBAAoBU,IAAI,CAACP;IAE3B,OAAOG,kBAAkBG,yBAAyBN,eAAeF;AACnE,EAAC"}
1
+ {"version":3,"sources":["../../src/utilities/getSafeRedirect.ts"],"sourcesContent":["export const getSafeRedirect = ({\n allowAbsoluteUrls = false,\n fallbackTo = '/',\n redirectTo,\n}: {\n allowAbsoluteUrls?: boolean\n fallbackTo?: string\n redirectTo: string | string[]\n}): string => {\n if (typeof redirectTo !== 'string') {\n return fallbackTo\n }\n\n const redirectPath = redirectTo.trim()\n\n const hasControlCharacters = [...redirectPath].some((character) => {\n const code = character.charCodeAt(0)\n return code <= 31 || code === 127\n })\n const hasAmbiguousPathPrefix = /^\\/%(?:25)*(?:09|0a|0d|2f|5c)/i.test(redirectPath)\n\n let parsedRedirect: URL\n try {\n parsedRedirect = new URL(redirectPath, 'http://localhost')\n } catch {\n return fallbackTo\n }\n\n const isSafeRedirect =\n !hasControlCharacters &&\n !hasAmbiguousPathPrefix &&\n // Must start with a single forward slash (e.g., \"/admin\")\n redirectPath.startsWith('/') &&\n // Must resolve to the same origin after URL parser normalization\n parsedRedirect.origin === 'http://localhost' &&\n // Prevent protocol-relative URLs (e.g., \"//example.com\")\n !redirectPath.startsWith('//') &&\n // Prevent encoded slashes that could resolve to protocol-relative\n !redirectPath.startsWith('/%2F') &&\n // Prevent backslash-based escape attempts (e.g., \"/\\\\/example.com\", \"/\\\\\\\\example.com\", \"/\\\\example.com\")\n !redirectPath.startsWith('/\\\\/') &&\n !redirectPath.startsWith('/\\\\\\\\') &&\n !redirectPath.startsWith('/\\\\') &&\n // Prevent javascript-based schemes (e.g., \"/javascript:alert(1)\")\n !redirectPath.toLowerCase().startsWith('/javascript:') &&\n // Prevent attempts to redirect to full URLs using \"/http:\" or \"/https:\"\n !redirectPath.toLowerCase().startsWith('/http')\n\n const isAbsoluteSafeRedirect =\n allowAbsoluteUrls &&\n !hasControlCharacters &&\n // Must be a valid absolute URL with http or https\n /^https?:\\/\\/\\S+$/i.test(redirectPath) &&\n (parsedRedirect.protocol === 'http:' || parsedRedirect.protocol === 'https:')\n\n return isSafeRedirect || isAbsoluteSafeRedirect ? redirectPath : fallbackTo\n}\n"],"names":["getSafeRedirect","allowAbsoluteUrls","fallbackTo","redirectTo","redirectPath","trim","hasControlCharacters","some","character","code","charCodeAt","hasAmbiguousPathPrefix","test","parsedRedirect","URL","isSafeRedirect","startsWith","origin","toLowerCase","isAbsoluteSafeRedirect","protocol"],"mappings":"AAAA,OAAO,MAAMA,kBAAkB,CAAC,EAC9BC,oBAAoB,KAAK,EACzBC,aAAa,GAAG,EAChBC,UAAU,EAKX;IACC,IAAI,OAAOA,eAAe,UAAU;QAClC,OAAOD;IACT;IAEA,MAAME,eAAeD,WAAWE,IAAI;IAEpC,MAAMC,uBAAuB;WAAIF;KAAa,CAACG,IAAI,CAAC,CAACC;QACnD,MAAMC,OAAOD,UAAUE,UAAU,CAAC;QAClC,OAAOD,QAAQ,MAAMA,SAAS;IAChC;IACA,MAAME,yBAAyB,iCAAiCC,IAAI,CAACR;IAErE,IAAIS;IACJ,IAAI;QACFA,iBAAiB,IAAIC,IAAIV,cAAc;IACzC,EAAE,OAAM;QACN,OAAOF;IACT;IAEA,MAAMa,iBACJ,CAACT,wBACD,CAACK,0BACD,0DAA0D;IAC1DP,aAAaY,UAAU,CAAC,QACxB,iEAAiE;IACjEH,eAAeI,MAAM,KAAK,sBAC1B,yDAAyD;IACzD,CAACb,aAAaY,UAAU,CAAC,SACzB,kEAAkE;IAClE,CAACZ,aAAaY,UAAU,CAAC,WACzB,0GAA0G;IAC1G,CAACZ,aAAaY,UAAU,CAAC,WACzB,CAACZ,aAAaY,UAAU,CAAC,YACzB,CAACZ,aAAaY,UAAU,CAAC,UACzB,kEAAkE;IAClE,CAACZ,aAAac,WAAW,GAAGF,UAAU,CAAC,mBACvC,wEAAwE;IACxE,CAACZ,aAAac,WAAW,GAAGF,UAAU,CAAC;IAEzC,MAAMG,yBACJlB,qBACA,CAACK,wBACD,kDAAkD;IAClD,oBAAoBM,IAAI,CAACR,iBACxBS,CAAAA,eAAeO,QAAQ,KAAK,WAAWP,eAAeO,QAAQ,KAAK,QAAO;IAE7E,OAAOL,kBAAkBI,yBAAyBf,eAAeF;AACnE,EAAC"}
@@ -39,6 +39,30 @@ describe('getSafeRedirect', ()=>{
39
39
  fallbackTo: fallback
40
40
  })).toBe(fallback);
41
41
  });
42
+ it.each([
43
+ 'redirect=%2F%09%2Fexample.invalid',
44
+ 'redirect=%2F%0D%2Fexample.invalid',
45
+ 'redirect=%2F%0A%2Fexample.invalid'
46
+ ])('should use the fallback when a path resolves outside the current origin: %s', (query)=>{
47
+ const redirectTo = new URLSearchParams(query).get('redirect');
48
+ expect(redirectTo).not.toBeNull();
49
+ expect(getSafeRedirect({
50
+ redirectTo: redirectTo,
51
+ fallbackTo: fallback
52
+ })).toBe(fallback);
53
+ });
54
+ it.each([
55
+ '/%2509/example.invalid',
56
+ '/%250D/example.invalid',
57
+ '/%250A/example.invalid',
58
+ '/%255Cexample.invalid',
59
+ '/%252fexample.invalid'
60
+ ])('should use the fallback for ambiguous encoded path prefixes: %s', (input)=>{
61
+ expect(getSafeRedirect({
62
+ redirectTo: input,
63
+ fallbackTo: fallback
64
+ })).toBe(fallback);
65
+ });
42
66
  // Unsafe redirect patterns
43
67
  it.each([
44
68
  '//example.com',
@@ -72,13 +96,63 @@ describe('getSafeRedirect', ()=>{
72
96
  fallbackTo: fallback
73
97
  })).toBe(fallback);
74
98
  });
75
- // If decoding the input fails (e.g., invalid percent encoding), it should not crash
76
- it('should return fallback on invalid encoding', ()=>{
99
+ it('should return fallback when the input is not a path or URL', ()=>{
77
100
  expect(getSafeRedirect({
78
101
  redirectTo: '%E0%A4%A',
79
102
  fallbackTo: fallback
80
103
  })).toBe(fallback);
81
104
  });
105
+ it('should preserve an accepted local redirect', ()=>{
106
+ const redirectTo = '/dashboard?tab=overview#details';
107
+ expect(getSafeRedirect({
108
+ redirectTo,
109
+ fallbackTo: fallback
110
+ })).toBe(redirectTo);
111
+ });
112
+ it('should preserve a parsed navigation target byte-for-byte', ()=>{
113
+ const redirectTo = new URLSearchParams('redirect=%2Foauth%2Fcallback%3Fcode%3DA%252FB%26state%3Dopaque%253D%23done').get('redirect');
114
+ expect(redirectTo).toBe('/oauth/callback?code=A%2FB&state=opaque%3D#done');
115
+ expect(getSafeRedirect({
116
+ redirectTo: redirectTo,
117
+ fallbackTo: fallback
118
+ })).toBe('/oauth/callback?code=A%2FB&state=opaque%3D#done');
119
+ });
120
+ it.each([
121
+ [
122
+ 'https://example.invalid/path?code=A%252FB#done',
123
+ 'https://example.invalid/path?code=A%252FB#done'
124
+ ],
125
+ [
126
+ 'http://example.invalid/dashboard',
127
+ 'http://example.invalid/dashboard'
128
+ ]
129
+ ])('should preserve an HTTP absolute redirect when enabled: %s', (input, expected)=>{
130
+ expect(getSafeRedirect({
131
+ allowAbsoluteUrls: true,
132
+ redirectTo: input,
133
+ fallbackTo: fallback
134
+ })).toBe(expected);
135
+ });
136
+ it.each([
137
+ [
138
+ 'https://example.invalid/path',
139
+ false
140
+ ],
141
+ [
142
+ 'mailto:user@example.invalid',
143
+ true
144
+ ],
145
+ [
146
+ '//example.invalid/path',
147
+ true
148
+ ]
149
+ ])('should use the fallback without an explicitly enabled HTTP(S) URL: %s', (input, allowAbsoluteUrls)=>{
150
+ expect(getSafeRedirect({
151
+ allowAbsoluteUrls,
152
+ redirectTo: input,
153
+ fallbackTo: fallback
154
+ })).toBe(fallback);
155
+ });
82
156
  });
83
157
 
84
158
  //# sourceMappingURL=getSafeRedirect.spec.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/getSafeRedirect.spec.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\nimport { getSafeRedirect } from './getSafeRedirect'\n\nconst fallback = '/admin' // default fallback if the input is unsafe or invalid\n\ndescribe('getSafeRedirect', () => {\n // Valid - safe redirect paths\n it.each([['/dashboard'], ['/admin/settings'], ['/projects?id=123'], ['/hello-world']])(\n 'should allow safe relative path: %s',\n (input) => {\n // If the input is a clean relative path, it should be returned as-is\n expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(input)\n },\n )\n\n // Invalid types or empty inputs\n it.each(['', null, undefined, 123, {}, []])(\n 'should fallback on invalid or non-string input: %s',\n (input) => {\n // If the input is not a valid string, it should return the fallback\n expect(getSafeRedirect({ redirectTo: input as any, fallbackTo: fallback })).toBe(fallback)\n },\n )\n\n // Unsafe redirect patterns\n it.each([\n '//example.com', // protocol-relative URL\n '/javascript:alert(1)', // JavaScript scheme\n '/JavaScript:alert(1)', // case-insensitive JavaScript\n '/http://unknown.com', // disguised external redirect\n '/https://unknown.com', // disguised external redirect\n '/%2Funknown.com', // encoded slash — could resolve to //\n '/\\\\/unknown.com', // escaped slash\n '/\\\\\\\\unknown.com', // double escaped slashes\n '/\\\\unknown.com', // single escaped slash\n '%2F%2Funknown.com', // fully encoded protocol-relative path\n '%2Fjavascript:alert(1)', // encoded JavaScript scheme\n ])('should block unsafe redirect: %s', (input) => {\n // All of these should return the fallback because they’re unsafe\n expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(fallback)\n })\n\n // Input with extra spaces should still be properly handled\n it('should trim whitespace before evaluating', () => {\n // A valid path with surrounding spaces should still be accepted\n expect(getSafeRedirect({ redirectTo: ' /dashboard ', fallbackTo: fallback })).toBe(\n '/dashboard',\n )\n\n // An unsafe path with spaces should still be rejected\n expect(getSafeRedirect({ redirectTo: ' //example.com ', fallbackTo: fallback })).toBe(\n fallback,\n )\n })\n\n // If decoding the input fails (e.g., invalid percent encoding), it should not crash\n it('should return fallback on invalid encoding', () => {\n expect(getSafeRedirect({ redirectTo: '%E0%A4%A', fallbackTo: fallback })).toBe(fallback)\n })\n})\n"],"names":["describe","it","expect","getSafeRedirect","fallback","each","input","redirectTo","fallbackTo","toBe","undefined"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,EAAE,EAAEC,MAAM,QAAQ,SAAQ;AAC7C,SAASC,eAAe,QAAQ,oBAAmB;AAEnD,MAAMC,WAAW,SAAS,qDAAqD;;AAE/EJ,SAAS,mBAAmB;IAC1B,8BAA8B;IAC9BC,GAAGI,IAAI,CAAC;QAAC;YAAC;SAAa;QAAE;YAAC;SAAkB;QAAE;YAAC;SAAmB;QAAE;YAAC;SAAe;KAAC,EACnF,uCACA,CAACC;QACC,qEAAqE;QACrEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAOE,YAAYJ;QAAS,IAAIK,IAAI,CAACH;IAC5E;IAGF,gCAAgC;IAChCL,GAAGI,IAAI,CAAC;QAAC;QAAI;QAAMK;QAAW;QAAK,CAAC;QAAG,EAAE;KAAC,EACxC,sDACA,CAACJ;QACC,oEAAoE;QACpEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAcE,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IACnF;IAGF,2BAA2B;IAC3BH,GAAGI,IAAI,CAAC;QACN;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD,EAAE,oCAAoC,CAACC;QACtC,iEAAiE;QACjEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAOE,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IAC5E;IAEA,2DAA2D;IAC3DH,GAAG,4CAA4C;QAC7C,gEAAgE;QAChEC,OAAOC,gBAAgB;YAAEI,YAAY;YAAoBC,YAAYJ;QAAS,IAAIK,IAAI,CACpF;QAGF,sDAAsD;QACtDP,OAAOC,gBAAgB;YAAEI,YAAY;YAAuBC,YAAYJ;QAAS,IAAIK,IAAI,CACvFL;IAEJ;IAEA,oFAAoF;IACpFH,GAAG,8CAA8C;QAC/CC,OAAOC,gBAAgB;YAAEI,YAAY;YAAYC,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IACjF;AACF"}
1
+ {"version":3,"sources":["../../src/utilities/getSafeRedirect.spec.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\nimport { getSafeRedirect } from './getSafeRedirect'\n\nconst fallback = '/admin' // default fallback if the input is unsafe or invalid\n\ndescribe('getSafeRedirect', () => {\n // Valid - safe redirect paths\n it.each([['/dashboard'], ['/admin/settings'], ['/projects?id=123'], ['/hello-world']])(\n 'should allow safe relative path: %s',\n (input) => {\n // If the input is a clean relative path, it should be returned as-is\n expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(input)\n },\n )\n\n // Invalid types or empty inputs\n it.each(['', null, undefined, 123, {}, []])(\n 'should fallback on invalid or non-string input: %s',\n (input) => {\n // If the input is not a valid string, it should return the fallback\n expect(getSafeRedirect({ redirectTo: input as any, fallbackTo: fallback })).toBe(fallback)\n },\n )\n\n it.each([\n 'redirect=%2F%09%2Fexample.invalid',\n 'redirect=%2F%0D%2Fexample.invalid',\n 'redirect=%2F%0A%2Fexample.invalid',\n ])('should use the fallback when a path resolves outside the current origin: %s', (query) => {\n const redirectTo = new URLSearchParams(query).get('redirect')\n\n expect(redirectTo).not.toBeNull()\n expect(getSafeRedirect({ redirectTo: redirectTo!, fallbackTo: fallback })).toBe(fallback)\n })\n\n it.each([\n '/%2509/example.invalid',\n '/%250D/example.invalid',\n '/%250A/example.invalid',\n '/%255Cexample.invalid',\n '/%252fexample.invalid',\n ])('should use the fallback for ambiguous encoded path prefixes: %s', (input) => {\n expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(fallback)\n })\n\n // Unsafe redirect patterns\n it.each([\n '//example.com', // protocol-relative URL\n '/javascript:alert(1)', // JavaScript scheme\n '/JavaScript:alert(1)', // case-insensitive JavaScript\n '/http://unknown.com', // disguised external redirect\n '/https://unknown.com', // disguised external redirect\n '/%2Funknown.com', // encoded slash — could resolve to //\n '/\\\\/unknown.com', // escaped slash\n '/\\\\\\\\unknown.com', // double escaped slashes\n '/\\\\unknown.com', // single escaped slash\n '%2F%2Funknown.com', // fully encoded protocol-relative path\n '%2Fjavascript:alert(1)', // encoded JavaScript scheme\n ])('should block unsafe redirect: %s', (input) => {\n // All of these should return the fallback because they’re unsafe\n expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(fallback)\n })\n\n // Input with extra spaces should still be properly handled\n it('should trim whitespace before evaluating', () => {\n // A valid path with surrounding spaces should still be accepted\n expect(getSafeRedirect({ redirectTo: ' /dashboard ', fallbackTo: fallback })).toBe(\n '/dashboard',\n )\n\n // An unsafe path with spaces should still be rejected\n expect(getSafeRedirect({ redirectTo: ' //example.com ', fallbackTo: fallback })).toBe(\n fallback,\n )\n })\n\n it('should return fallback when the input is not a path or URL', () => {\n expect(getSafeRedirect({ redirectTo: '%E0%A4%A', fallbackTo: fallback })).toBe(fallback)\n })\n\n it('should preserve an accepted local redirect', () => {\n const redirectTo = '/dashboard?tab=overview#details'\n\n expect(getSafeRedirect({ redirectTo, fallbackTo: fallback })).toBe(redirectTo)\n })\n\n it('should preserve a parsed navigation target byte-for-byte', () => {\n const redirectTo = new URLSearchParams(\n 'redirect=%2Foauth%2Fcallback%3Fcode%3DA%252FB%26state%3Dopaque%253D%23done',\n ).get('redirect')\n\n expect(redirectTo).toBe('/oauth/callback?code=A%2FB&state=opaque%3D#done')\n expect(getSafeRedirect({ redirectTo: redirectTo!, fallbackTo: fallback })).toBe(\n '/oauth/callback?code=A%2FB&state=opaque%3D#done',\n )\n })\n\n it.each([\n [\n 'https://example.invalid/path?code=A%252FB#done',\n 'https://example.invalid/path?code=A%252FB#done',\n ],\n ['http://example.invalid/dashboard', 'http://example.invalid/dashboard'],\n ])('should preserve an HTTP absolute redirect when enabled: %s', (input, expected) => {\n expect(\n getSafeRedirect({\n allowAbsoluteUrls: true,\n redirectTo: input,\n fallbackTo: fallback,\n }),\n ).toBe(expected)\n })\n\n it.each([\n ['https://example.invalid/path', false],\n ['mailto:user@example.invalid', true],\n ['//example.invalid/path', true],\n ])(\n 'should use the fallback without an explicitly enabled HTTP(S) URL: %s',\n (input, allowAbsoluteUrls) => {\n expect(getSafeRedirect({ allowAbsoluteUrls, redirectTo: input, fallbackTo: fallback })).toBe(\n fallback,\n )\n },\n )\n})\n"],"names":["describe","it","expect","getSafeRedirect","fallback","each","input","redirectTo","fallbackTo","toBe","undefined","query","URLSearchParams","get","not","toBeNull","expected","allowAbsoluteUrls"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,EAAE,EAAEC,MAAM,QAAQ,SAAQ;AAC7C,SAASC,eAAe,QAAQ,oBAAmB;AAEnD,MAAMC,WAAW,SAAS,qDAAqD;;AAE/EJ,SAAS,mBAAmB;IAC1B,8BAA8B;IAC9BC,GAAGI,IAAI,CAAC;QAAC;YAAC;SAAa;QAAE;YAAC;SAAkB;QAAE;YAAC;SAAmB;QAAE;YAAC;SAAe;KAAC,EACnF,uCACA,CAACC;QACC,qEAAqE;QACrEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAOE,YAAYJ;QAAS,IAAIK,IAAI,CAACH;IAC5E;IAGF,gCAAgC;IAChCL,GAAGI,IAAI,CAAC;QAAC;QAAI;QAAMK;QAAW;QAAK,CAAC;QAAG,EAAE;KAAC,EACxC,sDACA,CAACJ;QACC,oEAAoE;QACpEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAcE,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IACnF;IAGFH,GAAGI,IAAI,CAAC;QACN;QACA;QACA;KACD,EAAE,+EAA+E,CAACM;QACjF,MAAMJ,aAAa,IAAIK,gBAAgBD,OAAOE,GAAG,CAAC;QAElDX,OAAOK,YAAYO,GAAG,CAACC,QAAQ;QAC/Bb,OAAOC,gBAAgB;YAAEI,YAAYA;YAAaC,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IAClF;IAEAH,GAAGI,IAAI,CAAC;QACN;QACA;QACA;QACA;QACA;KACD,EAAE,mEAAmE,CAACC;QACrEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAOE,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IAC5E;IAEA,2BAA2B;IAC3BH,GAAGI,IAAI,CAAC;QACN;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD,EAAE,oCAAoC,CAACC;QACtC,iEAAiE;QACjEJ,OAAOC,gBAAgB;YAAEI,YAAYD;YAAOE,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IAC5E;IAEA,2DAA2D;IAC3DH,GAAG,4CAA4C;QAC7C,gEAAgE;QAChEC,OAAOC,gBAAgB;YAAEI,YAAY;YAAoBC,YAAYJ;QAAS,IAAIK,IAAI,CACpF;QAGF,sDAAsD;QACtDP,OAAOC,gBAAgB;YAAEI,YAAY;YAAuBC,YAAYJ;QAAS,IAAIK,IAAI,CACvFL;IAEJ;IAEAH,GAAG,8DAA8D;QAC/DC,OAAOC,gBAAgB;YAAEI,YAAY;YAAYC,YAAYJ;QAAS,IAAIK,IAAI,CAACL;IACjF;IAEAH,GAAG,8CAA8C;QAC/C,MAAMM,aAAa;QAEnBL,OAAOC,gBAAgB;YAAEI;YAAYC,YAAYJ;QAAS,IAAIK,IAAI,CAACF;IACrE;IAEAN,GAAG,4DAA4D;QAC7D,MAAMM,aAAa,IAAIK,gBACrB,8EACAC,GAAG,CAAC;QAENX,OAAOK,YAAYE,IAAI,CAAC;QACxBP,OAAOC,gBAAgB;YAAEI,YAAYA;YAAaC,YAAYJ;QAAS,IAAIK,IAAI,CAC7E;IAEJ;IAEAR,GAAGI,IAAI,CAAC;QACN;YACE;YACA;SACD;QACD;YAAC;YAAoC;SAAmC;KACzE,EAAE,8DAA8D,CAACC,OAAOU;QACvEd,OACEC,gBAAgB;YACdc,mBAAmB;YACnBV,YAAYD;YACZE,YAAYJ;QACd,IACAK,IAAI,CAACO;IACT;IAEAf,GAAGI,IAAI,CAAC;QACN;YAAC;YAAgC;SAAM;QACvC;YAAC;YAA+B;SAAK;QACrC;YAAC;YAA0B;SAAK;KACjC,EACC,yEACA,CAACC,OAAOW;QACNf,OAAOC,gBAAgB;YAAEc;YAAmBV,YAAYD;YAAOE,YAAYJ;QAAS,IAAIK,IAAI,CAC1FL;IAEJ;AAEJ"}
@@ -1,11 +1,9 @@
1
1
  export const appendVersionToQueryKey = (query = {})=>{
2
2
  return Object.entries(query).reduce((res, [key, val])=>{
3
3
  if ([
4
- 'AND',
5
4
  'and',
6
- 'OR',
7
5
  'or'
8
- ].includes(key) && Array.isArray(val)) {
6
+ ].includes(key.toLowerCase()) && Array.isArray(val)) {
9
7
  return {
10
8
  ...res,
11
9
  [key.toLowerCase()]: val.map((subQuery)=>appendVersionToQueryKey(subQuery))
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/versions/drafts/appendVersionToQueryKey.ts"],"sourcesContent":["import type { Where } from '../../types/index.js'\n\nexport const appendVersionToQueryKey = (query: Where = {}): Where => {\n return Object.entries(query).reduce((res, [key, val]) => {\n if (['AND', 'and', 'OR', 'or'].includes(key) && Array.isArray(val)) {\n return {\n ...res,\n [key.toLowerCase()]: val.map((subQuery) => appendVersionToQueryKey(subQuery)),\n }\n }\n\n if (key !== 'id') {\n return {\n ...res,\n [`version.${key}`]: val,\n }\n }\n\n return {\n ...res,\n parent: val,\n }\n }, {})\n}\n"],"names":["appendVersionToQueryKey","query","Object","entries","reduce","res","key","val","includes","Array","isArray","toLowerCase","map","subQuery","parent"],"mappings":"AAEA,OAAO,MAAMA,0BAA0B,CAACC,QAAe,CAAC,CAAC;IACvD,OAAOC,OAAOC,OAAO,CAACF,OAAOG,MAAM,CAAC,CAACC,KAAK,CAACC,KAAKC,IAAI;QAClD,IAAI;YAAC;YAAO;YAAO;YAAM;SAAK,CAACC,QAAQ,CAACF,QAAQG,MAAMC,OAAO,CAACH,MAAM;YAClE,OAAO;gBACL,GAAGF,GAAG;gBACN,CAACC,IAAIK,WAAW,GAAG,EAAEJ,IAAIK,GAAG,CAAC,CAACC,WAAab,wBAAwBa;YACrE;QACF;QAEA,IAAIP,QAAQ,MAAM;YAChB,OAAO;gBACL,GAAGD,GAAG;gBACN,CAAC,CAAC,QAAQ,EAAEC,KAAK,CAAC,EAAEC;YACtB;QACF;QAEA,OAAO;YACL,GAAGF,GAAG;YACNS,QAAQP;QACV;IACF,GAAG,CAAC;AACN,EAAC"}
1
+ {"version":3,"sources":["../../../src/versions/drafts/appendVersionToQueryKey.ts"],"sourcesContent":["import type { Where } from '../../types/index.js'\n\nexport const appendVersionToQueryKey = (query: Where = {}): Where => {\n return Object.entries(query).reduce((res, [key, val]) => {\n if (['and', 'or'].includes(key.toLowerCase()) && Array.isArray(val)) {\n return {\n ...res,\n [key.toLowerCase()]: val.map((subQuery) => appendVersionToQueryKey(subQuery)),\n }\n }\n\n if (key !== 'id') {\n return {\n ...res,\n [`version.${key}`]: val,\n }\n }\n\n return {\n ...res,\n parent: val,\n }\n }, {})\n}\n"],"names":["appendVersionToQueryKey","query","Object","entries","reduce","res","key","val","includes","toLowerCase","Array","isArray","map","subQuery","parent"],"mappings":"AAEA,OAAO,MAAMA,0BAA0B,CAACC,QAAe,CAAC,CAAC;IACvD,OAAOC,OAAOC,OAAO,CAACF,OAAOG,MAAM,CAAC,CAACC,KAAK,CAACC,KAAKC,IAAI;QAClD,IAAI;YAAC;YAAO;SAAK,CAACC,QAAQ,CAACF,IAAIG,WAAW,OAAOC,MAAMC,OAAO,CAACJ,MAAM;YACnE,OAAO;gBACL,GAAGF,GAAG;gBACN,CAACC,IAAIG,WAAW,GAAG,EAAEF,IAAIK,GAAG,CAAC,CAACC,WAAab,wBAAwBa;YACrE;QACF;QAEA,IAAIP,QAAQ,MAAM;YAChB,OAAO;gBACL,GAAGD,GAAG;gBACN,CAAC,CAAC,QAAQ,EAAEC,KAAK,CAAC,EAAEC;YACtB;QACF;QAEA,OAAO;YACL,GAAGF,GAAG;YACNS,QAAQP;QACV;IACF,GAAG,CAAC;AACN,EAAC"}
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { appendVersionToQueryKey } from './appendVersionToQueryKey.js';
3
+ describe('appendVersionToQueryKey', ()=>{
4
+ it.each([
5
+ 'aNd',
6
+ 'oR'
7
+ ])('should preserve case-insensitive %s conditions when prefixing version fields', (logicalOperator)=>{
8
+ expect(appendVersionToQueryKey({
9
+ [logicalOperator]: [
10
+ {
11
+ title: {
12
+ equals: 'example'
13
+ }
14
+ }
15
+ ]
16
+ })).toStrictEqual({
17
+ [logicalOperator.toLowerCase()]: [
18
+ {
19
+ 'version.title': {
20
+ equals: 'example'
21
+ }
22
+ }
23
+ ]
24
+ });
25
+ });
26
+ });
27
+
28
+ //# sourceMappingURL=appendVersionToQueryKey.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/versions/drafts/appendVersionToQueryKey.spec.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\n\nimport { appendVersionToQueryKey } from './appendVersionToQueryKey.js'\n\ndescribe('appendVersionToQueryKey', () => {\n it.each(['aNd', 'oR'])(\n 'should preserve case-insensitive %s conditions when prefixing version fields',\n (logicalOperator) => {\n expect(\n appendVersionToQueryKey({\n [logicalOperator]: [\n {\n title: {\n equals: 'example',\n },\n },\n ],\n }),\n ).toStrictEqual({\n [logicalOperator.toLowerCase()]: [\n {\n 'version.title': {\n equals: 'example',\n },\n },\n ],\n })\n },\n )\n})\n"],"names":["describe","expect","it","appendVersionToQueryKey","each","logicalOperator","title","equals","toStrictEqual","toLowerCase"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,uBAAuB,QAAQ,+BAA8B;AAEtEH,SAAS,2BAA2B;IAClCE,GAAGE,IAAI,CAAC;QAAC;QAAO;KAAK,EACnB,gFACA,CAACC;QACCJ,OACEE,wBAAwB;YACtB,CAACE,gBAAgB,EAAE;gBACjB;oBACEC,OAAO;wBACLC,QAAQ;oBACV;gBACF;aACD;QACH,IACAC,aAAa,CAAC;YACd,CAACH,gBAAgBI,WAAW,GAAG,EAAE;gBAC/B;oBACE,iBAAiB;wBACfF,QAAQ;oBACV;gBACF;aACD;QACH;IACF;AAEJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload",
3
- "version": "3.87.1",
3
+ "version": "3.88.0",
4
4
  "description": "Node, React, Headless CMS and Application Framework built on Next.js",
5
5
  "keywords": [
6
6
  "admin panel",
@@ -115,7 +115,7 @@
115
115
  "undici": "7.29.0",
116
116
  "uuid": "13.0.2",
117
117
  "ws": "^8.16.0",
118
- "@payloadcms/translations": "3.87.1"
118
+ "@payloadcms/translations": "3.88.0"
119
119
  },
120
120
  "devDependencies": {
121
121
  "@hyrious/esbuild-plugin-commonjs": "0.2.6",