payload 3.87.0 → 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 (58) 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.d.ts.map +1 -1
  27. package/dist/index.js +3 -8
  28. package/dist/index.js.map +1 -1
  29. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.js +1 -2
  30. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.js.map +1 -1
  31. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.spec.js +27 -0
  32. package/dist/uploads/fetchAPI-multipart/isEligibleRequest.spec.js.map +1 -1
  33. package/dist/utilities/getNextJsHMRURL.d.ts +6 -0
  34. package/dist/utilities/getNextJsHMRURL.d.ts.map +1 -0
  35. package/dist/utilities/getNextJsHMRURL.js +33 -0
  36. package/dist/utilities/getNextJsHMRURL.js.map +1 -0
  37. package/dist/utilities/getNextJsHMRURL.spec.js +60 -0
  38. package/dist/utilities/getNextJsHMRURL.spec.js.map +1 -0
  39. package/dist/utilities/getNextVersion.d.ts +7 -0
  40. package/dist/utilities/getNextVersion.d.ts.map +1 -0
  41. package/dist/utilities/getNextVersion.js +20 -0
  42. package/dist/utilities/getNextVersion.js.map +1 -0
  43. package/dist/utilities/getNextVersion.spec.js +16 -0
  44. package/dist/utilities/getNextVersion.spec.js.map +1 -0
  45. package/dist/utilities/getSafeRedirect.d.ts.map +1 -1
  46. package/dist/utilities/getSafeRedirect.js +16 -9
  47. package/dist/utilities/getSafeRedirect.js.map +1 -1
  48. package/dist/utilities/getSafeRedirect.spec.js +76 -2
  49. package/dist/utilities/getSafeRedirect.spec.js.map +1 -1
  50. package/dist/versions/drafts/appendVersionToQueryKey.js +1 -3
  51. package/dist/versions/drafts/appendVersionToQueryKey.js.map +1 -1
  52. package/dist/versions/drafts/appendVersionToQueryKey.spec.js +28 -0
  53. package/dist/versions/drafts/appendVersionToQueryKey.spec.js.map +1 -0
  54. package/package.json +3 -3
  55. package/dist/uploads/tempFile.d.ts +0 -7
  56. package/dist/uploads/tempFile.d.ts.map +0 -1
  57. package/dist/uploads/tempFile.js +0 -39
  58. package/dist/uploads/tempFile.js.map +0 -1
