@10x-media/dual-session 0.1.0-beta.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 (55) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +83 -0
  4. package/dist/auth/authorization.js +49 -0
  5. package/dist/auth/authorization.js.map +1 -0
  6. package/dist/auth/cookies.d.ts +27 -0
  7. package/dist/auth/cookies.js +53 -0
  8. package/dist/auth/cookies.js.map +1 -0
  9. package/dist/auth/csrf.js +18 -0
  10. package/dist/auth/csrf.js.map +1 -0
  11. package/dist/auth/endpoints.js +284 -0
  12. package/dist/auth/endpoints.js.map +1 -0
  13. package/dist/auth/misclassification.js +31 -0
  14. package/dist/auth/misclassification.js.map +1 -0
  15. package/dist/auth/runtime.d.ts +59 -0
  16. package/dist/auth/runtime.js +91 -0
  17. package/dist/auth/runtime.js.map +1 -0
  18. package/dist/auth/strategy.js +100 -0
  19. package/dist/auth/strategy.js.map +1 -0
  20. package/dist/exports/client.d.ts +1 -0
  21. package/dist/exports/client.js +2 -0
  22. package/dist/exports/i18n.d.ts +3 -0
  23. package/dist/exports/i18n.js +3 -0
  24. package/dist/exports/proxy.d.ts +4 -0
  25. package/dist/exports/proxy.js +3 -0
  26. package/dist/exports/types.d.ts +2 -0
  27. package/dist/exports/types.js +1 -0
  28. package/dist/index.d.ts +37 -0
  29. package/dist/index.js +88 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/plugin/constants.d.ts +6 -0
  32. package/dist/plugin/constants.js +7 -0
  33. package/dist/plugin/constants.js.map +1 -0
  34. package/dist/plugin/registerTranslations.js +19 -0
  35. package/dist/plugin/registerTranslations.js.map +1 -0
  36. package/dist/plugin/resolveCollections.js +27 -0
  37. package/dist/plugin/resolveCollections.js.map +1 -0
  38. package/dist/plugin/selectCollections.js +54 -0
  39. package/dist/plugin/selectCollections.js.map +1 -0
  40. package/dist/scope/proxy.d.ts +35 -0
  41. package/dist/scope/proxy.js +42 -0
  42. package/dist/scope/proxy.js.map +1 -0
  43. package/dist/scope/resolveAuthScope.d.ts +43 -0
  44. package/dist/scope/resolveAuthScope.js +43 -0
  45. package/dist/scope/resolveAuthScope.js.map +1 -0
  46. package/dist/translations/en.js +12 -0
  47. package/dist/translations/en.js.map +1 -0
  48. package/dist/translations/index.d.ts +14 -0
  49. package/dist/translations/index.js +27 -0
  50. package/dist/translations/index.js.map +1 -0
  51. package/dist/translations/keys.d.ts +13 -0
  52. package/dist/translations/keys.js +11 -0
  53. package/dist/translations/keys.js.map +1 -0
  54. package/dist/types.d.ts +78 -0
  55. package/package.json +124 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # @10x-media/dual-session