@@ -0,0 +1,33 @@
1
+ import { compareVersions, parseVersion } from './dependencies/versionUtils.js';
2
+ import { getNextVersion } from './getNextVersion.js';
3
+ /** Next.js serves the dev HMR WebSocket on this path from 16.3 onwards. */ const modernHMRPath = '/_next/hmr';
4
+ /** Next.js served the dev HMR WebSocket on this path before 16.3. */ const legacyHMRPath = '/_next/webpack-hmr';
5
+ const firstModernHMRPathVersion = '16.3.0';
6
+ /**
7
+ * Builds the URL of the Next.js dev HMR WebSocket, on the path served by the
8
+ * installed Next.js version. `PAYLOAD_HMR_URL_OVERRIDE` is used verbatim.
9
+ */ export const getNextJsHMRURL = ()=>{
10
+ if (process.env.PAYLOAD_HMR_URL_OVERRIDE) {
11
+ return process.env.PAYLOAD_HMR_URL_OVERRIDE;
12
+ }
13
+ const port = process.env.PORT || '3000';
14
+ const hasHTTPS = process.env.USE_HTTPS === 'true' || process.argv.includes('--experimental-https');
15
+ const protocol = hasHTTPS ? 'wss' : 'ws';
16
+ // The __NEXT_ASSET_PREFIX env variable is set for both assetPrefix and basePath (tested in Next.js 15.1.6)
17
+ const prefix = process.env.__NEXT_ASSET_PREFIX ?? '';
18
+ return `${protocol}://localhost:${port}${prefix}${getHMRPath()}`;
19
+ };
20
+ /**
21
+ * Pre-release identifiers are ignored, so that a `16.3.0-canary` build maps to the 16.3 path.
22
+ * An unreadable version uses the current path, as Payload being unable to resolve Next.js
23
+ * normally means it is not running under Next.js, where this strategy does not apply.
24
+ */ const getHMRPath = ()=>{
25
+ const nextVersion = getNextVersion();
26
+ if (!nextVersion) {
27
+ return modernHMRPath;
28
+ }
29
+ const mainVersion = parseVersion(nextVersion).parts.join('.');
30
+ return compareVersions(mainVersion, firstModernHMRPathVersion) === 'lower' ? legacyHMRPath : modernHMRPath;
31
+ };
32
+
33
+ //# sourceMappingURL=getNextJsHMRURL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/getNextJsHMRURL.ts"],"sourcesContent":["import { compareVersions, parseVersion } from './dependencies/versionUtils.js'\nimport { getNextVersion } from './getNextVersion.js'\n\n/** Next.js serves the dev HMR WebSocket on this path from 16.3 onwards. */\nconst modernHMRPath = '/_next/hmr'\n\n/** Next.js served the dev HMR WebSocket on this path before 16.3. */\nconst legacyHMRPath = '/_next/webpack-hmr'\n\nconst firstModernHMRPathVersion = '16.3.0'\n\n/**\n * Builds the URL of the Next.js dev HMR WebSocket, on the path served by the\n * installed Next.js version. `PAYLOAD_HMR_URL_OVERRIDE` is used verbatim.\n */\nexport const getNextJsHMRURL = (): string => {\n if (process.env.PAYLOAD_HMR_URL_OVERRIDE) {\n return process.env.PAYLOAD_HMR_URL_OVERRIDE\n }\n\n const port = process.env.PORT || '3000'\n const hasHTTPS = process.env.USE_HTTPS === 'true' || process.argv.includes('--experimental-https')\n const protocol = hasHTTPS ? 'wss' : 'ws'\n // The __NEXT_ASSET_PREFIX env variable is set for both assetPrefix and basePath (tested in Next.js 15.1.6)\n const prefix = process.env.__NEXT_ASSET_PREFIX ?? ''\n\n return `${protocol}://localhost:${port}${prefix}${getHMRPath()}`\n}\n\n/**\n * Pre-release identifiers are ignored, so that a `16.3.0-canary` build maps to the 16.3 path.\n * An unreadable version uses the current path, as Payload being unable to resolve Next.js\n * normally means it is not running under Next.js, where this strategy does not apply.\n */\nconst getHMRPath = (): string => {\n const nextVersion = getNextVersion()\n\n if (!nextVersion) {\n return modernHMRPath\n }\n\n const mainVersion = parseVersion(nextVersion).parts.join('.')\n\n return compareVersions(mainVersion, firstModernHMRPathVersion) === 'lower'\n ? legacyHMRPath\n : modernHMRPath\n}\n"],"names":["compareVersions","parseVersion","getNextVersion","modernHMRPath","legacyHMRPath","firstModernHMRPathVersion","getNextJsHMRURL","process","env","PAYLOAD_HMR_URL_OVERRIDE","port","PORT","hasHTTPS","USE_HTTPS","argv","includes","protocol","prefix","__NEXT_ASSET_PREFIX","getHMRPath","nextVersion","mainVersion","parts","join"],"mappings":"AAAA,SAASA,eAAe,EAAEC,YAAY,QAAQ,iCAAgC;AAC9E,SAASC,cAAc,QAAQ,sBAAqB;AAEpD,yEAAyE,GACzE,MAAMC,gBAAgB;AAEtB,mEAAmE,GACnE,MAAMC,gBAAgB;AAEtB,MAAMC,4BAA4B;AAElC;;;CAGC,GACD,OAAO,MAAMC,kBAAkB;IAC7B,IAAIC,QAAQC,GAAG,CAACC,wBAAwB,EAAE;QACxC,OAAOF,QAAQC,GAAG,CAACC,wBAAwB;IAC7C;IAEA,MAAMC,OAAOH,QAAQC,GAAG,CAACG,IAAI,IAAI;IACjC,MAAMC,WAAWL,QAAQC,GAAG,CAACK,SAAS,KAAK,UAAUN,QAAQO,IAAI,CAACC,QAAQ,CAAC;IAC3E,MAAMC,WAAWJ,WAAW,QAAQ;IACpC,2GAA2G;IAC3G,MAAMK,SAASV,QAAQC,GAAG,CAACU,mBAAmB,IAAI;IAElD,OAAO,GAAGF,SAAS,aAAa,EAAEN,OAAOO,SAASE,cAAc;AAClE,EAAC;AAED;;;;CAIC,GACD,MAAMA,aAAa;IACjB,MAAMC,cAAclB;IAEpB,IAAI,CAACkB,aAAa;QAChB,OAAOjB;IACT;IAEA,MAAMkB,cAAcpB,aAAamB,aAAaE,KAAK,CAACC,IAAI,CAAC;IAEzD,OAAOvB,gBAAgBqB,aAAahB,+BAA+B,UAC/DD,gBACAD;AACN"}
@@ -0,0 +1,60 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ vi.mock('./getNextVersion.js', ()=>({
3
+ getNextVersion: vi.fn()
4
+ }));
5
+ const { getNextVersion } = await import('./getNextVersion.js');
6
+ const { getNextJsHMRURL } = await import('./getNextJsHMRURL.js');
7
+ const modernURL = 'ws://localhost:3000/_next/hmr';
8
+ const legacyURL = 'ws://localhost:3000/_next/webpack-hmr';
9
+ describe('getNextJsHMRURL', ()=>{
10
+ beforeEach(()=>{
11
+ vi.mocked(getNextVersion).mockReturnValue('16.3.0');
12
+ vi.stubEnv('PAYLOAD_HMR_URL_OVERRIDE', undefined);
13
+ vi.stubEnv('PORT', '3000');
14
+ });
15
+ afterEach(()=>{
16
+ vi.unstubAllEnvs();
17
+ });
18
+ it('should use the current HMR path on Next.js 16.3', ()=>{
19
+ expect(getNextJsHMRURL()).toBe(modernURL);
20
+ });
21
+ it('should use the current HMR path on Next.js versions above 16.3', ()=>{
22
+ vi.mocked(getNextVersion).mockReturnValue('17.0.1');
23
+ expect(getNextJsHMRURL()).toBe(modernURL);
24
+ });
25
+ it('should use the legacy HMR path on Next.js below 16.3', ()=>{
26
+ vi.mocked(getNextVersion).mockReturnValue('16.2.7');
27
+ expect(getNextJsHMRURL()).toBe(legacyURL);
28
+ });
29
+ it('should use the legacy HMR path on a Next.js 15 install', ()=>{
30
+ vi.mocked(getNextVersion).mockReturnValue('15.5.0');
31
+ expect(getNextJsHMRURL()).toBe(legacyURL);
32
+ });
33
+ it('should ignore pre-release identifiers when choosing the path', ()=>{
34
+ vi.mocked(getNextVersion).mockReturnValue('16.3.0-canary.12');
35
+ expect(getNextJsHMRURL()).toBe(modernURL);
36
+ });
37
+ it('should use the legacy HMR path on a pre-release below 16.3', ()=>{
38
+ vi.mocked(getNextVersion).mockReturnValue('16.2.0-canary.5');
39
+ expect(getNextJsHMRURL()).toBe(legacyURL);
40
+ });
41
+ it('should use the current HMR path when the Next.js version is unknown', ()=>{
42
+ vi.mocked(getNextVersion).mockReturnValue(undefined);
43
+ expect(getNextJsHMRURL()).toBe(modernURL);
44
+ });
45
+ it('should use PAYLOAD_HMR_URL_OVERRIDE whatever the version', ()=>{
46
+ vi.mocked(getNextVersion).mockReturnValue(undefined);
47
+ vi.stubEnv('PAYLOAD_HMR_URL_OVERRIDE', 'ws://localhost:4000/custom-hmr');
48
+ expect(getNextJsHMRURL()).toBe('ws://localhost:4000/custom-hmr');
49
+ });
50
+ it('should use the wss protocol when HTTPS is enabled', ()=>{
51
+ vi.stubEnv('USE_HTTPS', 'true');
52
+ expect(getNextJsHMRURL()).toBe('wss://localhost:3000/_next/hmr');
53
+ });
54
+ it('should include the Next.js asset prefix', ()=>{
55
+ vi.stubEnv('__NEXT_ASSET_PREFIX', '/base');
56
+ expect(getNextJsHMRURL()).toBe('ws://localhost:3000/base/_next/hmr');
57
+ });
58
+ });
59
+
60
+ //# sourceMappingURL=getNextJsHMRURL.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/getNextJsHMRURL.spec.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nvi.mock('./getNextVersion.js', () => ({ getNextVersion: vi.fn() }))\n\nconst { getNextVersion } = await import('./getNextVersion.js')\nconst { getNextJsHMRURL } = await import('./getNextJsHMRURL.js')\n\nconst modernURL = 'ws://localhost:3000/_next/hmr'\nconst legacyURL = 'ws://localhost:3000/_next/webpack-hmr'\n\ndescribe('getNextJsHMRURL', () => {\n beforeEach(() => {\n vi.mocked(getNextVersion).mockReturnValue('16.3.0')\n vi.stubEnv('PAYLOAD_HMR_URL_OVERRIDE', undefined)\n vi.stubEnv('PORT', '3000')\n })\n\n afterEach(() => {\n vi.unstubAllEnvs()\n })\n\n it('should use the current HMR path on Next.js 16.3', () => {\n expect(getNextJsHMRURL()).toBe(modernURL)\n })\n\n it('should use the current HMR path on Next.js versions above 16.3', () => {\n vi.mocked(getNextVersion).mockReturnValue('17.0.1')\n\n expect(getNextJsHMRURL()).toBe(modernURL)\n })\n\n it('should use the legacy HMR path on Next.js below 16.3', () => {\n vi.mocked(getNextVersion).mockReturnValue('16.2.7')\n\n expect(getNextJsHMRURL()).toBe(legacyURL)\n })\n\n it('should use the legacy HMR path on a Next.js 15 install', () => {\n vi.mocked(getNextVersion).mockReturnValue('15.5.0')\n\n expect(getNextJsHMRURL()).toBe(legacyURL)\n })\n\n it('should ignore pre-release identifiers when choosing the path', () => {\n vi.mocked(getNextVersion).mockReturnValue('16.3.0-canary.12')\n\n expect(getNextJsHMRURL()).toBe(modernURL)\n })\n\n it('should use the legacy HMR path on a pre-release below 16.3', () => {\n vi.mocked(getNextVersion).mockReturnValue('16.2.0-canary.5')\n\n expect(getNextJsHMRURL()).toBe(legacyURL)\n })\n\n it('should use the current HMR path when the Next.js version is unknown', () => {\n vi.mocked(getNextVersion).mockReturnValue(undefined)\n\n expect(getNextJsHMRURL()).toBe(modernURL)\n })\n\n it('should use PAYLOAD_HMR_URL_OVERRIDE whatever the version', () => {\n vi.mocked(getNextVersion).mockReturnValue(undefined)\n vi.stubEnv('PAYLOAD_HMR_URL_OVERRIDE', 'ws://localhost:4000/custom-hmr')\n\n expect(getNextJsHMRURL()).toBe('ws://localhost:4000/custom-hmr')\n })\n\n it('should use the wss protocol when HTTPS is enabled', () => {\n vi.stubEnv('USE_HTTPS', 'true')\n\n expect(getNextJsHMRURL()).toBe('wss://localhost:3000/_next/hmr')\n })\n\n it('should include the Next.js asset prefix', () => {\n vi.stubEnv('__NEXT_ASSET_PREFIX', '/base')\n\n expect(getNextJsHMRURL()).toBe('ws://localhost:3000/base/_next/hmr')\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","mock","getNextVersion","fn","getNextJsHMRURL","modernURL","legacyURL","mocked","mockReturnValue","stubEnv","undefined","unstubAllEnvs","toBe"],"mappings":"AAAA,SAASA,SAAS,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAExEA,GAAGC,IAAI,CAAC,uBAAuB,IAAO,CAAA;QAAEC,gBAAgBF,GAAGG,EAAE;IAAG,CAAA;AAEhE,MAAM,EAAED,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC;AACxC,MAAM,EAAEE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC;AAEzC,MAAMC,YAAY;AAClB,MAAMC,YAAY;AAElBT,SAAS,mBAAmB;IAC1BD,WAAW;QACTI,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAAC;QAC1CR,GAAGS,OAAO,CAAC,4BAA4BC;QACvCV,GAAGS,OAAO,CAAC,QAAQ;IACrB;IAEAd,UAAU;QACRK,GAAGW,aAAa;IAClB;IAEAZ,GAAG,mDAAmD;QACpDD,OAAOM,mBAAmBQ,IAAI,CAACP;IACjC;IAEAN,GAAG,kEAAkE;QACnEC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAAC;QAE1CV,OAAOM,mBAAmBQ,IAAI,CAACP;IACjC;IAEAN,GAAG,wDAAwD;QACzDC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAAC;QAE1CV,OAAOM,mBAAmBQ,IAAI,CAACN;IACjC;IAEAP,GAAG,0DAA0D;QAC3DC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAAC;QAE1CV,OAAOM,mBAAmBQ,IAAI,CAACN;IACjC;IAEAP,GAAG,gEAAgE;QACjEC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAAC;QAE1CV,OAAOM,mBAAmBQ,IAAI,CAACP;IACjC;IAEAN,GAAG,8DAA8D;QAC/DC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAAC;QAE1CV,OAAOM,mBAAmBQ,IAAI,CAACN;IACjC;IAEAP,GAAG,uEAAuE;QACxEC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAACE;QAE1CZ,OAAOM,mBAAmBQ,IAAI,CAACP;IACjC;IAEAN,GAAG,4DAA4D;QAC7DC,GAAGO,MAAM,CAACL,gBAAgBM,eAAe,CAACE;QAC1CV,GAAGS,OAAO,CAAC,4BAA4B;QAEvCX,OAAOM,mBAAmBQ,IAAI,CAAC;IACjC;IAEAb,GAAG,qDAAqD;QACtDC,GAAGS,OAAO,CAAC,aAAa;QAExBX,OAAOM,mBAAmBQ,IAAI,CAAC;IACjC;IAEAb,GAAG,2CAA2C;QAC5CC,GAAGS,OAAO,CAAC,uBAAuB;QAElCX,OAAOM,mBAAmBQ,IAAI,CAAC;IACjC;AACF"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Reads the version of the Next.js installed alongside the running app.
3
+ * Returns undefined if Next.js cannot be resolved, e.g. because Payload runs
4
+ * outside of Next.js or from a directory the app's dependencies are not visible from.
5
+ */
6
+ export declare const getNextVersion: () => string | undefined;
7
+ //# sourceMappingURL=getNextVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getNextVersion.d.ts","sourceRoot":"","sources":["../../src/utilities/getNextVersion.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAO,MAAM,GAAG,SAc1C,CAAA"}
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'fs';
2
+ import { resolveFrom } from './dependencies/resolveFrom.js';
3
+ /**
4
+ * Reads the version of the Next.js installed alongside the running app.
5
+ * Returns undefined if Next.js cannot be resolved, e.g. because Payload runs
6
+ * outside of Next.js or from a directory the app's dependencies are not visible from.
7
+ */ export const getNextVersion = ()=>{
8
+ try {
9
+ const packageJSONPath = resolveFrom(process.cwd(), 'next/package.json', true);
10
+ if (!packageJSONPath) {
11
+ return undefined;
12
+ }
13
+ const { version } = JSON.parse(readFileSync(packageJSONPath, 'utf-8'));
14
+ return typeof version === 'string' ? version : undefined;
15
+ } catch (_) {
16
+ return undefined;
17
+ }
18
+ };
19
+
20
+ //# sourceMappingURL=getNextVersion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/getNextVersion.ts"],"sourcesContent":["import { readFileSync } from 'fs'\n\nimport { resolveFrom } from './dependencies/resolveFrom.js'\n\n/**\n * Reads the version of the Next.js installed alongside the running app.\n * Returns undefined if Next.js cannot be resolved, e.g. because Payload runs\n * outside of Next.js or from a directory the app's dependencies are not visible from.\n */\nexport const getNextVersion = (): string | undefined => {\n try {\n const packageJSONPath = resolveFrom(process.cwd(), 'next/package.json', true)\n\n if (!packageJSONPath) {\n return undefined\n }\n\n const { version } = JSON.parse(readFileSync(packageJSONPath, 'utf-8'))\n\n return typeof version === 'string' ? version : undefined\n } catch (_) {\n return undefined\n }\n}\n"],"names":["readFileSync","resolveFrom","getNextVersion","packageJSONPath","process","cwd","undefined","version","JSON","parse","_"],"mappings":"AAAA,SAASA,YAAY,QAAQ,KAAI;AAEjC,SAASC,WAAW,QAAQ,gCAA+B;AAE3D;;;;CAIC,GACD,OAAO,MAAMC,iBAAiB;IAC5B,IAAI;QACF,MAAMC,kBAAkBF,YAAYG,QAAQC,GAAG,IAAI,qBAAqB;QAExE,IAAI,CAACF,iBAAiB;YACpB,OAAOG;QACT;QAEA,MAAM,EAAEC,OAAO,EAAE,GAAGC,KAAKC,KAAK,CAACT,aAAaG,iBAAiB;QAE7D,OAAO,OAAOI,YAAY,WAAWA,UAAUD;IACjD,EAAE,OAAOI,GAAG;QACV,OAAOJ;IACT;AACF,EAAC"}
@@ -0,0 +1,16 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { getNextVersion } from './getNextVersion.js';
3
+ describe('getNextVersion', ()=>{
4
+ afterEach(()=>{
5
+ vi.restoreAllMocks();
6
+ });
7
+ it('should read the version of the installed Next.js', ()=>{
8
+ expect(getNextVersion()).toMatch(/^\d+\.\d+\.\d+/);
9
+ });
10
+ it('should return undefined when Next.js cannot be resolved', ()=>{
11
+ vi.spyOn(process, 'cwd').mockReturnValue('/');
12
+ expect(getNextVersion()).toBeUndefined();
13
+ });
14
+ });
15
+
16
+ //# sourceMappingURL=getNextVersion.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/getNextVersion.spec.ts"],"sourcesContent":["import { afterEach, describe, expect, it, vi } from 'vitest'\n\nimport { getNextVersion } from './getNextVersion.js'\n\ndescribe('getNextVersion', () => {\n afterEach(() => {\n vi.restoreAllMocks()\n })\n\n it('should read the version of the installed Next.js', () => {\n expect(getNextVersion()).toMatch(/^\\d+\\.\\d+\\.\\d+/)\n })\n\n it('should return undefined when Next.js cannot be resolved', () => {\n vi.spyOn(process, 'cwd').mockReturnValue('/')\n\n expect(getNextVersion()).toBeUndefined()\n })\n})\n"],"names":["afterEach","describe","expect","it","vi","getNextVersion","restoreAllMocks","toMatch","spyOn","process","mockReturnValue","toBeUndefined"],"mappings":"AAAA,SAASA,SAAS,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAE5D,SAASC,cAAc,QAAQ,sBAAqB;AAEpDJ,SAAS,kBAAkB;IACzBD,UAAU;QACRI,GAAGE,eAAe;IACpB;IAEAH,GAAG,oDAAoD;QACrDD,OAAOG,kBAAkBE,OAAO,CAAC;IACnC;IAEAJ,GAAG,2DAA2D;QAC5DC,GAAGI,KAAK,CAACC,SAAS,OAAOC,eAAe,CAAC;QAEzCR,OAAOG,kBAAkBM,aAAa;IACxC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"getSafeRedirect.d.ts","sourceRoot":"","sources":["../../src/utilities/getSafeRedirect.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,mDAIzB;IACD,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC9B,KAAG,MAmCH,CAAA"}
1
+ {"version":3,"file":"getSafeRedirect.d.ts","sourceRoot":"","sources":["../../src/utilities/getSafeRedirect.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,mDAIzB;IACD,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC9B,KAAG,MAgDH,CAAA"}
@@ -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.0",
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",
@@ -112,10 +112,10 @@
112
112
  "sanitize-filename": "1.6.3",
113
113
  "ts-essentials": "10.0.3",
114
114
  "tsx": "4.22.4",
115
- "undici": "7.28.0",
115
+ "undici": "7.29.0",
116
116
  "uuid": "13.0.2",
117
117
  "ws": "^8.16.0",
118
- "@payloadcms/translations": "3.87.0"
118
+ "@payloadcms/translations": "3.88.0"
119
119
  },