2
+
3
+ ## 0.1.0-beta.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial beta of `@10x-media/dual-session`: give each Payload auth collection its own session cookie, so an admin session and a frontend session can live in one browser at once.
8
+
9
+ Payload signs every collection's token into a single config-wide `${cookiePrefix}-token`, so a customer logging in on the website overwrites the editor's admin session, and logging into the admin panel logs them out of the website. This plugin moves the collections you name onto cookies of their own.
10
+
11
+ - **Shadowed endpoints**: the six auth routes that write a cookie (`/login`, `/logout`, `/refresh-token`, `/me`, `/reset-password`, `/first-register`) are replaced by handlers that delegate to the same core operations, so hooks, lockout, sessions and verification are unchanged and only the cookie name differs. Endpoints that never touch a cookie are left alone.
12
+ - **A strategy that mirrors core's**: same CSRF gate, same `verify` check, same session `sid` check, reading the isolated cookie instead of the shared one. It stands down for an `Authorization` header, so `jwtOrder` and API keys keep the precedence core gives them.
13
+ - **Auth scopes**: an optional Next proxy (`@10x-media/dual-session/proxy`) stamps each request with the session it may authenticate against, resolved from the admin route and the `Referer`/`Sec-Fetch-Site` pair, so the admin panel and the website get different answers from the same REST URL. Without it, `adminSessionPriority` keeps the admin panel reachable.
14
+ - **Ranked sessions**: list order decides which isolated collection wins when a visitor holds several at once, independently of config order.
15
+ - **Per-collection cookie names**, and `disabled` for opting out per environment.
16
+ - **Custom auth**: declared strategies keep first refusal, and `generateIsolatedAuthCookie` is the one-line replacement for `generatePayloadCookie` in an OAuth callback or server action. `resolveIsolatedCookieName` answers with the name alone.
17
+ - **The admin panel is untouched**: the collection behind `admin.user` keeps the shared cookie, written byte for byte as core writes it.
18
+
19
+ Requires Payload `^3.83.0`. Next is optional and only needed for the proxy.
20
+
21
+ - Support one auth collection with roles backing both sessions, via an `isolate` predicate.
22
+
23
+ A collection entry now accepts `isolate: (user) => boolean`, which decides per user rather than per collection which cookie a session lands in. Users it returns `false` for keep the shared `${cookiePrefix}-token`, so projects with a single `users` collection gated by `access.admin` can hold an editor's admin session and a website visitor's session in one browser. This is also the only way to list the collection named by `admin.user`: listing it without a predicate still throws, and giving that entry the `admin` scope now throws too, because an isolated cookie answering admin-scoped requests would outrank the admin's own session.
24
+
25
+ The shared cookie the plugin writes for a non-isolated user is the same string `generatePayloadCookie` produces, so the admin half is core behaviour untouched. All six shadowed endpoints resolve the cookie from the user in front of them, except `first-register`, which always writes the shared cookie because the first user has no roles yet to classify. In development, a login routed to the isolated cookie for a user who would pass `access.admin` logs a warning, so a predicate that disagrees with the admin gate is not silent.
26
+
27
+ `generateIsolatedAuthCookie` and `resolveIsolatedCookieName` take an optional `user`, required for a collection configured with `isolate`. They throw rather than guess which cookie to write.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 10x Media GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ ![Banner](./assets/banner.jpg)
2
+
3
+ # @10x-media/dual-session
4
+
5
+ Give each Payload auth collection its own session cookie, so an admin session and a frontend session can coexist. Payload signs every collection's token into one config-wide `payload-token`, which means a customer logging in on your website overwrites the editor's admin session in the same browser, and back again. This plugin moves the collections you name onto cookies of their own.
6
+
7
+ [![npm](https://img.shields.io/npm/v/@10x-media/dual-session?style=flat-square)](https://www.npmjs.com/package/@10x-media/dual-session)
8
+
9
+ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-plugins) collection. In beta: published under the `beta` dist-tag until a stable 1.0.
10
+
11
+ > [!WARNING]
12
+ > **Experimental.** This plugin changes how sessions are established, which is not a place where a subtle bug announces itself. It is covered by unit, integration and end-to-end tests against Payload's real routing on both Mongo and Postgres, but auth surfaces differ a lot between projects and yours may take a path none of those cover. Try it in staging first, and [report anything that looks off](https://github.com/10x-media/payload-plugins/issues).
13
+
14
+ ## Features
15
+
16
+ - **One cookie per collection**: `payload-customers-token` alongside `payload-token`, so both sessions are live at once.
17
+ - **Core operations, not reimplementations**: the six cookie-writing auth endpoints are shadowed by replacements that delegate to the same Payload operations, so hooks, lockout, sessions and verification are unchanged.
18
+ - **A strategy that mirrors Payload's own**: same CSRF gate, same `verify` check, same session `sid` check. It just reads a different cookie.
19
+ - **Request attribution via an optional Next proxy**, so the admin panel and the website get different answers from the same REST URL.
20
+ - **Ranked frontend sessions**: list order decides which one wins when a visitor holds several.
21
+ - **Works with your own SSO**: declared strategies keep first refusal, and `generateIsolatedAuthCookie` is the one-line swap for `generatePayloadCookie` in a custom OAuth callback.
22
+ - **One collection with roles, too**: an `isolate` predicate splits a single `users` collection so editors keep the shared cookie and website visitors get their own.
23
+ - **The admin panel is left alone**: the collection behind `admin.user` keeps the shared cookie, byte for byte as core writes it.
24
+
25
+ ## Before you install
26
+
27
+ If another auth plugin is already in the config (OAuth, magic links, passkeys, SSO, 2FA), check it first. This plugin owns a collection's auth endpoints and decides where its cookie is written, so anything that writes the session cookie itself or declares its own `/login` on the same collection conflicts, silently. Plugins that only read `req.user` or add a strategy compose fine. [Other plugins that touch auth](https://docs.10xmedia.de/dual-session/limits#other-plugins-that-touch-auth) has the two-minute check and the ways out.
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ pnpm add @10x-media/dual-session@beta
33
+ ```
34
+
35
+ ```ts
36
+ // payload.config.ts
37
+ import { buildConfig } from 'payload'
38
+ import { dualSession } from '@10x-media/dual-session'
39
+
40
+ export default buildConfig({
41
+ admin: { user: 'users' },
42
+ collections: [users, customers],
43
+ plugins: [dualSession({ collections: ['customers'] })],
44
+ })
45
+ ```
46
+
47
+ ```ts
48
+ // proxy.ts (middleware.ts on Next 15)
49
+ import { createAuthScopeProxy } from '@10x-media/dual-session/proxy'
50
+
51
+ export default createAuthScopeProxy()
52
+
53
+ export const config = {
54
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
55
+ }
56
+ ```
57
+
58
+ The matcher must cover `/api`; the widely copied default excludes it, which leaves every REST call unattributed.
59
+
60
+ If editors and website visitors are one collection told apart by a `roles` field, split it by user instead of by collection:
61
+
62
+ ```ts
63
+ dualSession({
64
+ collections: [{ slug: 'users', isolate: (user) => !checkRole(['admin', 'editor'], user) }],
65
+ })
66
+ ```
67
+
68
+ ## Documentation
69
+
70
+ Full documentation at [docs.10xmedia.de](https://docs.10xmedia.de/dual-session):
71
+
72
+ - [Overview](https://docs.10xmedia.de/dual-session)
73
+ - [Quick start](https://docs.10xmedia.de/dual-session/quick-start)
74
+ - [One collection, two sessions](https://docs.10xmedia.de/dual-session/role-split)
75
+ - [Scopes](https://docs.10xmedia.de/dual-session/scopes)
76
+ - [Frontends and clients](https://docs.10xmedia.de/dual-session/clients)
77
+ - [Custom auth](https://docs.10xmedia.de/dual-session/custom-auth)
78
+ - [Configuration](https://docs.10xmedia.de/dual-session/configuration)
79
+ - [Limits](https://docs.10xmedia.de/dual-session/limits)
80
+
81
+ ## License
82
+
83
+ [MIT](./LICENSE). Copyright 10x Media GmbH.
@@ -0,0 +1,49 @@
1
+ //#region src/auth/authorization.ts
2
+ /** The `Authorization` prefixes `extractJWT` recognises, keyed by its `jwtOrder` entries. */
3
+ const SCHEMES = {
4
+ Bearer: "Bearer ",
5
+ JWT: "JWT "
6
+ };
7
+ /**
8
+ * The token an `Authorization` header carries, if Payload would read it ahead of a cookie.
9
+ *
10
+ * Mirrors `extractJWT` minus its cookie method: for an isolated collection the shared
11
+ * `${cookiePrefix}-token` is never a valid source, so consulting core's extraction whole
12
+ * would answer with another session's token. Schemes listed after `cookie` in `jwtOrder`
13
+ * do not outrank it and are ignored.
14
+ */
15
+ const extractAuthorizationToken = ({ headers, payload }) => {
16
+ const header = headers.get("Authorization");
17
+ if (!header) return;
18
+ for (const method of payload.config.auth.jwtOrder) {
19
+ if (method === "cookie") return;
20
+ const prefix = SCHEMES[method];
21
+ if (prefix && header.startsWith(prefix)) return header.slice(prefix.length);
22
+ }
23
+ };
24
+ /**
25
+ * True when `Authorization` carries credentials Payload would honour ahead of a cookie.
26
+ *
27
+ * Two unrelated credentials share that header. An **API key** (`<slug> API-Key <key>`) is
28
+ * matched by its own strategy, which sits directly behind a collection's declared ones and
29
+ * owes nothing to `jwtOrder`. A **JWT** is read by `extractJWT`, where `jwtOrder` ranks the
30
+ * `JWT` and `Bearer` schemes against the cookie and defaults to putting them first.
31
+ *
32
+ * The isolated strategy runs ahead of both, so without this check moving a collection onto
33
+ * its own cookie would quietly invert that precedence: a request holding both an isolated
34
+ * cookie and an `Authorization` header would resolve to the cookie's user where core
35
+ * resolves to the header's.
36
+ */
37
+ const hasPrecedingAuthorization = ({ headers, payload, slug }) => {
38
+ const header = headers.get("Authorization");
39
+ if (!header) return false;
40
+ if (payload.collections[slug]?.config.auth.useAPIKey && header.startsWith(`${slug} API-Key `)) return true;
41
+ return extractAuthorizationToken({
42
+ headers,
43
+ payload
44
+ }) !== void 0;
45
+ };
46
+ //#endregion
47
+ export { extractAuthorizationToken, hasPrecedingAuthorization };
48
+
49
+ //# sourceMappingURL=authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization.js","names":[],"sources":["../../src/auth/authorization.ts"],"sourcesContent":["import type { CollectionSlug, Payload } from 'payload'\n\n/** The `Authorization` prefixes `extractJWT` recognises, keyed by its `jwtOrder` entries. */\nconst SCHEMES: Partial<Record<string, string>> = {\n\tBearer: 'Bearer ',\n\tJWT: 'JWT ',\n}\n\n/**\n * The token an `Authorization` header carries, if Payload would read it ahead of a cookie.\n *\n * Mirrors `extractJWT` minus its cookie method: for an isolated collection the shared\n * `${cookiePrefix}-token` is never a valid source, so consulting core's extraction whole\n * would answer with another session's token. Schemes listed after `cookie` in `jwtOrder`\n * do not outrank it and are ignored.\n */\nexport const extractAuthorizationToken = ({\n\theaders,\n\tpayload,\n}: {\n\theaders: Headers\n\tpayload: Payload\n}): string | undefined => {\n\tconst header = headers.get('Authorization')\n\n\tif (!header) {\n\t\treturn undefined\n\t}\n\n\tfor (const method of payload.config.auth.jwtOrder) {\n\t\tif (method === 'cookie') {\n\t\t\treturn undefined\n\t\t}\n\n\t\tconst prefix = SCHEMES[method]\n\n\t\tif (prefix && header.startsWith(prefix)) {\n\t\t\treturn header.slice(prefix.length)\n\t\t}\n\t}\n\n\treturn undefined\n}\n\n/**\n * True when `Authorization` carries credentials Payload would honour ahead of a cookie.\n *\n * Two unrelated credentials share that header. An **API key** (`<slug> API-Key <key>`) is\n * matched by its own strategy, which sits directly behind a collection's declared ones and\n * owes nothing to `jwtOrder`. A **JWT** is read by `extractJWT`, where `jwtOrder` ranks the\n * `JWT` and `Bearer` schemes against the cookie and defaults to putting them first.\n *\n * The isolated strategy runs ahead of both, so without this check moving a collection onto\n * its own cookie would quietly invert that precedence: a request holding both an isolated\n * cookie and an `Authorization` header would resolve to the cookie's user where core\n * resolves to the header's.\n */\nexport const hasPrecedingAuthorization = ({\n\theaders,\n\tpayload,\n\tslug,\n}: {\n\theaders: Headers\n\tpayload: Payload\n\tslug: CollectionSlug\n}): boolean => {\n\tconst header = headers.get('Authorization')\n\n\tif (!header) {\n\t\treturn false\n\t}\n\n\tif (payload.collections[slug]?.config.auth.useAPIKey && header.startsWith(`${slug} API-Key `)) {\n\t\treturn true\n\t}\n\n\treturn extractAuthorizationToken({ headers, payload }) !== undefined\n}\n"],"mappings":";;AAGA,MAAM,UAA2C;CAChD,QAAQ;CACR,KAAK;AACN;;;;;;;;;AAUA,MAAa,6BAA6B,EACzC,SACA,cAIyB;CACzB,MAAM,SAAS,QAAQ,IAAI,eAAe;CAE1C,IAAI,CAAC,QACJ;CAGD,KAAK,MAAM,UAAU,QAAQ,OAAO,KAAK,UAAU;EAClD,IAAI,WAAW,UACd;EAGD,MAAM,SAAS,QAAQ;EAEvB,IAAI,UAAU,OAAO,WAAW,MAAM,GACrC,OAAO,OAAO,MAAM,OAAO,MAAM;CAEnC;AAGD;;;;;;;;;;;;;;AAeA,MAAa,6BAA6B,EACzC,SACA,SACA,WAKc;CACd,MAAM,SAAS,QAAQ,IAAI,eAAe;CAE1C,IAAI,CAAC,QACJ,OAAO;CAGR,IAAI,QAAQ,YAAY,OAAO,OAAO,KAAK,aAAa,OAAO,WAAW,GAAG,KAAK,UAAU,GAC3F,OAAO;CAGR,OAAO,0BAA0B;EAAE;EAAS;CAAQ,CAAC,MAAM,KAAA;AAC5D"}
@@ -0,0 +1,27 @@
1
+ import { CollectionSlug, SanitizedCollectionConfig, TypedUser } from "payload";
2
+
3
+ //#region src/auth/cookies.d.ts
4
+ type AuthConfig = SanitizedCollectionConfig['auth'];
5
+ declare const getIsolatedCookieName: ({
6
+ cookiePrefix,
7
+ slug
8
+ }: {
9
+ cookiePrefix: string;
10
+ slug: CollectionSlug;
11
+ }) => string;
12
+ /**
13
+ * Same shape as Payload's `generatePayloadCookie`, but with a caller-provided cookie name
14
+ * instead of the config-wide `${cookiePrefix}-token`.
15
+ */
16
+ declare const generateIsolatedCookie: ({
17
+ authConfig,
18
+ name,
19
+ token
20
+ }: {
21
+ authConfig: AuthConfig;
22
+ name: string;
23
+ token: string;
24
+ }) => string;
25
+ //#endregion
26
+ export { generateIsolatedCookie, getIsolatedCookieName };
27
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { generateCookie, getCookieExpiration } from "payload/shared";
2
+ //#region src/auth/cookies.ts
3
+ /** Mirrors Payload's own `sameSite` normalisation in `generatePayloadCookie`. */
4
+ const resolveSameSite = (sameSite) => {
5
+ if (typeof sameSite === "string") return sameSite;
6
+ return sameSite ? "Strict" : void 0;
7
+ };
8
+ const getIsolatedCookieName = ({ cookiePrefix, slug }) => `${cookiePrefix}-${slug}-token`;
9
+ /** Payload's config-wide auth cookie, the one every non-isolated session lives in. */
10
+ const getSharedCookieName = (cookiePrefix) => `${cookiePrefix}-token`;
11
+ /**
12
+ * The cookie a given user's session belongs in, or `undefined` when there is no user to
13
+ * ask about.
14
+ *
15
+ * This is the whole of the role-split model: the cookie is a function of the user, not of
16
+ * the collection. Without an `isolate` predicate every session of the collection is
17
+ * isolated, which is the two-collection case. With one, the users it rejects stay on the
18
+ * shared cookie, and because `generateIsolatedCookie` under the shared name reproduces
19
+ * `generatePayloadCookie` exactly, those sessions are indistinguishable from core's.
20
+ */
21
+ const resolveSlotCookieName = ({ entry, sharedName, user }) => {
22
+ if (!user) return;
23
+ return entry.isolate && !entry.isolate(user) ? sharedName : entry.cookieName;
24
+ };
25
+ /**
26
+ * Same shape as Payload's `generatePayloadCookie`, but with a caller-provided cookie name
27
+ * instead of the config-wide `${cookiePrefix}-token`.
28
+ */
29
+ const generateIsolatedCookie = ({ authConfig, name, token }) => generateCookie({
30
+ name,
31
+ domain: authConfig.cookies.domain ?? void 0,
32
+ expires: getCookieExpiration({ seconds: authConfig.tokenExpiration }),
33
+ httpOnly: true,
34
+ path: "/",
35
+ returnCookieAsObject: false,
36
+ sameSite: resolveSameSite(authConfig.cookies.sameSite),
37
+ secure: authConfig.cookies.secure,
38
+ value: token
39
+ });
40
+ const generateExpiredIsolatedCookie = ({ authConfig, name }) => generateCookie({
41
+ name,
42
+ domain: authConfig.cookies.domain ?? void 0,
43
+ expires: /* @__PURE__ */ new Date(Date.now() - 1e3),
44
+ httpOnly: true,
45
+ path: "/",
46
+ returnCookieAsObject: false,
47
+ sameSite: resolveSameSite(authConfig.cookies.sameSite),
48
+ secure: authConfig.cookies.secure
49
+ });
50
+ //#endregion
51
+ export { generateExpiredIsolatedCookie, generateIsolatedCookie, getIsolatedCookieName, getSharedCookieName, resolveSlotCookieName };
52
+
53
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.js","names":[],"sources":["../../src/auth/cookies.ts"],"sourcesContent":["import type { CollectionSlug, SanitizedCollectionConfig, TypedUser } from 'payload'\nimport { generateCookie, getCookieExpiration } from 'payload/shared'\n\nimport type { ResolvedIsolatedCollection } from '../types'\n\ntype AuthConfig = SanitizedCollectionConfig['auth']\n\n/** Mirrors Payload's own `sameSite` normalisation in `generatePayloadCookie`. */\nconst resolveSameSite = (sameSite: AuthConfig['cookies']['sameSite']) => {\n\tif (typeof sameSite === 'string') {\n\t\treturn sameSite\n\t}\n\treturn sameSite ? ('Strict' as const) : undefined\n}\n\nexport const getIsolatedCookieName = ({\n\tcookiePrefix,\n\tslug,\n}: {\n\tcookiePrefix: string\n\tslug: CollectionSlug\n}) => `${cookiePrefix}-${slug}-token`\n\n/** Payload's config-wide auth cookie, the one every non-isolated session lives in. */\nexport const getSharedCookieName = (cookiePrefix: string) => `${cookiePrefix}-token`\n\n/**\n * The cookie a given user's session belongs in, or `undefined` when there is no user to\n * ask about.\n *\n * This is the whole of the role-split model: the cookie is a function of the user, not of\n * the collection. Without an `isolate` predicate every session of the collection is\n * isolated, which is the two-collection case. With one, the users it rejects stay on the\n * shared cookie, and because `generateIsolatedCookie` under the shared name reproduces\n * `generatePayloadCookie` exactly, those sessions are indistinguishable from core's.\n */\nexport const resolveSlotCookieName = ({\n\tentry,\n\tsharedName,\n\tuser,\n}: {\n\tentry: Pick<ResolvedIsolatedCollection, 'cookieName' | 'isolate'>\n\tsharedName: string\n\tuser: null | TypedUser | undefined\n}): string | undefined => {\n\tif (!user) {\n\t\treturn undefined\n\t}\n\n\treturn entry.isolate && !entry.isolate(user) ? sharedName : entry.cookieName\n}\n\n/**\n * Same shape as Payload's `generatePayloadCookie`, but with a caller-provided cookie name\n * instead of the config-wide `${cookiePrefix}-token`.\n */\nexport const generateIsolatedCookie = ({\n\tauthConfig,\n\tname,\n\ttoken,\n}: {\n\tauthConfig: AuthConfig\n\tname: string\n\ttoken: string\n}) =>\n\tgenerateCookie<false>({\n\t\tname,\n\t\tdomain: authConfig.cookies.domain ?? undefined,\n\t\texpires: getCookieExpiration({ seconds: authConfig.tokenExpiration }),\n\t\thttpOnly: true,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue: token,\n\t})\n\nexport const generateExpiredIsolatedCookie = ({\n\tauthConfig,\n\tname,\n}: {\n\tauthConfig: AuthConfig\n\tname: string\n}) =>\n\tgenerateCookie<false>({\n\t\tname,\n\t\tdomain: authConfig.cookies.domain ?? undefined,\n\t\texpires: new Date(Date.now() - 1000),\n\t\thttpOnly: true,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t})\n"],"mappings":";;;AAQA,MAAM,mBAAmB,aAAgD;CACxE,IAAI,OAAO,aAAa,UACvB,OAAO;CAER,OAAO,WAAY,WAAqB,KAAA;AACzC;AAEA,MAAa,yBAAyB,EACrC,cACA,WAIK,GAAG,aAAa,GAAG,KAAK;;AAG9B,MAAa,uBAAuB,iBAAyB,GAAG,aAAa;;;;;;;;;;;AAY7E,MAAa,yBAAyB,EACrC,OACA,YACA,WAKyB;CACzB,IAAI,CAAC,MACJ;CAGD,OAAO,MAAM,WAAW,CAAC,MAAM,QAAQ,IAAI,IAAI,aAAa,MAAM;AACnE;;;;;AAMA,MAAa,0BAA0B,EACtC,YACA,MACA,YAMA,eAAsB;CACrB;CACA,QAAQ,WAAW,QAAQ,UAAU,KAAA;CACrC,SAAS,oBAAoB,EAAE,SAAS,WAAW,gBAAgB,CAAC;CACpE,UAAU;CACV,MAAM;CACN,sBAAsB;CACtB,UAAU,gBAAgB,WAAW,QAAQ,QAAQ;CACrD,QAAQ,WAAW,QAAQ;CAC3B,OAAO;AACR,CAAC;AAEF,MAAa,iCAAiC,EAC7C,YACA,WAKA,eAAsB;CACrB;CACA,QAAQ,WAAW,QAAQ,UAAU,KAAA;CACrC,yBAAS,IAAI,KAAK,KAAK,IAAI,IAAI,GAAI;CACnC,UAAU;CACV,MAAM;CACN,sBAAsB;CACtB,UAAU,gBAAgB,WAAW,QAAQ,QAAQ;CACrD,QAAQ,WAAW,QAAQ;AAC5B,CAAC"}
@@ -0,0 +1,18 @@
1
+ //#region src/auth/csrf.ts
2
+ /**
3
+ * Port of the CSRF gate Payload applies to its own cookie token extraction
4
+ * (`extractJWT`'s `cookie` method). Isolated cookies must be held to the same
5
+ * standard, otherwise moving a collection off the shared cookie would quietly
6
+ * weaken its CSRF protection.
7
+ */
8
+ const isCookieAuthAllowed = ({ headers, payload }) => {
9
+ const origin = headers.get("Origin");
10
+ if (origin) return payload.config.csrf.length === 0 || payload.config.csrf.includes(origin);
11
+ if (payload.config.csrf.length === 0) return true;
12
+ const secFetchSite = headers.get("Sec-Fetch-Site");
13
+ return secFetchSite === "same-origin" || secFetchSite === "same-site" || secFetchSite === "none";
14
+ };
15
+ //#endregion
16
+ export { isCookieAuthAllowed };
17
+
18
+ //# sourceMappingURL=csrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.js","names":[],"sources":["../../src/auth/csrf.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\n/**\n * Port of the CSRF gate Payload applies to its own cookie token extraction\n * (`extractJWT`'s `cookie` method). Isolated cookies must be held to the same\n * standard, otherwise moving a collection off the shared cookie would quietly\n * weaken its CSRF protection.\n */\nexport const isCookieAuthAllowed = ({\n\theaders,\n\tpayload,\n}: {\n\theaders: Headers\n\tpayload: Payload\n}): boolean => {\n\tconst origin = headers.get('Origin')\n\n\t// Origin present, validate against the csrf allowlist\n\tif (origin) {\n\t\treturn payload.config.csrf.length === 0 || payload.config.csrf.includes(origin)\n\t}\n\n\t// No Origin and no csrf configured, so there is no allowlist to enforce\n\tif (payload.config.csrf.length === 0) {\n\t\treturn true\n\t}\n\n\t// No Origin with csrf configured, fall back to Sec-Fetch-Site\n\tconst secFetchSite = headers.get('Sec-Fetch-Site')\n\n\t// Allow same-origin, same-site, and direct navigations (none)\n\treturn secFetchSite === 'same-origin' || secFetchSite === 'same-site' || secFetchSite === 'none'\n}\n"],"mappings":";;;;;;;AAQA,MAAa,uBAAuB,EACnC,SACA,cAIc;CACd,MAAM,SAAS,QAAQ,IAAI,QAAQ;CAGnC,IAAI,QACH,OAAO,QAAQ,OAAO,KAAK,WAAW,KAAK,QAAQ,OAAO,KAAK,SAAS,MAAM;CAI/E,IAAI,QAAQ,OAAO,KAAK,WAAW,GAClC,OAAO;CAIR,MAAM,eAAe,QAAQ,IAAI,gBAAgB;CAGjD,OAAO,iBAAiB,iBAAiB,iBAAiB,eAAe,iBAAiB;AAC3F"}
@@ -0,0 +1,284 @@
1
+ import { extractAuthorizationToken } from "./authorization.js";
2
+ import { generateExpiredIsolatedCookie, generateIsolatedCookie, getSharedCookieName, resolveSlotCookieName } from "./cookies.js";
3
+ import { warnIfAdminMisclassified } from "./misclassification.js";
4
+ import { addDataAndFileToRequest, addLocalesToRequestFromData, headersWithCors, loginOperation, logoutOperation, meOperation, refreshOperation, registerFirstUserOperation, resetPasswordOperation, sanitizeJoinParams, sanitizePopulateParam, sanitizeSelectParam } from "payload";
5
+ import { isNumber, parseCookies } from "payload/shared";
6
+ //#region src/auth/endpoints.ts
7
+ const getDepth = (req) => {
8
+ const depth = req.query.depth ?? req.searchParams.get("depth");
9
+ return isNumber(depth) ? Number(depth) : void 0;
10
+ };
11
+ /**
12
+ * Payload wraps its own auth endpoints with `wrapInternalEndpoints`, which parses the
13
+ * body onto `req.data`. Custom endpoints are not wrapped, so replacements have to do
14
+ * it themselves.
15
+ */
16
+ const prepareWrite = async (req) => {
17
+ await addDataAndFileToRequest(req);
18
+ addLocalesToRequestFromData(req);
19
+ };
20
+ const getString = (req, key) => typeof req.data?.[key] === "string" ? req.data[key] : "";
21
+ /**
22
+ * The core auth operations type the user they return loosely (`Record<string, unknown>` on
23
+ * some paths, the collection's own doc on others), while `TypedUser` is the union across
24
+ * every auth collection. It is the same object either way.
25
+ */
26
+ const asUser = (user) => user;
27
+ /**
28
+ * Replacements for the built-in auth endpoints that read from and write to a
29
+ * collection-scoped cookie. Each one delegates to the same core operation the built-in
30
+ * handler uses, so behaviour (hooks, lockout, sessions, verification) is unchanged.
31
+ * Only the cookie name differs.
32
+ *
33
+ * Payload appends its built-in endpoints *after* the ones declared on the collection,
34
+ * and `handleEndpoints` matches the first entry that fits. Declaring these on the
35
+ * collection therefore shadows the built-ins.
36
+ *
37
+ * Which cookie a handler touches is a question about the user, not about the collection,
38
+ * so every handler that writes one asks {@link resolveSlotCookieName} first. With no
39
+ * `isolate` predicate the answer is always the isolated cookie and these behave exactly as
40
+ * before; with one, the users it rejects are written to the shared cookie in the same bytes
41
+ * core would have written.
42
+ */
43
+ const buildIsolatedAuthEndpoints = ({ entry }) => {
44
+ const { slug } = entry;
45
+ const getCollection = (req) => {
46
+ const collection = req.payload.collections[slug];
47
+ if (!collection) throw new Error(`dualSession: collection "${slug}" is not registered on payload.`);
48
+ return collection;
49
+ };
50
+ const slotFor = (req, user) => resolveSlotCookieName({
51
+ entry,
52
+ sharedName: getSharedCookieName(req.payload.config.cookiePrefix),
53
+ user
54
+ });
55
+ return [
56
+ {
57
+ method: "post",
58
+ path: "/login",
59
+ handler: async (req) => {
60
+ await prepareWrite(req);
61
+ const collection = getCollection(req);
62
+ const authConfig = collection.config.auth;
63
+ const result = await loginOperation({
64
+ collection,
65
+ data: authConfig.loginWithUsername !== false ? {
66
+ email: getString(req, "email"),
67
+ password: getString(req, "password"),
68
+ username: getString(req, "username")
69
+ } : {
70
+ email: getString(req, "email"),
71
+ password: getString(req, "password")
72
+ },
73
+ depth: getDepth(req),
74
+ req
75
+ });
76
+ const headers = headersWithCors({
77
+ headers: new Headers(),
78
+ req
79
+ });
80
+ const user = asUser(result.user);
81
+ const name = slotFor(req, user);
82
+ if (result.token && name) {
83
+ headers.set("Set-Cookie", generateIsolatedCookie({
84
+ authConfig,
85
+ name,
86
+ token: result.token
87
+ }));
88
+ await warnIfAdminMisclassified({
89
+ collection,
90
+ cookieName: name,
91
+ entry,
92
+ req,
93
+ user
94
+ });
95
+ }
96
+ if (authConfig.removeTokenFromResponses) delete result.token;
97
+ return Response.json({
98
+ message: req.t("authentication:passed"),
99
+ ...result
100
+ }, {
101
+ headers,
102
+ status: 200
103
+ });
104
+ }
105
+ },
106
+ {
107
+ method: "post",
108
+ path: "/logout",
109
+ handler: async (req) => {
110
+ await prepareWrite(req);
111
+ const collection = getCollection(req);
112
+ const headers = headersWithCors({
113
+ headers: new Headers(),
114
+ req
115
+ });
116
+ if (!await logoutOperation({
117
+ allSessions: req.searchParams.get("allSessions") === "true",
118
+ collection,
119
+ req
120
+ })) return Response.json({ message: req.t("error:logoutFailed") }, {
121
+ headers,
122
+ status: 400
123
+ });
124
+ const name = slotFor(req, req.user);
125
+ if (name) headers.set("Set-Cookie", generateExpiredIsolatedCookie({
126
+ authConfig: collection.config.auth,
127
+ name
128
+ }));
129
+ return Response.json({ message: req.t("authentication:logoutSuccessful") }, {
130
+ headers,
131
+ status: 200
132
+ });
133
+ }
134
+ },
135
+ {
136
+ method: "post",
137
+ path: "/refresh-token",
138
+ handler: async (req) => {
139
+ await prepareWrite(req);
140
+ const collection = getCollection(req);
141
+ const authConfig = collection.config.auth;
142
+ const headers = headersWithCors({
143
+ headers: new Headers(),
144
+ req
145
+ });
146
+ const { refreshedToken, ...result } = await refreshOperation({
147
+ collection,
148
+ req
149
+ });
150
+ const name = slotFor(req, req.user);
151
+ if (result.setCookie && refreshedToken && name) headers.set("Set-Cookie", generateIsolatedCookie({
152
+ authConfig,
153
+ name,
154
+ token: refreshedToken
155
+ }));
156
+ return Response.json({
157
+ message: req.t("authentication:tokenRefreshSuccessful"),
158
+ ...result,
159
+ ...authConfig.removeTokenFromResponses ? {} : { refreshedToken }
160
+ }, {
161
+ headers,
162
+ status: 200
163
+ });
164
+ }
165
+ },
166
+ {
167
+ method: "get",
168
+ path: "/me",
169
+ handler: async (req) => {
170
+ const collection = getCollection(req);
171
+ const { joins, populate, select } = req.query;
172
+ const slot = slotFor(req, req.user);
173
+ const result = await meOperation({
174
+ collection,
175
+ currentToken: (slot ? parseCookies(req.headers).get(slot) : void 0) ?? extractAuthorizationToken({
176
+ headers: req.headers,
177
+ payload: req.payload
178
+ }),
179
+ depth: getDepth(req),
180
+ draft: (req.query.draft ?? req.searchParams.get("draft")) === "true",
181
+ joins: sanitizeJoinParams(joins),
182
+ populate: sanitizePopulateParam(populate),
183
+ req,
184
+ select: sanitizeSelectParam(select)
185
+ });
186
+ if (collection.config.auth.removeTokenFromResponses) delete result.token;
187
+ return Response.json({
188
+ ...result,
189
+ message: req.t("authentication:account")
190
+ }, {
191
+ headers: headersWithCors({
192
+ headers: new Headers(),
193
+ req
194
+ }),
195
+ status: 200
196
+ });
197
+ }
198
+ },
199
+ {
200
+ method: "post",
201
+ path: "/reset-password",
202
+ handler: async (req) => {
203
+ await prepareWrite(req);
204
+ const collection = getCollection(req);
205
+ const authConfig = collection.config.auth;
206
+ const result = await resetPasswordOperation({
207
+ collection,
208
+ data: {
209
+ password: getString(req, "password"),
210
+ token: getString(req, "token")
211
+ },
212
+ depth: getDepth(req),
213
+ req
214
+ });
215
+ const headers = headersWithCors({
216
+ headers: new Headers(),
217
+ req
218
+ });
219
+ const name = slotFor(req, asUser(result.user));
220
+ if (result.token && name) headers.set("Set-Cookie", generateIsolatedCookie({
221
+ authConfig,
222
+ name,
223
+ token: result.token
224
+ }));
225
+ if (authConfig.removeTokenFromResponses) delete result.token;
226
+ return Response.json({
227
+ message: req.t("authentication:passwordResetSuccessfully"),
228
+ ...result
229
+ }, {
230
+ headers,
231
+ status: 200
232
+ });
233
+ }
234
+ },
235
+ {
236
+ method: "post",
237
+ path: "/first-register",
238
+ handler: async (req) => {
239
+ await prepareWrite(req);
240
+ const collection = getCollection(req);
241
+ const authConfig = collection.config.auth;
242
+ const authData = authConfig.loginWithUsername !== false ? {
243
+ email: getString(req, "email"),
244
+ password: getString(req, "password"),
245
+ username: getString(req, "username")
246
+ } : {
247
+ email: getString(req, "email"),
248
+ password: getString(req, "password")
249
+ };
250
+ const result = await registerFirstUserOperation({
251
+ collection,
252
+ data: {
253
+ ...req.data,
254
+ ...authData
255
+ },
256
+ req
257
+ });
258
+ const headers = headersWithCors({
259
+ headers: new Headers(),
260
+ req
261
+ });
262
+ const name = slug === req.payload.config.admin.user ? getSharedCookieName(req.payload.config.cookiePrefix) : slotFor(req, asUser(result.user));
263
+ if (result.token && name) headers.set("Set-Cookie", generateIsolatedCookie({
264
+ authConfig,
265
+ name,
266
+ token: result.token
267
+ }));
268
+ return Response.json({
269
+ exp: result.exp,
270
+ message: req.t("authentication:successfullyRegisteredFirstUser"),
271
+ token: authConfig.removeTokenFromResponses ? void 0 : result.token,
272
+ user: result.user
273
+ }, {
274
+ headers,
275
+ status: 200
276
+ });
277
+ }
278
+ }
279
+ ];
280
+ };
281
+ //#endregion
282
+ export { buildIsolatedAuthEndpoints };
283
+
284
+ //# sourceMappingURL=endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.js","names":[],"sources":["../../src/auth/endpoints.ts"],"sourcesContent":["import {\n\ttype AuthCollectionSlug,\n\taddDataAndFileToRequest,\n\taddLocalesToRequestFromData,\n\ttype Endpoint,\n\theadersWithCors,\n\tloginOperation,\n\tlogoutOperation,\n\tmeOperation,\n\ttype PayloadRequest,\n\trefreshOperation,\n\tregisterFirstUserOperation,\n\tresetPasswordOperation,\n\tsanitizeJoinParams,\n\tsanitizePopulateParam,\n\tsanitizeSelectParam,\n\ttype TypedUser,\n} from 'payload'\nimport { isNumber, parseCookies } from 'payload/shared'\n\nimport type { ResolvedIsolatedCollection } from '../types'\nimport { extractAuthorizationToken } from './authorization'\nimport {\n\tgenerateExpiredIsolatedCookie,\n\tgenerateIsolatedCookie,\n\tgetSharedCookieName,\n\tresolveSlotCookieName,\n} from './cookies'\nimport { warnIfAdminMisclassified } from './misclassification'\n\nconst getDepth = (req: PayloadRequest) => {\n\tconst depth = req.query.depth ?? req.searchParams.get('depth')\n\treturn isNumber(depth) ? Number(depth) : undefined\n}\n\n/**\n * Payload wraps its own auth endpoints with `wrapInternalEndpoints`, which parses the\n * body onto `req.data`. Custom endpoints are not wrapped, so replacements have to do\n * it themselves.\n */\nconst prepareWrite = async (req: PayloadRequest) => {\n\tawait addDataAndFileToRequest(req)\n\taddLocalesToRequestFromData(req)\n}\n\nconst getString = (req: PayloadRequest, key: string) =>\n\ttypeof req.data?.[key] === 'string' ? (req.data[key] as string) : ''\n\n/**\n * The core auth operations type the user they return loosely (`Record<string, unknown>` on\n * some paths, the collection's own doc on others), while `TypedUser` is the union across\n * every auth collection. It is the same object either way.\n */\nconst asUser = (user: unknown) => user as TypedUser | undefined\n\n/**\n * Replacements for the built-in auth endpoints that read from and write to a\n * collection-scoped cookie. Each one delegates to the same core operation the built-in\n * handler uses, so behaviour (hooks, lockout, sessions, verification) is unchanged.\n * Only the cookie name differs.\n *\n * Payload appends its built-in endpoints *after* the ones declared on the collection,\n * and `handleEndpoints` matches the first entry that fits. Declaring these on the\n * collection therefore shadows the built-ins.\n *\n * Which cookie a handler touches is a question about the user, not about the collection,\n * so every handler that writes one asks {@link resolveSlotCookieName} first. With no\n * `isolate` predicate the answer is always the isolated cookie and these behave exactly as\n * before; with one, the users it rejects are written to the shared cookie in the same bytes\n * core would have written.\n */\nexport const buildIsolatedAuthEndpoints = ({\n\tentry,\n}: {\n\tentry: ResolvedIsolatedCollection\n}): Endpoint[] => {\n\tconst { slug } = entry\n\n\tconst getCollection = (req: PayloadRequest) => {\n\t\tconst collection = req.payload.collections[slug]\n\t\tif (!collection) {\n\t\t\tthrow new Error(`dualSession: collection \"${slug}\" is not registered on payload.`)\n\t\t}\n\t\treturn collection\n\t}\n\n\tconst slotFor = (req: PayloadRequest, user: null | TypedUser | undefined) =>\n\t\tresolveSlotCookieName({\n\t\t\tentry,\n\t\t\tsharedName: getSharedCookieName(req.payload.config.cookiePrefix),\n\t\t\tuser,\n\t\t})\n\n\treturn [\n\t\t{\n\t\t\tmethod: 'post',\n\t\t\tpath: '/login',\n\t\t\thandler: async (req) => {\n\t\t\t\tawait prepareWrite(req)\n\n\t\t\t\tconst collection = getCollection(req)\n\t\t\t\tconst authConfig = collection.config.auth\n\n\t\t\t\tconst data =\n\t\t\t\t\tauthConfig.loginWithUsername !== false\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\temail: getString(req, 'email'),\n\t\t\t\t\t\t\t\tpassword: getString(req, 'password'),\n\t\t\t\t\t\t\t\tusername: getString(req, 'username'),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: { email: getString(req, 'email'), password: getString(req, 'password') }\n\n\t\t\t\tconst result = await loginOperation<AuthCollectionSlug>({\n\t\t\t\t\tcollection,\n\t\t\t\t\tdata,\n\t\t\t\t\tdepth: getDepth(req),\n\t\t\t\t\treq,\n\t\t\t\t})\n\n\t\t\t\tconst headers = headersWithCors({ headers: new Headers(), req })\n\t\t\t\tconst user = asUser(result.user)\n\t\t\t\tconst name = slotFor(req, user)\n\n\t\t\t\tif (result.token && name) {\n\t\t\t\t\theaders.set(\n\t\t\t\t\t\t'Set-Cookie',\n\t\t\t\t\t\tgenerateIsolatedCookie({ authConfig, name, token: result.token })\n\t\t\t\t\t)\n\n\t\t\t\t\tawait warnIfAdminMisclassified({ collection, cookieName: name, entry, req, user })\n\t\t\t\t}\n\n\t\t\t\tif (authConfig.removeTokenFromResponses) {\n\t\t\t\t\tdelete result.token\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{ message: req.t('authentication:passed'), ...result },\n\t\t\t\t\t{ headers, status: 200 }\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmethod: 'post',\n\t\t\tpath: '/logout',\n\t\t\thandler: async (req) => {\n\t\t\t\tawait prepareWrite(req)\n\n\t\t\t\tconst collection = getCollection(req)\n\t\t\t\tconst headers = headersWithCors({ headers: new Headers(), req })\n\n\t\t\t\tconst result = await logoutOperation({\n\t\t\t\t\tallSessions: req.searchParams.get('allSessions') === 'true',\n\t\t\t\t\tcollection,\n\t\t\t\t\treq,\n\t\t\t\t})\n\n\t\t\t\tif (!result) {\n\t\t\t\t\treturn Response.json({ message: req.t('error:logoutFailed') }, { headers, status: 400 })\n\t\t\t\t}\n\n\t\t\t\t// `logoutOperation` refuses without a user, so by here `req.user` is this\n\t\t\t\t// collection's and the slot is always resolvable.\n\t\t\t\tconst name = slotFor(req, req.user)\n\n\t\t\t\tif (name) {\n\t\t\t\t\theaders.set(\n\t\t\t\t\t\t'Set-Cookie',\n\t\t\t\t\t\tgenerateExpiredIsolatedCookie({ authConfig: collection.config.auth, name })\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{ message: req.t('authentication:logoutSuccessful') },\n\t\t\t\t\t{ headers, status: 200 }\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmethod: 'post',\n\t\t\tpath: '/refresh-token',\n\t\t\thandler: async (req) => {\n\t\t\t\tawait prepareWrite(req)\n\n\t\t\t\tconst collection = getCollection(req)\n\t\t\t\tconst authConfig = collection.config.auth\n\t\t\t\tconst headers = headersWithCors({ headers: new Headers(), req })\n\n\t\t\t\tconst { refreshedToken, ...result } = await refreshOperation({ collection, req })\n\t\t\t\tconst name = slotFor(req, req.user)\n\n\t\t\t\tif (result.setCookie && refreshedToken && name) {\n\t\t\t\t\theaders.set(\n\t\t\t\t\t\t'Set-Cookie',\n\t\t\t\t\t\tgenerateIsolatedCookie({ authConfig, name, token: refreshedToken })\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\tmessage: req.t('authentication:tokenRefreshSuccessful'),\n\t\t\t\t\t\t...result,\n\t\t\t\t\t\t...(authConfig.removeTokenFromResponses ? {} : { refreshedToken }),\n\t\t\t\t\t},\n\t\t\t\t\t{ headers, status: 200 }\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmethod: 'get',\n\t\t\tpath: '/me',\n\t\t\thandler: async (req) => {\n\t\t\t\tconst collection = getCollection(req)\n\t\t\t\tconst { joins, populate, select } = req.query as {\n\t\t\t\t\tjoins?: Parameters<typeof sanitizeJoinParams>[0]\n\t\t\t\t} & Record<string, unknown>\n\n\t\t\t\t// The built-in handler reads the token via `extractJWT`, which only knows about\n\t\t\t\t// the shared cookie, which would report the admin's token back to a frontend user.\n\t\t\t\t// With no cookie in this user's slot the request was authenticated by something\n\t\t\t\t// else (an `Authorization` header, a custom strategy), so fall back to core's own\n\t\t\t\t// extraction rather than reporting no token at all.\n\t\t\t\tconst slot = slotFor(req, req.user)\n\t\t\t\tconst currentToken =\n\t\t\t\t\t(slot ? parseCookies(req.headers).get(slot) : undefined) ??\n\t\t\t\t\textractAuthorizationToken({ headers: req.headers, payload: req.payload })\n\n\t\t\t\tconst result = await meOperation({\n\t\t\t\t\tcollection,\n\t\t\t\t\tcurrentToken,\n\t\t\t\t\tdepth: getDepth(req),\n\t\t\t\t\tdraft: (req.query.draft ?? req.searchParams.get('draft')) === 'true',\n\t\t\t\t\tjoins: sanitizeJoinParams(joins),\n\t\t\t\t\tpopulate: sanitizePopulateParam(populate),\n\t\t\t\t\treq,\n\t\t\t\t\tselect: sanitizeSelectParam(select),\n\t\t\t\t})\n\n\t\t\t\tif (collection.config.auth.removeTokenFromResponses) {\n\t\t\t\t\tdelete result.token\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{ ...result, message: req.t('authentication:account') },\n\t\t\t\t\t{ headers: headersWithCors({ headers: new Headers(), req }), status: 200 }\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmethod: 'post',\n\t\t\tpath: '/reset-password',\n\t\t\thandler: async (req) => {\n\t\t\t\tawait prepareWrite(req)\n\n\t\t\t\tconst collection = getCollection(req)\n\t\t\t\tconst authConfig = collection.config.auth\n\n\t\t\t\tconst result = await resetPasswordOperation({\n\t\t\t\t\tcollection,\n\t\t\t\t\tdata: { password: getString(req, 'password'), token: getString(req, 'token') },\n\t\t\t\t\tdepth: getDepth(req),\n\t\t\t\t\treq,\n\t\t\t\t})\n\n\t\t\t\tconst headers = headersWithCors({ headers: new Headers(), req })\n\t\t\t\tconst name = slotFor(req, asUser(result.user))\n\n\t\t\t\tif (result.token && name) {\n\t\t\t\t\theaders.set(\n\t\t\t\t\t\t'Set-Cookie',\n\t\t\t\t\t\tgenerateIsolatedCookie({ authConfig, name, token: result.token })\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tif (authConfig.removeTokenFromResponses) {\n\t\t\t\t\tdelete result.token\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{ message: req.t('authentication:passwordResetSuccessfully'), ...result },\n\t\t\t\t\t{ headers, status: 200 }\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmethod: 'post',\n\t\t\tpath: '/first-register',\n\t\t\thandler: async (req) => {\n\t\t\t\tawait prepareWrite(req)\n\n\t\t\t\tconst collection = getCollection(req)\n\t\t\t\tconst authConfig = collection.config.auth\n\n\t\t\t\tconst authData =\n\t\t\t\t\tauthConfig.loginWithUsername !== false\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\temail: getString(req, 'email'),\n\t\t\t\t\t\t\t\tpassword: getString(req, 'password'),\n\t\t\t\t\t\t\t\tusername: getString(req, 'username'),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: { email: getString(req, 'email'), password: getString(req, 'password') }\n\n\t\t\t\tconst result = await registerFirstUserOperation({\n\t\t\t\t\tcollection,\n\t\t\t\t\tdata: { ...req.data, ...authData },\n\t\t\t\t\treq,\n\t\t\t\t})\n\n\t\t\t\tconst headers = headersWithCors({ headers: new Headers(), req })\n\n\t\t\t\t// The very first user of the collection that backs the admin panel is created to\n\t\t\t\t// get *into* it, and has no roles yet for a predicate to read, so this one always\n\t\t\t\t// writes the shared cookie rather than asking.\n\t\t\t\tconst name =\n\t\t\t\t\tslug === req.payload.config.admin.user\n\t\t\t\t\t\t? getSharedCookieName(req.payload.config.cookiePrefix)\n\t\t\t\t\t\t: slotFor(req, asUser(result.user))\n\n\t\t\t\tif (result.token && name) {\n\t\t\t\t\theaders.set(\n\t\t\t\t\t\t'Set-Cookie',\n\t\t\t\t\t\tgenerateIsolatedCookie({ authConfig, name, token: result.token })\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\texp: result.exp,\n\t\t\t\t\t\tmessage: req.t('authentication:successfullyRegisteredFirstUser'),\n\t\t\t\t\t\ttoken: authConfig.removeTokenFromResponses ? undefined : result.token,\n\t\t\t\t\t\tuser: result.user,\n\t\t\t\t\t},\n\t\t\t\t\t{ headers, status: 200 }\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t]\n}\n"],"mappings":"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"}