120
120
  "devDependencies": {
121
121
  "@hyrious/esbuild-plugin-commonjs": "0.2.6",
@@ -1,7 +0,0 @@
1
- type Options = {
2
- extension?: string;
3
- name?: string;
4
- };
5
- export declare const temporaryFileTask: (callback: (temporaryPath: string) => Promise<any>, options?: Options) => Promise<any>;
6
- export {};
7
- //# sourceMappingURL=tempFile.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tempFile.d.ts","sourceRoot":"","sources":["../../src/uploads/tempFile.ts"],"names":[],"mappings":"AAaA,KAAK,OAAO,GAAG;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,eAAO,MAAM,iBAAiB,aAClB,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,YACxC,OAAO,iBAIjB,CAAA"}
@@ -1,39 +0,0 @@
1
- import fs from 'fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { v4 as uuid } from 'uuid';
5
- async function runTask(temporaryPath, callback) {
6
- try {
7
- return await callback(temporaryPath);
8
- } finally{
9
- await fs.rm(temporaryPath, {
10
- force: true,
11
- maxRetries: 2,
12
- recursive: true
13
- });
14
- }
15
- }
16
- export const temporaryFileTask = async (callback, options = {})=>{
17
- const filePath = await temporaryFile(options);
18
- return runTask(filePath, callback);
19
- };
20
- async function temporaryFile(options) {
21
- if (options.name) {
22
- if (options.extension !== undefined && options.extension !== null) {
23
- throw new Error('The `name` and `extension` options are mutually exclusive');
24
- }
25
- return path.join(await temporaryDirectory(), options.name);
26
- }
27
- return await getPath() + (options.extension === undefined || options.extension === null ? '' : '.' + options.extension.replace(/^\./, ''));
28
- }
29
- async function temporaryDirectory({ prefix = '' } = {}) {
30
- const directory = await getPath(prefix);
31
- await fs.mkdir(directory);
32
- return directory;
33
- }
34
- async function getPath(prefix = '') {
35
- const temporaryDirectory = await fs.realpath(os.tmpdir());
36
- return path.join(temporaryDirectory, prefix + uuid());
37
- }
38
-
39
- //# sourceMappingURL=tempFile.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/uploads/tempFile.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { v4 as uuid } from 'uuid'\n\nasync function runTask(temporaryPath: string, callback: (temporaryPath: string) => Promise<any>) {\n try {\n return await callback(temporaryPath)\n } finally {\n await fs.rm(temporaryPath, { force: true, maxRetries: 2, recursive: true })\n }\n}\n\ntype Options = {\n extension?: string\n name?: string\n}\n\nexport const temporaryFileTask = async (\n callback: (temporaryPath: string) => Promise<any>,\n options: Options = {},\n) => {\n const filePath = await temporaryFile(options)\n return runTask(filePath, callback)\n}\n\nasync function temporaryFile(options: Options) {\n if (options.name) {\n if (options.extension !== undefined && options.extension !== null) {\n throw new Error('The `name` and `extension` options are mutually exclusive')\n }\n\n return path.join(await temporaryDirectory(), options.name)\n }\n\n return (\n (await getPath()) +\n (options.extension === undefined || options.extension === null\n ? ''\n : '.' + options.extension.replace(/^\\./, ''))\n )\n}\n\nasync function temporaryDirectory({ prefix = '' } = {}) {\n const directory = await getPath(prefix)\n await fs.mkdir(directory)\n return directory\n}\n\nasync function getPath(prefix = ''): Promise<string> {\n const temporaryDirectory = await fs.realpath(os.tmpdir())\n return path.join(temporaryDirectory, prefix + uuid())\n}\n"],"names":["fs","os","path","v4","uuid","runTask","temporaryPath","callback","rm","force","maxRetries","recursive","temporaryFileTask","options","filePath","temporaryFile","name","extension","undefined","Error","join","temporaryDirectory","getPath","replace","prefix","directory","mkdir","realpath","tmpdir"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAC5B,SAASC,MAAMC,IAAI,QAAQ,OAAM;AAEjC,eAAeC,QAAQC,aAAqB,EAAEC,QAAiD;IAC7F,IAAI;QACF,OAAO,MAAMA,SAASD;IACxB,SAAU;QACR,MAAMN,GAAGQ,EAAE,CAACF,eAAe;YAAEG,OAAO;YAAMC,YAAY;YAAGC,WAAW;QAAK;IAC3E;AACF;AAOA,OAAO,MAAMC,oBAAoB,OAC/BL,UACAM,UAAmB,CAAC,CAAC;IAErB,MAAMC,WAAW,MAAMC,cAAcF;IACrC,OAAOR,QAAQS,UAAUP;AAC3B,EAAC;AAED,eAAeQ,cAAcF,OAAgB;IAC3C,IAAIA,QAAQG,IAAI,EAAE;QAChB,IAAIH,QAAQI,SAAS,KAAKC,aAAaL,QAAQI,SAAS,KAAK,MAAM;YACjE,MAAM,IAAIE,MAAM;QAClB;QAEA,OAAOjB,KAAKkB,IAAI,CAAC,MAAMC,sBAAsBR,QAAQG,IAAI;IAC3D;IAEA,OACE,AAAC,MAAMM,YACNT,CAAAA,QAAQI,SAAS,KAAKC,aAAaL,QAAQI,SAAS,KAAK,OACtD,KACA,MAAMJ,QAAQI,SAAS,CAACM,OAAO,CAAC,OAAO,GAAE;AAEjD;AAEA,eAAeF,mBAAmB,EAAEG,SAAS,EAAE,EAAE,GAAG,CAAC,CAAC;IACpD,MAAMC,YAAY,MAAMH,QAAQE;IAChC,MAAMxB,GAAG0B,KAAK,CAACD;IACf,OAAOA;AACT;AAEA,eAAeH,QAAQE,SAAS,EAAE;IAChC,MAAMH,qBAAqB,MAAMrB,GAAG2B,QAAQ,CAAC1B,GAAG2B,MAAM;IACtD,OAAO1B,KAAKkB,IAAI,CAACC,oBAAoBG,SAASpB;AAChD"}