@10x-media/impersonation 0.1.0-beta.0 → 0.1.0-beta.2
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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -21
- package/dist/auth/cookies.js +10 -1
- package/dist/auth/cookies.js.map +1 -1
- package/dist/endpoints/codes.js +22 -0
- package/dist/endpoints/codes.js.map +1 -0
- package/dist/endpoints/exit.js +9 -26
- package/dist/endpoints/exit.js.map +1 -1
- package/dist/endpoints/start.js +4 -11
- package/dist/endpoints/start.js.map +1 -1
- package/dist/plugin/constants.js +6 -2
- package/dist/plugin/constants.js.map +1 -1
- package/dist/plugin/normalizeOptions.js +2 -2
- package/dist/plugin/normalizeOptions.js.map +1 -1
- package/dist/plugin/registerCollection.js +0 -4
- package/dist/plugin/registerCollection.js.map +1 -1
- package/dist/plugin/registerRetention.js.map +1 -1
- package/dist/translations/errorKey.js +26 -0
- package/dist/translations/errorKey.js.map +1 -0
- package/dist/translations/lookup.js +12 -7
- package/dist/translations/lookup.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/EndSessionMenuItem.js +1 -1
- package/dist/ui/ImpersonationBar.d.ts +8 -1
- package/dist/ui/ImpersonationBar.js +22 -22
- package/dist/ui/ImpersonationBar.js.map +1 -1
- package/dist/ui/ImpersonationProvider.js +14 -6
- package/dist/ui/ImpersonationProvider.js.map +1 -1
- package/dist/ui/StartConfirmModal.js +2 -1
- package/dist/ui/StartConfirmModal.js.map +1 -1
- package/dist/ui/api.js +2 -21
- package/dist/ui/api.js.map +1 -1
- package/dist/ui/sessionEnds.js +28 -0
- package/dist/ui/sessionEnds.js.map +1 -0
- package/dist/ui/useImpersonation.js +1 -0
- package/dist/ui/useImpersonation.js.map +1 -1
- package/package.json +3 -3
- package/dist/auth/tenantCookie.js +0 -74
- package/dist/auth/tenantCookie.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @10x-media/impersonation
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The admin bar no longer throws a hydration error on page load: its session countdown appears once the bar has mounted. The bar's text, button labels and exit errors now take overrides from the `translations` option and from `i18n.translations`, as the plugin's other admin strings do.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-beta.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- The tenant selector is now a plain `cookies.clearOnSwitch` entry. Swap start and exit expire it, and `@payloadcms/plugin-multi-tenant` selects the tenant itself; the impersonator's selection is no longer restored on exit. Parallel leaves every `clearOnSwitch` cookie in place. The default is `payload-tenant` regardless of `cookiePrefix`, matching the name the multi-tenant plugin hardcodes. The `impersonatorTenantCookie` field is removed from `impersonation-sessions`, so Postgres installs get a dropped column in their next migration.
|
|
14
|
+
|
|
3
15
|
## 0.1.0-beta.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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/dist/auth/cookies.js
CHANGED
|
@@ -36,6 +36,15 @@ const expirePayloadCookie = ({ authConfig, cookiePrefix }) => generateExpiredPay
|
|
|
36
36
|
collectionAuthConfig: authConfig,
|
|
37
37
|
cookiePrefix
|
|
38
38
|
});
|
|
39
|
+
/**
|
|
40
|
+
* Expires `cookies.clearOnSwitch` when a swap starts. Parallel keeps them: they
|
|
41
|
+
* are origin-wide, and the impersonator's session did not change.
|
|
42
|
+
*/
|
|
43
|
+
const clearOnSwitchOnStart = ({ authConfig, cookiePrefix, except, mode, names }) => mode === "parallel" ? [] : expireCookies({
|
|
44
|
+
authConfig,
|
|
45
|
+
cookiePrefix,
|
|
46
|
+
names: names.filter((name) => !except.includes(name))
|
|
47
|
+
});
|
|
39
48
|
const expireCookies = ({ authConfig, cookiePrefix, names }) => names.map((name) => name === sharedCookieName(cookiePrefix) ? expirePayloadCookie({
|
|
40
49
|
authConfig,
|
|
41
50
|
cookiePrefix
|
|
@@ -44,6 +53,6 @@ const expireCookies = ({ authConfig, cookiePrefix, names }) => names.map((name)
|
|
|
44
53
|
name
|
|
45
54
|
}));
|
|
46
55
|
//#endregion
|
|
47
|
-
export { expireCookie, expireCookies, expirePayloadCookie, generateHintCookie, sharedCookieName
|
|
56
|
+
export { clearOnSwitchOnStart, expireCookie, expireCookies, expirePayloadCookie, generateHintCookie, sharedCookieName };
|
|
48
57
|
|
|
49
58
|
//# sourceMappingURL=cookies.js.map
|
package/dist/auth/cookies.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies.js","names":[],"sources":["../../src/auth/cookies.ts"],"sourcesContent":["import type { SanitizedCollectionConfig } from 'payload'\nimport { generateCookie, generateExpiredPayloadCookie, getCookieExpiration } from 'payload/shared'\n\ntype AuthConfig = SanitizedCollectionConfig['auth']\n\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 sharedCookieName = (cookiePrefix: string) => `${cookiePrefix}-token`\n\nexport const writeCookie = ({\n\tauthConfig,\n\thttpOnly = true,\n\tname,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\thttpOnly?: boolean\n\tname: string\n\tvalue: 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,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue,\n\t})\n\nexport const generateHintCookie = ({\n\tauthConfig,\n\tname,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\tname: string\n\tvalue: string\n}) => writeCookie({ authConfig, name, value })\n\nexport const expireCookie = ({\n\tauthConfig,\n\thttpOnly = true,\n\tname,\n}: {\n\tauthConfig: AuthConfig\n\thttpOnly?: boolean\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,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue: '',\n\t})\n\nexport const expirePayloadCookie = ({\n\tauthConfig,\n\tcookiePrefix,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n}) => generateExpiredPayloadCookie({ collectionAuthConfig: authConfig, cookiePrefix })\n\nexport const expireCookies = ({\n\tauthConfig,\n\tcookiePrefix,\n\tnames,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\tnames: string[]\n}): string[] =>\n\tnames.map((name) =>\n\t\tname === sharedCookieName(cookiePrefix)\n\t\t\t? expirePayloadCookie({ authConfig, cookiePrefix })\n\t\t\t: expireCookie({ authConfig, name })\n\t)\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"cookies.js","names":[],"sources":["../../src/auth/cookies.ts"],"sourcesContent":["import type { SanitizedCollectionConfig } from 'payload'\nimport { generateCookie, generateExpiredPayloadCookie, getCookieExpiration } from 'payload/shared'\n\nimport type { ImpersonationMode } from '../types'\n\ntype AuthConfig = SanitizedCollectionConfig['auth']\n\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 sharedCookieName = (cookiePrefix: string) => `${cookiePrefix}-token`\n\nexport const writeCookie = ({\n\tauthConfig,\n\thttpOnly = true,\n\tname,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\thttpOnly?: boolean\n\tname: string\n\tvalue: 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,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue,\n\t})\n\nexport const generateHintCookie = ({\n\tauthConfig,\n\tname,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\tname: string\n\tvalue: string\n}) => writeCookie({ authConfig, name, value })\n\nexport const expireCookie = ({\n\tauthConfig,\n\thttpOnly = true,\n\tname,\n}: {\n\tauthConfig: AuthConfig\n\thttpOnly?: boolean\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,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue: '',\n\t})\n\nexport const expirePayloadCookie = ({\n\tauthConfig,\n\tcookiePrefix,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n}) => generateExpiredPayloadCookie({ collectionAuthConfig: authConfig, cookiePrefix })\n\n/**\n * Expires `cookies.clearOnSwitch` when a swap starts. Parallel keeps them: they\n * are origin-wide, and the impersonator's session did not change.\n */\nexport const clearOnSwitchOnStart = ({\n\tauthConfig,\n\tcookiePrefix,\n\texcept,\n\tmode,\n\tnames,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\texcept: string[]\n\tmode: ImpersonationMode\n\tnames: string[]\n}): string[] =>\n\tmode === 'parallel'\n\t\t? []\n\t\t: expireCookies({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\tnames: names.filter((name) => !except.includes(name)),\n\t\t\t})\n\nexport const expireCookies = ({\n\tauthConfig,\n\tcookiePrefix,\n\tnames,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\tnames: string[]\n}): string[] =>\n\tnames.map((name) =>\n\t\tname === sharedCookieName(cookiePrefix)\n\t\t\t? expirePayloadCookie({ authConfig, cookiePrefix })\n\t\t\t: expireCookie({ authConfig, name })\n\t)\n"],"mappings":";;AAOA,MAAM,mBAAmB,aAAgD;CACxE,IAAI,OAAO,aAAa,UACvB,OAAO;CAER,OAAO,WAAY,WAAqB,KAAA;AACzC;AAEA,MAAa,oBAAoB,iBAAyB,GAAG,aAAa;AAE1E,MAAa,eAAe,EAC3B,YACA,WAAW,MACX,MACA,YAOA,eAAsB;CACrB;CACA,QAAQ,WAAW,QAAQ,UAAU,KAAA;CACrC,SAAS,oBAAoB,EAAE,SAAS,WAAW,gBAAgB,CAAC;CACpE;CACA,MAAM;CACN,sBAAsB;CACtB,UAAU,gBAAgB,WAAW,QAAQ,QAAQ;CACrD,QAAQ,WAAW,QAAQ;CAC3B;AACD,CAAC;AAEF,MAAa,sBAAsB,EAClC,YACA,MACA,YAKK,YAAY;CAAE;CAAY;CAAM;AAAM,CAAC;AAE7C,MAAa,gBAAgB,EAC5B,YACA,WAAW,MACX,WAMA,eAAsB;CACrB;CACA,QAAQ,WAAW,QAAQ,UAAU,KAAA;CACrC,yBAAS,IAAI,KAAK,KAAK,IAAI,IAAI,GAAI;CACnC;CACA,MAAM;CACN,sBAAsB;CACtB,UAAU,gBAAgB,WAAW,QAAQ,QAAQ;CACrD,QAAQ,WAAW,QAAQ;CAC3B,OAAO;AACR,CAAC;AAEF,MAAa,uBAAuB,EACnC,YACA,mBAIK,6BAA6B;CAAE,sBAAsB;CAAY;AAAa,CAAC;;;;;AAMrF,MAAa,wBAAwB,EACpC,YACA,cACA,QACA,MACA,YAQA,SAAS,aACN,CAAC,IACD,cAAc;CACd;CACA;CACA,OAAO,MAAM,QAAQ,SAAS,CAAC,OAAO,SAAS,IAAI,CAAC;AACrD,CAAC;AAEJ,MAAa,iBAAiB,EAC7B,YACA,cACA,YAMA,MAAM,KAAK,SACV,SAAS,iBAAiB,YAAY,IACnC,oBAAoB;CAAE;CAAY;AAAa,CAAC,IAChD,aAAa;CAAE;CAAY;AAAK,CAAC,CACrC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/endpoints/codes.ts
|
|
2
|
+
const FAILURE_CODES = [
|
|
3
|
+
"alreadyImpersonating",
|
|
4
|
+
"failed",
|
|
5
|
+
"forbidden",
|
|
6
|
+
"impersonatorGone",
|
|
7
|
+
"impersonatorSessionExpired",
|
|
8
|
+
"invalidBody",
|
|
9
|
+
"notImpersonating",
|
|
10
|
+
"origin",
|
|
11
|
+
"reasonRequired",
|
|
12
|
+
"selfTarget",
|
|
13
|
+
"targetNotFound",
|
|
14
|
+
"targetTrashed",
|
|
15
|
+
"targetUnverified",
|
|
16
|
+
"unsupportedAuth",
|
|
17
|
+
"unsupportedCollection"
|
|
18
|
+
];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { FAILURE_CODES };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","names":[],"sources":["../../src/endpoints/codes.ts"],"sourcesContent":["export const FAILURE_CODES = [\n\t'alreadyImpersonating',\n\t'failed',\n\t'forbidden',\n\t'impersonatorGone',\n\t'impersonatorSessionExpired',\n\t'invalidBody',\n\t'notImpersonating',\n\t'origin',\n\t'reasonRequired',\n\t'selfTarget',\n\t'targetNotFound',\n\t'targetTrashed',\n\t'targetUnverified',\n\t'unsupportedAuth',\n\t'unsupportedCollection',\n] as const\n\nexport type FailureCode = (typeof FAILURE_CODES)[number]\n"],"mappings":";AAAA,MAAa,gBAAgB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
|
package/dist/endpoints/exit.js
CHANGED
|
@@ -6,7 +6,6 @@ import { closeAndRevoke } from "../session/close.js";
|
|
|
6
6
|
import { boundSid } from "../types.js";
|
|
7
7
|
import { callerSid, fail, getOptions, isApiKeyStrategy, json, prepareMutation, rejectGate } from "./shared.js";
|
|
8
8
|
import { resignSession } from "../auth/issue.js";
|
|
9
|
-
import { clearOnSwitchWithoutTenant, exitTenantCookies } from "../auth/tenantCookie.js";
|
|
10
9
|
import { releaseLocks } from "../session/locks.js";
|
|
11
10
|
//#region src/endpoints/exit.ts
|
|
12
11
|
const exitHandler = async (req) => {
|
|
@@ -50,11 +49,10 @@ const exitHandler = async (req) => {
|
|
|
50
49
|
status: 500
|
|
51
50
|
});
|
|
52
51
|
const cookiePrefix = req.payload.config.cookiePrefix;
|
|
53
|
-
const extraClearOnSwitch = clearOnSwitchWithoutTenant(options.cookies.clearOnSwitch, cookiePrefix);
|
|
54
52
|
const expireHintAndClear = expireCookies({
|
|
55
53
|
authConfig,
|
|
56
54
|
cookiePrefix,
|
|
57
|
-
names: [...
|
|
55
|
+
names: [...options.cookies.clearOnSwitch, options.hintCookieName]
|
|
58
56
|
});
|
|
59
57
|
if (!impersonator || !collectionBySlug(req.payload, impersonator.collection)) {
|
|
60
58
|
await closeAndRevoke({
|
|
@@ -145,21 +143,13 @@ const exitHandler = async (req) => {
|
|
|
145
143
|
}
|
|
146
144
|
return json({
|
|
147
145
|
body: { ok: true },
|
|
148
|
-
cookies: [
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
name: options.hintCookieName
|
|
156
|
-
}),
|
|
157
|
-
...expireCookies({
|
|
158
|
-
authConfig,
|
|
159
|
-
cookiePrefix,
|
|
160
|
-
names: extraClearOnSwitch
|
|
161
|
-
})
|
|
162
|
-
],
|
|
146
|
+
cookies: [...isolatedName ? [expireCookie({
|
|
147
|
+
authConfig,
|
|
148
|
+
name: isolatedName
|
|
149
|
+
})] : [], expireCookie({
|
|
150
|
+
authConfig,
|
|
151
|
+
name: options.hintCookieName
|
|
152
|
+
})],
|
|
163
153
|
req,
|
|
164
154
|
status: 200
|
|
165
155
|
});
|
|
@@ -214,17 +204,10 @@ const exitHandler = async (req) => {
|
|
|
214
204
|
authConfig,
|
|
215
205
|
name: options.hintCookieName
|
|
216
206
|
}),
|
|
217
|
-
...exitTenantCookies({
|
|
218
|
-
authConfig,
|
|
219
|
-
cookiePrefix,
|
|
220
|
-
mode: closed.mode,
|
|
221
|
-
options,
|
|
222
|
-
snapshot: row.impersonatorTenantCookie
|
|
223
|
-
}),
|
|
224
207
|
...expireCookies({
|
|
225
208
|
authConfig,
|
|
226
209
|
cookiePrefix,
|
|
227
|
-
names:
|
|
210
|
+
names: options.cookies.clearOnSwitch
|
|
228
211
|
})
|
|
229
212
|
],
|
|
230
213
|
req,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exit.js","names":[],"sources":["../../src/endpoints/exit.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadRequest } from 'payload'\n\nimport { expireCookie, expireCookies, expirePayloadCookie } from '../auth/cookies'\nimport { resignSession } from '../auth/issue'\nimport { isolatedCookieNameFor } from '../auth/mode'\nimport {
|
|
1
|
+
{"version":3,"file":"exit.js","names":[],"sources":["../../src/endpoints/exit.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadRequest } from 'payload'\n\nimport { expireCookie, expireCookies, expirePayloadCookie } from '../auth/cookies'\nimport { resignSession } from '../auth/issue'\nimport { isolatedCookieNameFor } from '../auth/mode'\nimport { collectionBySlug } from '../ids'\nimport { closeAndRevoke } from '../session/close'\nimport { releaseLocks } from '../session/locks'\nimport { findOpenBySid, relationOf } from '../session/resolve'\nimport { boundSid } from '../types'\nimport {\n\tcallerSid,\n\tfail,\n\tgetOptions,\n\tisApiKeyStrategy,\n\tjson,\n\tprepareMutation,\n\trejectGate,\n} from './shared'\n\nexport const exitHandler = async (req: PayloadRequest): Promise<Response> => {\n\tconst gate = await prepareMutation(req)\n\tif (gate) {\n\t\treturn rejectGate(req, gate)\n\t}\n\n\tconst options = getOptions(req)\n\tconst user = req.user\n\tif (!user) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\tif (isApiKeyStrategy(user) || !callerSid(user)) {\n\t\treturn fail({ error: 'unsupportedAuth', req, status: 403 })\n\t}\n\n\tconst sid = boundSid(user, options.session.binding)\n\tif (!sid) {\n\t\treturn fail({ error: 'unsupportedAuth', req, status: 403 })\n\t}\n\n\tconst row = await findOpenBySid({ options, payload: req.payload, req, sid })\n\tif (!row) {\n\t\treturn fail({ error: 'notImpersonating', req, status: 409 })\n\t}\n\n\tconst impersonator = relationOf(row.impersonator)\n\tconst target = relationOf(row.target)\n\tconst targetCollection = target ? collectionBySlug(req.payload, target.collection) : undefined\n\tconst authConfig =\n\t\ttargetCollection?.config.auth ??\n\t\tcollectionBySlug(req.payload, req.payload.config.admin.user)?.config.auth\n\tif (!authConfig) {\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n\n\tconst cookiePrefix = req.payload.config.cookiePrefix\n\tconst expireHintAndClear = expireCookies({\n\t\tauthConfig,\n\t\tcookiePrefix,\n\t\tnames: [...options.cookies.clearOnSwitch, options.hintCookieName],\n\t})\n\n\tif (!impersonator || !collectionBySlug(req.payload, impersonator.collection)) {\n\t\tawait closeAndRevoke({\n\t\t\tendedBy: 'impersonatorGone',\n\t\t\toptions,\n\t\t\tpayload: req.payload,\n\t\t\trecord: row,\n\t\t\treq,\n\t\t})\n\t\tawait releaseLocks({ payload: req.payload, record: row, req })\n\t\treturn fail({\n\t\t\tcookies: [\n\t\t\t\texpirePayloadCookie({\n\t\t\t\t\tauthConfig,\n\t\t\t\t\tcookiePrefix,\n\t\t\t\t}),\n\t\t\t\t...expireHintAndClear,\n\t\t\t],\n\t\t\terror: 'impersonatorGone',\n\t\t\treq,\n\t\t\tstatus: 409,\n\t\t})\n\t}\n\n\tlet impersonatorDoc: { deletedAt?: string; lockUntil?: Date | string } | null = null\n\ttry {\n\t\timpersonatorDoc = (await req.payload.findByID({\n\t\t\tid: impersonator.id,\n\t\t\tcollection: impersonator.collection as CollectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})) as { deletedAt?: string; lockUntil?: Date | string }\n\t} catch {\n\t\timpersonatorDoc = null\n\t}\n\n\tif (!impersonatorDoc || impersonatorDoc.deletedAt) {\n\t\tawait closeAndRevoke({\n\t\t\tendedBy: 'impersonatorGone',\n\t\t\toptions,\n\t\t\tpayload: req.payload,\n\t\t\trecord: row,\n\t\t\treq,\n\t\t})\n\t\tawait releaseLocks({ payload: req.payload, record: row, req })\n\t\treturn fail({\n\t\t\tcookies: [\n\t\t\t\texpirePayloadCookie({\n\t\t\t\t\tauthConfig,\n\t\t\t\t\tcookiePrefix,\n\t\t\t\t}),\n\t\t\t\t...expireHintAndClear,\n\t\t\t],\n\t\t\terror: 'impersonatorGone',\n\t\t\treq,\n\t\t\tstatus: 409,\n\t\t})\n\t}\n\n\tif (row.mode === 'parallel') {\n\t\tconst closed = await closeAndRevoke({\n\t\t\tendedBy: 'exit',\n\t\t\toptions,\n\t\t\tpayload: req.payload,\n\t\t\trecord: row,\n\t\t\treq,\n\t\t})\n\t\tawait releaseLocks({ payload: req.payload, record: closed, req })\n\t\tlet isolatedName: string | undefined\n\t\tif (target) {\n\t\t\tconst targetUser = (await req.payload.db.findOne({\n\t\t\t\tcollection: target.collection as CollectionSlug,\n\t\t\t\treq,\n\t\t\t\twhere: { id: { equals: target.id } },\n\t\t\t})) as { collection?: string } | null\n\t\t\tif (targetUser) {\n\t\t\t\ttargetUser.collection = target.collection\n\t\t\t\tisolatedName = await isolatedCookieNameFor({\n\t\t\t\t\tcollection: target.collection as CollectionSlug,\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\tuser: targetUser as never,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn json({\n\t\t\tbody: { ok: true },\n\t\t\tcookies: [\n\t\t\t\t...(isolatedName ? [expireCookie({ authConfig, name: isolatedName })] : []),\n\t\t\t\texpireCookie({ authConfig, name: options.hintCookieName }),\n\t\t\t],\n\t\t\treq,\n\t\t\tstatus: 200,\n\t\t})\n\t}\n\n\tconst restored = await resignSession({\n\t\tcollection: impersonator.collection as CollectionSlug,\n\t\tpayload: req.payload,\n\t\treq,\n\t\tsid: row.impersonatorSid,\n\t\tuserId: impersonator.id,\n\t})\n\n\tif (!restored) {\n\t\tawait closeAndRevoke({\n\t\t\tendedBy: 'expired',\n\t\t\toptions,\n\t\t\tpayload: req.payload,\n\t\t\trecord: row,\n\t\t\treq,\n\t\t})\n\t\tawait releaseLocks({ payload: req.payload, record: row, req })\n\t\treturn fail({\n\t\t\tcookies: [\n\t\t\t\texpirePayloadCookie({\n\t\t\t\t\tauthConfig,\n\t\t\t\t\tcookiePrefix,\n\t\t\t\t}),\n\t\t\t\t...expireHintAndClear,\n\t\t\t],\n\t\t\terror: 'impersonatorSessionExpired',\n\t\t\treq,\n\t\t\tstatus: 409,\n\t\t})\n\t}\n\n\tconst closed = await closeAndRevoke({\n\t\tendedBy: 'exit',\n\t\toptions,\n\t\tpayload: req.payload,\n\t\trecord: row,\n\t\treq,\n\t})\n\tawait releaseLocks({ payload: req.payload, record: closed, req })\n\n\treturn json({\n\t\tbody: { ok: true },\n\t\tcookies: [\n\t\t\trestored.cookie,\n\t\t\texpireCookie({ authConfig, name: options.hintCookieName }),\n\t\t\t...expireCookies({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\tnames: options.cookies.clearOnSwitch,\n\t\t\t}),\n\t\t],\n\t\treq,\n\t\tstatus: 200,\n\t})\n}\n\nexport const exitEndpoint = (path: string): Endpoint => ({\n\thandler: exitHandler,\n\tmethod: 'post',\n\tpath,\n})\n"],"mappings":"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"}
|
package/dist/endpoints/start.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { asId, collectionBySlug, idsEqual, userTitle } from "../ids.js";
|
|
2
2
|
import { findOpenByImpersonatorSid, findOpenByTargetSid, openByImpersonatorSid, sameUser } from "../session/resolve.js";
|
|
3
|
-
import {
|
|
3
|
+
import { clearOnSwitchOnStart, generateHintCookie } from "../auth/cookies.js";
|
|
4
4
|
import { revokeSession } from "../auth/revoke.js";
|
|
5
5
|
import { closeRecord } from "../session/close.js";
|
|
6
6
|
import { boundSid } from "../types.js";
|
|
7
7
|
import { isStartableAuthCollection } from "../plugin/startable.js";
|
|
8
8
|
import { callerSid, defaultRedirect, fail, getOptions, isApiKeyStrategy, isLiteralTrue, json, prepareMutation, rejectGate, safeRedirect, trimReason } from "./shared.js";
|
|
9
9
|
import { issueSession } from "../auth/issue.js";
|
|
10
|
-
import { clearOnSwitchWithoutTenant, readTenantCookie, startTenantCookies } from "../auth/tenantCookie.js";
|
|
11
10
|
import { resolveTargetFilters, targetMatchesFilter } from "../access/filterTargets.js";
|
|
12
11
|
//#region src/endpoints/start.ts
|
|
13
12
|
const allowedTargets = (req, options) => new Set(Object.values(req.payload.collections).filter((collection) => isStartableAuthCollection(collection.config, options)).map((collection) => collection.config.slug));
|
|
@@ -201,7 +200,6 @@ const startHandler = async (req) => {
|
|
|
201
200
|
impersonatorLocale: locale,
|
|
202
201
|
impersonatorSid: sid,
|
|
203
202
|
impersonatorTitle: userTitle(req.payload, user.collection, user),
|
|
204
|
-
impersonatorTenantCookie: readTenantCookie(req.headers, req.payload.config.cookiePrefix),
|
|
205
203
|
ip: req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? void 0,
|
|
206
204
|
mode: minted.mode,
|
|
207
205
|
reason: options.reason === "off" ? void 0 : reason,
|
|
@@ -269,17 +267,12 @@ const startHandler = async (req) => {
|
|
|
269
267
|
name: options.hintCookieName,
|
|
270
268
|
value: String(row.id)
|
|
271
269
|
}),
|
|
272
|
-
...
|
|
270
|
+
...clearOnSwitchOnStart({
|
|
273
271
|
authConfig,
|
|
274
272
|
cookiePrefix,
|
|
273
|
+
except: [minted.cookieName],
|
|
275
274
|
mode: minted.mode,
|
|
276
|
-
options
|
|
277
|
-
target: readable
|
|
278
|
-
}),
|
|
279
|
-
...expireCookies({
|
|
280
|
-
authConfig,
|
|
281
|
-
cookiePrefix,
|
|
282
|
-
names: clearOnSwitchWithoutTenant(options.cookies.clearOnSwitch, cookiePrefix).filter((name) => name !== minted.cookieName)
|
|
275
|
+
names: options.cookies.clearOnSwitch
|
|
283
276
|
})
|
|
284
277
|
];
|
|
285
278
|
const redirect = safeRedirect(body.redirect, defaultRedirect(req, collection));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.js","names":[],"sources":["../../src/endpoints/start.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadRequest } from 'payload'\nimport { resolveTargetFilters, targetMatchesFilter } from '../access/filterTargets'\nimport { expireCookies, generateHintCookie } from '../auth/cookies'\nimport { issueSession } from '../auth/issue'\nimport { revokeSession } from '../auth/revoke'\nimport {\n\tclearOnSwitchWithoutTenant,\n\treadTenantCookie,\n\tstartTenantCookies,\n} from '../auth/tenantCookie'\nimport { asId, collectionBySlug, idsEqual, userTitle } from '../ids'\nimport { REASON_MAX_LENGTH } from '../plugin/constants'\nimport { isStartableAuthCollection } from '../plugin/startable'\nimport { closeRecord } from '../session/close'\nimport {\n\tfindOpenByImpersonatorSid,\n\tfindOpenByTargetSid,\n\topenByImpersonatorSid,\n\tsameUser,\n} from '../session/resolve'\nimport type { ImpersonationRecord, ResolvedOptions } from '../types'\nimport { boundSid } from '../types'\nimport {\n\tcallerSid,\n\tdefaultRedirect,\n\tfail,\n\tgetOptions,\n\tisApiKeyStrategy,\n\tisLiteralTrue,\n\tjson,\n\tprepareMutation,\n\trejectGate,\n\tsafeRedirect,\n\ttrimReason,\n} from './shared'\n\nconst allowedTargets = (req: PayloadRequest, options: ResolvedOptions): Set<string> =>\n\tnew Set(\n\t\tObject.values(req.payload.collections)\n\t\t\t.filter((collection) => isStartableAuthCollection(collection.config, options))\n\t\t\t.map((collection) => collection.config.slug)\n\t)\n\nexport const startHandler = async (req: PayloadRequest): Promise<Response> => {\n\tconst gate = await prepareMutation(req)\n\tif (gate) {\n\t\treturn rejectGate(req, gate)\n\t}\n\n\tconst options = getOptions(req)\n\tconst user = req.user\n\tif (!user) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\tif (isApiKeyStrategy(user) || !callerSid(user)) {\n\t\treturn fail({ error: 'unsupportedAuth', req, status: 403 })\n\t}\n\n\tconst sid = boundSid(user, options.session.binding)\n\tif (!sid) {\n\t\treturn fail({ error: 'unsupportedAuth', req, status: 403 })\n\t}\n\n\tconst nested =\n\t\t(await findOpenByTargetSid({ options, payload: req.payload, req, sid })) ??\n\t\t(await findOpenByImpersonatorSid({ options, payload: req.payload, req, sid }))\n\tif (nested) {\n\t\treturn fail({ error: 'alreadyImpersonating', req, status: 409 })\n\t}\n\n\tconst body = (req.data ?? {}) as Record<string, unknown>\n\tconst collection = typeof body.collection === 'string' ? body.collection : ''\n\tconst targetId = asId(body.id)\n\tif (!collection || targetId == null) {\n\t\treturn fail({ error: 'invalidBody', req, status: 400 })\n\t}\n\n\tif (!allowedTargets(req, options).has(collection)) {\n\t\treturn fail({ error: 'unsupportedCollection', req, status: 400 })\n\t}\n\n\tif (options.access.filterTargets) {\n\t\tconst filters = await resolveTargetFilters({\n\t\t\tcollections: [collection as CollectionSlug],\n\t\t\toptions,\n\t\t\treq,\n\t\t})\n\t\tconst filter = filters[collection]\n\t\tif (filter === undefined) {\n\t\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t\t}\n\t\tif (\n\t\t\t!(await targetMatchesFilter({\n\t\t\t\tcollection: collection as CollectionSlug,\n\t\t\t\tfilter,\n\t\t\t\treq,\n\t\t\t\ttargetId,\n\t\t\t}))\n\t\t) {\n\t\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t\t}\n\t}\n\n\tconst registered = collectionBySlug(req.payload, collection)\n\tif (!registered) {\n\t\treturn fail({ error: 'targetNotFound', req, status: 404 })\n\t}\n\n\tif (sameUser(user, collection, targetId)) {\n\t\treturn fail({ error: 'selfTarget', req, status: 400 })\n\t}\n\n\tconst supportsTrash = Boolean((registered.config as { trash?: boolean }).trash)\n\tlet existing: Record<string, unknown> | null = null\n\ttry {\n\t\texisting = (await req.payload.findByID({\n\t\t\tid: targetId,\n\t\t\tcollection: collection as CollectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t\t...(supportsTrash ? { trash: true } : {}),\n\t\t})) as unknown as Record<string, unknown>\n\t} catch {\n\t\texisting = null\n\t}\n\tif (!existing) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tlet readable: Record<string, unknown> | null = null\n\ttry {\n\t\treadable = (await req.payload.findByID({\n\t\t\tid: targetId,\n\t\t\tcollection: collection as CollectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: false,\n\t\t\treq,\n\t\t})) as unknown as Record<string, unknown>\n\t} catch {\n\t\treadable = null\n\t}\n\n\tlet allowed: unknown\n\ttry {\n\t\tallowed = await options.access.impersonate({\n\t\t\treq,\n\t\t\ttarget: readable ?? existing,\n\t\t\ttargetCollection: collection as CollectionSlug,\n\t\t})\n\t} catch {\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n\tif (!isLiteralTrue(allowed)) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tif (existing.deletedAt) {\n\t\treturn fail({ error: 'targetTrashed', req, status: 403 })\n\t}\n\tif (!readable) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tif (registered.config.auth.verify && readable._verified !== true) {\n\t\treturn fail({ error: 'targetUnverified', req, status: 403 })\n\t}\n\n\tconst reason = trimReason(body.reason, REASON_MAX_LENGTH)\n\tif (options.reason === 'required' && !reason) {\n\t\treturn fail({ error: 'reasonRequired', req, status: 400 })\n\t}\n\n\tconst issue = options.session.issue ?? issueSession\n\tconst revoke = options.session.revoke ?? revokeSession\n\tlet minted: Awaited<ReturnType<typeof issueSession>>\n\ttry {\n\t\tminted = await issue({\n\t\t\tcollection: collection as CollectionSlug,\n\t\t\tpayload: req.payload,\n\t\t\treq,\n\t\t\tuserId: targetId,\n\t\t})\n\t} catch (error) {\n\t\treq.payload.logger.error({\n\t\t\terr: error,\n\t\t\tmsg: '@10x-media/impersonation: start failed to mint',\n\t\t})\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n\n\tconst now = new Date()\n\tconst absoluteExpiresAt =\n\t\ttypeof options.maxDuration === 'number'\n\t\t\t? new Date(now.getTime() + options.maxDuration * 1000).toISOString()\n\t\t\t: undefined\n\n\tconst locale =\n\t\t(req as PayloadRequest & { i18n?: { language?: string } }).i18n?.language ??\n\t\treq.payload.config.i18n?.fallbackLanguage ??\n\t\t'en'\n\n\ttry {\n\t\tconst row = (await req.payload.create({\n\t\t\tcollection: options.collectionSlug,\n\t\t\tdata: {\n\t\t\t\tabsoluteExpiresAt,\n\t\t\t\timpersonator: { relationTo: user.collection, value: user.id },\n\t\t\t\timpersonatorEmail: typeof user.email === 'string' ? user.email : undefined,\n\t\t\t\timpersonatorLocale: locale,\n\t\t\t\timpersonatorSid: sid,\n\t\t\t\timpersonatorTitle: userTitle(\n\t\t\t\t\treq.payload,\n\t\t\t\t\tuser.collection,\n\t\t\t\t\tuser as unknown as Record<string, unknown>\n\t\t\t\t),\n\t\t\t\timpersonatorTenantCookie: readTenantCookie(req.headers, req.payload.config.cookiePrefix),\n\t\t\t\tip: req.headers.get('x-forwarded-for') ?? req.headers.get('x-real-ip') ?? undefined,\n\t\t\t\tmode: minted.mode,\n\t\t\t\treason: options.reason === 'off' ? undefined : reason,\n\t\t\t\tstartedAt: now.toISOString(),\n\t\t\t\ttarget: { relationTo: collection, value: targetId },\n\t\t\t\ttargetEmail:\n\t\t\t\t\ttypeof readable.email === 'string'\n\t\t\t\t\t\t? readable.email\n\t\t\t\t\t\t: typeof minted.user.email === 'string'\n\t\t\t\t\t\t\t? minted.user.email\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\ttargetLocked: Boolean(readable.lockUntil),\n\t\t\t\ttargetSid: minted.sid,\n\t\t\t\ttargetTitle: userTitle(req.payload, collection, readable),\n\t\t\t\tuserAgent: req.headers.get('user-agent') ?? undefined,\n\t\t\t} as never,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})) as unknown as ImpersonationRecord\n\n\t\tconst siblings = await req.payload.find({\n\t\t\tcollection: options.collectionSlug,\n\t\t\tdepth: 0,\n\t\t\tlimit: 2,\n\t\t\toverrideAccess: true,\n\t\t\tpagination: false,\n\t\t\treq,\n\t\t\tsort: 'startedAt',\n\t\t\twhere: openByImpersonatorSid(sid),\n\t\t})\n\t\tconst winner = [...(siblings.docs as unknown as ImpersonationRecord[])].sort((left, right) => {\n\t\t\tconst byTime = String(left.startedAt).localeCompare(String(right.startedAt))\n\t\t\treturn byTime !== 0 ? byTime : String(left.id).localeCompare(String(right.id))\n\t\t})[0]\n\t\tif (siblings.totalDocs > 1 && winner && !idsEqual(winner.id, row.id)) {\n\t\t\tawait closeRecord({ endedBy: 'failed', options, payload: req.payload, record: row, req })\n\t\t\tawait revoke({\n\t\t\t\tcollection: collection as CollectionSlug,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\tsid: minted.sid,\n\t\t\t\tuserId: targetId,\n\t\t\t})\n\t\t\treturn fail({ error: 'alreadyImpersonating', req, status: 409 })\n\t\t}\n\n\t\tawait options.onStart?.({ payload: req.payload, record: row, req })\n\n\t\tconst authConfig = registered.config.auth\n\t\tconst cookiePrefix = req.payload.config.cookiePrefix\n\t\tconst cookies = [\n\t\t\tminted.cookie,\n\t\t\tgenerateHintCookie({\n\t\t\t\tauthConfig,\n\t\t\t\tname: options.hintCookieName,\n\t\t\t\tvalue: String(row.id),\n\t\t\t}),\n\t\t\t...startTenantCookies({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\tmode: minted.mode,\n\t\t\t\toptions,\n\t\t\t\ttarget: readable,\n\t\t\t}),\n\t\t\t...expireCookies({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\tnames: clearOnSwitchWithoutTenant(options.cookies.clearOnSwitch, cookiePrefix).filter(\n\t\t\t\t\t(name) => name !== minted.cookieName\n\t\t\t\t),\n\t\t\t}),\n\t\t]\n\n\t\tconst redirect = safeRedirect(body.redirect, defaultRedirect(req, collection))\n\t\tconst bodyOut: Record<string, unknown> = {\n\t\t\texp: minted.exp,\n\t\t\tredirect,\n\t\t\tuser: readable,\n\t\t}\n\t\tif (!authConfig.removeTokenFromResponses) {\n\t\t\tbodyOut.token = minted.token\n\t\t}\n\n\t\treturn json({ body: bodyOut, cookies, req, status: 200 })\n\t} catch (error) {\n\t\treq.payload.logger.error({\n\t\t\terr: error,\n\t\t\tmsg: '@10x-media/impersonation: start failed after mint',\n\t\t})\n\t\ttry {\n\t\t\tawait revoke({\n\t\t\t\tcollection: collection as CollectionSlug,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\tsid: minted.sid,\n\t\t\t\tuserId: targetId,\n\t\t\t})\n\t\t} catch (revokeError) {\n\t\t\treq.payload.logger.error({\n\t\t\t\terr: revokeError,\n\t\t\t\tmsg: '@10x-media/impersonation: compensating revoke failed',\n\t\t\t})\n\t\t}\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n}\n\nexport const startEndpoint = (path: string): Endpoint => ({\n\thandler: startHandler,\n\tmethod: 'post',\n\tpath,\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"start.js","names":[],"sources":["../../src/endpoints/start.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadRequest } from 'payload'\nimport { resolveTargetFilters, targetMatchesFilter } from '../access/filterTargets'\nimport { clearOnSwitchOnStart, generateHintCookie } from '../auth/cookies'\nimport { issueSession } from '../auth/issue'\nimport { revokeSession } from '../auth/revoke'\nimport { asId, collectionBySlug, idsEqual, userTitle } from '../ids'\nimport { REASON_MAX_LENGTH } from '../plugin/constants'\nimport { isStartableAuthCollection } from '../plugin/startable'\nimport { closeRecord } from '../session/close'\nimport {\n\tfindOpenByImpersonatorSid,\n\tfindOpenByTargetSid,\n\topenByImpersonatorSid,\n\tsameUser,\n} from '../session/resolve'\nimport type { ImpersonationRecord, ResolvedOptions } from '../types'\nimport { boundSid } from '../types'\nimport {\n\tcallerSid,\n\tdefaultRedirect,\n\tfail,\n\tgetOptions,\n\tisApiKeyStrategy,\n\tisLiteralTrue,\n\tjson,\n\tprepareMutation,\n\trejectGate,\n\tsafeRedirect,\n\ttrimReason,\n} from './shared'\n\nconst allowedTargets = (req: PayloadRequest, options: ResolvedOptions): Set<string> =>\n\tnew Set(\n\t\tObject.values(req.payload.collections)\n\t\t\t.filter((collection) => isStartableAuthCollection(collection.config, options))\n\t\t\t.map((collection) => collection.config.slug)\n\t)\n\nexport const startHandler = async (req: PayloadRequest): Promise<Response> => {\n\tconst gate = await prepareMutation(req)\n\tif (gate) {\n\t\treturn rejectGate(req, gate)\n\t}\n\n\tconst options = getOptions(req)\n\tconst user = req.user\n\tif (!user) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\tif (isApiKeyStrategy(user) || !callerSid(user)) {\n\t\treturn fail({ error: 'unsupportedAuth', req, status: 403 })\n\t}\n\n\tconst sid = boundSid(user, options.session.binding)\n\tif (!sid) {\n\t\treturn fail({ error: 'unsupportedAuth', req, status: 403 })\n\t}\n\n\tconst nested =\n\t\t(await findOpenByTargetSid({ options, payload: req.payload, req, sid })) ??\n\t\t(await findOpenByImpersonatorSid({ options, payload: req.payload, req, sid }))\n\tif (nested) {\n\t\treturn fail({ error: 'alreadyImpersonating', req, status: 409 })\n\t}\n\n\tconst body = (req.data ?? {}) as Record<string, unknown>\n\tconst collection = typeof body.collection === 'string' ? body.collection : ''\n\tconst targetId = asId(body.id)\n\tif (!collection || targetId == null) {\n\t\treturn fail({ error: 'invalidBody', req, status: 400 })\n\t}\n\n\tif (!allowedTargets(req, options).has(collection)) {\n\t\treturn fail({ error: 'unsupportedCollection', req, status: 400 })\n\t}\n\n\tif (options.access.filterTargets) {\n\t\tconst filters = await resolveTargetFilters({\n\t\t\tcollections: [collection as CollectionSlug],\n\t\t\toptions,\n\t\t\treq,\n\t\t})\n\t\tconst filter = filters[collection]\n\t\tif (filter === undefined) {\n\t\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t\t}\n\t\tif (\n\t\t\t!(await targetMatchesFilter({\n\t\t\t\tcollection: collection as CollectionSlug,\n\t\t\t\tfilter,\n\t\t\t\treq,\n\t\t\t\ttargetId,\n\t\t\t}))\n\t\t) {\n\t\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t\t}\n\t}\n\n\tconst registered = collectionBySlug(req.payload, collection)\n\tif (!registered) {\n\t\treturn fail({ error: 'targetNotFound', req, status: 404 })\n\t}\n\n\tif (sameUser(user, collection, targetId)) {\n\t\treturn fail({ error: 'selfTarget', req, status: 400 })\n\t}\n\n\tconst supportsTrash = Boolean((registered.config as { trash?: boolean }).trash)\n\tlet existing: Record<string, unknown> | null = null\n\ttry {\n\t\texisting = (await req.payload.findByID({\n\t\t\tid: targetId,\n\t\t\tcollection: collection as CollectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t\t...(supportsTrash ? { trash: true } : {}),\n\t\t})) as unknown as Record<string, unknown>\n\t} catch {\n\t\texisting = null\n\t}\n\tif (!existing) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tlet readable: Record<string, unknown> | null = null\n\ttry {\n\t\treadable = (await req.payload.findByID({\n\t\t\tid: targetId,\n\t\t\tcollection: collection as CollectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: false,\n\t\t\treq,\n\t\t})) as unknown as Record<string, unknown>\n\t} catch {\n\t\treadable = null\n\t}\n\n\tlet allowed: unknown\n\ttry {\n\t\tallowed = await options.access.impersonate({\n\t\t\treq,\n\t\t\ttarget: readable ?? existing,\n\t\t\ttargetCollection: collection as CollectionSlug,\n\t\t})\n\t} catch {\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n\tif (!isLiteralTrue(allowed)) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tif (existing.deletedAt) {\n\t\treturn fail({ error: 'targetTrashed', req, status: 403 })\n\t}\n\tif (!readable) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tif (registered.config.auth.verify && readable._verified !== true) {\n\t\treturn fail({ error: 'targetUnverified', req, status: 403 })\n\t}\n\n\tconst reason = trimReason(body.reason, REASON_MAX_LENGTH)\n\tif (options.reason === 'required' && !reason) {\n\t\treturn fail({ error: 'reasonRequired', req, status: 400 })\n\t}\n\n\tconst issue = options.session.issue ?? issueSession\n\tconst revoke = options.session.revoke ?? revokeSession\n\tlet minted: Awaited<ReturnType<typeof issueSession>>\n\ttry {\n\t\tminted = await issue({\n\t\t\tcollection: collection as CollectionSlug,\n\t\t\tpayload: req.payload,\n\t\t\treq,\n\t\t\tuserId: targetId,\n\t\t})\n\t} catch (error) {\n\t\treq.payload.logger.error({\n\t\t\terr: error,\n\t\t\tmsg: '@10x-media/impersonation: start failed to mint',\n\t\t})\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n\n\tconst now = new Date()\n\tconst absoluteExpiresAt =\n\t\ttypeof options.maxDuration === 'number'\n\t\t\t? new Date(now.getTime() + options.maxDuration * 1000).toISOString()\n\t\t\t: undefined\n\n\tconst locale =\n\t\t(req as PayloadRequest & { i18n?: { language?: string } }).i18n?.language ??\n\t\treq.payload.config.i18n?.fallbackLanguage ??\n\t\t'en'\n\n\ttry {\n\t\tconst row = (await req.payload.create({\n\t\t\tcollection: options.collectionSlug,\n\t\t\tdata: {\n\t\t\t\tabsoluteExpiresAt,\n\t\t\t\timpersonator: { relationTo: user.collection, value: user.id },\n\t\t\t\timpersonatorEmail: typeof user.email === 'string' ? user.email : undefined,\n\t\t\t\timpersonatorLocale: locale,\n\t\t\t\timpersonatorSid: sid,\n\t\t\t\timpersonatorTitle: userTitle(\n\t\t\t\t\treq.payload,\n\t\t\t\t\tuser.collection,\n\t\t\t\t\tuser as unknown as Record<string, unknown>\n\t\t\t\t),\n\t\t\t\tip: req.headers.get('x-forwarded-for') ?? req.headers.get('x-real-ip') ?? undefined,\n\t\t\t\tmode: minted.mode,\n\t\t\t\treason: options.reason === 'off' ? undefined : reason,\n\t\t\t\tstartedAt: now.toISOString(),\n\t\t\t\ttarget: { relationTo: collection, value: targetId },\n\t\t\t\ttargetEmail:\n\t\t\t\t\ttypeof readable.email === 'string'\n\t\t\t\t\t\t? readable.email\n\t\t\t\t\t\t: typeof minted.user.email === 'string'\n\t\t\t\t\t\t\t? minted.user.email\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\ttargetLocked: Boolean(readable.lockUntil),\n\t\t\t\ttargetSid: minted.sid,\n\t\t\t\ttargetTitle: userTitle(req.payload, collection, readable),\n\t\t\t\tuserAgent: req.headers.get('user-agent') ?? undefined,\n\t\t\t} as never,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})) as unknown as ImpersonationRecord\n\n\t\tconst siblings = await req.payload.find({\n\t\t\tcollection: options.collectionSlug,\n\t\t\tdepth: 0,\n\t\t\tlimit: 2,\n\t\t\toverrideAccess: true,\n\t\t\tpagination: false,\n\t\t\treq,\n\t\t\tsort: 'startedAt',\n\t\t\twhere: openByImpersonatorSid(sid),\n\t\t})\n\t\tconst winner = [...(siblings.docs as unknown as ImpersonationRecord[])].sort((left, right) => {\n\t\t\tconst byTime = String(left.startedAt).localeCompare(String(right.startedAt))\n\t\t\treturn byTime !== 0 ? byTime : String(left.id).localeCompare(String(right.id))\n\t\t})[0]\n\t\tif (siblings.totalDocs > 1 && winner && !idsEqual(winner.id, row.id)) {\n\t\t\tawait closeRecord({ endedBy: 'failed', options, payload: req.payload, record: row, req })\n\t\t\tawait revoke({\n\t\t\t\tcollection: collection as CollectionSlug,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\tsid: minted.sid,\n\t\t\t\tuserId: targetId,\n\t\t\t})\n\t\t\treturn fail({ error: 'alreadyImpersonating', req, status: 409 })\n\t\t}\n\n\t\tawait options.onStart?.({ payload: req.payload, record: row, req })\n\n\t\tconst authConfig = registered.config.auth\n\t\tconst cookiePrefix = req.payload.config.cookiePrefix\n\t\tconst cookies = [\n\t\t\tminted.cookie,\n\t\t\tgenerateHintCookie({\n\t\t\t\tauthConfig,\n\t\t\t\tname: options.hintCookieName,\n\t\t\t\tvalue: String(row.id),\n\t\t\t}),\n\t\t\t...clearOnSwitchOnStart({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\texcept: [minted.cookieName],\n\t\t\t\tmode: minted.mode,\n\t\t\t\tnames: options.cookies.clearOnSwitch,\n\t\t\t}),\n\t\t]\n\n\t\tconst redirect = safeRedirect(body.redirect, defaultRedirect(req, collection))\n\t\tconst bodyOut: Record<string, unknown> = {\n\t\t\texp: minted.exp,\n\t\t\tredirect,\n\t\t\tuser: readable,\n\t\t}\n\t\tif (!authConfig.removeTokenFromResponses) {\n\t\t\tbodyOut.token = minted.token\n\t\t}\n\n\t\treturn json({ body: bodyOut, cookies, req, status: 200 })\n\t} catch (error) {\n\t\treq.payload.logger.error({\n\t\t\terr: error,\n\t\t\tmsg: '@10x-media/impersonation: start failed after mint',\n\t\t})\n\t\ttry {\n\t\t\tawait revoke({\n\t\t\t\tcollection: collection as CollectionSlug,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\tsid: minted.sid,\n\t\t\t\tuserId: targetId,\n\t\t\t})\n\t\t} catch (revokeError) {\n\t\t\treq.payload.logger.error({\n\t\t\t\terr: revokeError,\n\t\t\t\tmsg: '@10x-media/impersonation: compensating revoke failed',\n\t\t\t})\n\t\t}\n\t\treturn fail({ error: 'failed', req, status: 500 })\n\t}\n}\n\nexport const startEndpoint = (path: string): Endpoint => ({\n\thandler: startHandler,\n\tmethod: 'post',\n\tpath,\n})\n"],"mappings":";;;;;;;;;;;AA+BA,MAAM,kBAAkB,KAAqB,YAC5C,IAAI,IACH,OAAO,OAAO,IAAI,QAAQ,WAAW,EACnC,QAAQ,eAAe,0BAA0B,WAAW,QAAQ,OAAO,CAAC,EAC5E,KAAK,eAAe,WAAW,OAAO,IAAI,CAC7C;AAED,MAAa,eAAe,OAAO,QAA2C;CAC7E,MAAM,OAAO,MAAM,gBAAgB,GAAG;CACtC,IAAI,MACH,OAAO,WAAW,KAAK,IAAI;CAG5B,MAAM,UAAU,WAAW,GAAG;CAC9B,MAAM,OAAO,IAAI;CACjB,IAAI,CAAC,MACJ,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAErD,IAAI,iBAAiB,IAAI,KAAK,CAAC,UAAU,IAAI,GAC5C,OAAO,KAAK;EAAE,OAAO;EAAmB;EAAK,QAAQ;CAAI,CAAC;CAG3D,MAAM,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO;CAClD,IAAI,CAAC,KACJ,OAAO,KAAK;EAAE,OAAO;EAAmB;EAAK,QAAQ;CAAI,CAAC;CAM3D,IAFE,MAAM,oBAAoB;EAAE;EAAS,SAAS,IAAI;EAAS;EAAK;CAAI,CAAC,KACrE,MAAM,0BAA0B;EAAE;EAAS,SAAS,IAAI;EAAS;EAAK;CAAI,CAAC,GAE5E,OAAO,KAAK;EAAE,OAAO;EAAwB;EAAK,QAAQ;CAAI,CAAC;CAGhE,MAAM,OAAQ,IAAI,QAAQ,CAAC;CAC3B,MAAM,aAAa,OAAO,KAAK,eAAe,WAAW,KAAK,aAAa;CAC3E,MAAM,WAAW,KAAK,KAAK,EAAE;CAC7B,IAAI,CAAC,cAAc,YAAY,MAC9B,OAAO,KAAK;EAAE,OAAO;EAAe;EAAK,QAAQ;CAAI,CAAC;CAGvD,IAAI,CAAC,eAAe,KAAK,OAAO,EAAE,IAAI,UAAU,GAC/C,OAAO,KAAK;EAAE,OAAO;EAAyB;EAAK,QAAQ;CAAI,CAAC;CAGjE,IAAI,QAAQ,OAAO,eAAe;EAMjC,MAAM,UAAS,MALO,qBAAqB;GAC1C,aAAa,CAAC,UAA4B;GAC1C;GACA;EACD,CAAC,GACsB;EACvB,IAAI,WAAW,KAAA,GACd,OAAO,KAAK;GAAE,OAAO;GAAa;GAAK,QAAQ;EAAI,CAAC;EAErD,IACC,CAAE,MAAM,oBAAoB;GACf;GACZ;GACA;GACA;EACD,CAAC,GAED,OAAO,KAAK;GAAE,OAAO;GAAa;GAAK,QAAQ;EAAI,CAAC;CAEtD;CAEA,MAAM,aAAa,iBAAiB,IAAI,SAAS,UAAU;CAC3D,IAAI,CAAC,YACJ,OAAO,KAAK;EAAE,OAAO;EAAkB;EAAK,QAAQ;CAAI,CAAC;CAG1D,IAAI,SAAS,MAAM,YAAY,QAAQ,GACtC,OAAO,KAAK;EAAE,OAAO;EAAc;EAAK,QAAQ;CAAI,CAAC;CAGtD,MAAM,gBAAgB,QAAS,WAAW,OAA+B,KAAK;CAC9E,IAAI,WAA2C;CAC/C,IAAI;EACH,WAAY,MAAM,IAAI,QAAQ,SAAS;GACtC,IAAI;GACQ;GACZ,OAAO;GACP,gBAAgB;GAChB;GACA,GAAI,gBAAgB,EAAE,OAAO,KAAK,IAAI,CAAC;EACxC,CAAC;CACF,QAAQ;EACP,WAAW;CACZ;CACA,IAAI,CAAC,UACJ,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,IAAI,WAA2C;CAC/C,IAAI;EACH,WAAY,MAAM,IAAI,QAAQ,SAAS;GACtC,IAAI;GACQ;GACZ,OAAO;GACP,gBAAgB;GAChB;EACD,CAAC;CACF,QAAQ;EACP,WAAW;CACZ;CAEA,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,QAAQ,OAAO,YAAY;GAC1C;GACA,QAAQ,YAAY;GACpB,kBAAkB;EACnB,CAAC;CACF,QAAQ;EACP,OAAO,KAAK;GAAE,OAAO;GAAU;GAAK,QAAQ;EAAI,CAAC;CAClD;CACA,IAAI,CAAC,cAAc,OAAO,GACzB,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,IAAI,SAAS,WACZ,OAAO,KAAK;EAAE,OAAO;EAAiB;EAAK,QAAQ;CAAI,CAAC;CAEzD,IAAI,CAAC,UACJ,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,IAAI,WAAW,OAAO,KAAK,UAAU,SAAS,cAAc,MAC3D,OAAO,KAAK;EAAE,OAAO;EAAoB;EAAK,QAAQ;CAAI,CAAC;CAG5D,MAAM,SAAS,WAAW,KAAK,QAAA,GAAyB;CACxD,IAAI,QAAQ,WAAW,cAAc,CAAC,QACrC,OAAO,KAAK;EAAE,OAAO;EAAkB;EAAK,QAAQ;CAAI,CAAC;CAG1D,MAAM,QAAQ,QAAQ,QAAQ,SAAS;CACvC,MAAM,SAAS,QAAQ,QAAQ,UAAU;CACzC,IAAI;CACJ,IAAI;EACH,SAAS,MAAM,MAAM;GACR;GACZ,SAAS,IAAI;GACb;GACA,QAAQ;EACT,CAAC;CACF,SAAS,OAAO;EACf,IAAI,QAAQ,OAAO,MAAM;GACxB,KAAK;GACL,KAAK;EACN,CAAC;EACD,OAAO,KAAK;GAAE,OAAO;GAAU;GAAK,QAAQ;EAAI,CAAC;CAClD;CAEA,MAAM,sBAAM,IAAI,KAAK;CACrB,MAAM,oBACL,OAAO,QAAQ,gBAAgB,WAC5B,IAAI,KAAK,IAAI,QAAQ,IAAI,QAAQ,cAAc,GAAI,EAAE,YAAY,IACjE,KAAA;CAEJ,MAAM,SACJ,IAA0D,MAAM,YACjE,IAAI,QAAQ,OAAO,MAAM,oBACzB;CAED,IAAI;EACH,MAAM,MAAO,MAAM,IAAI,QAAQ,OAAO;GACrC,YAAY,QAAQ;GACpB,MAAM;IACL;IACA,cAAc;KAAE,YAAY,KAAK;KAAY,OAAO,KAAK;IAAG;IAC5D,mBAAmB,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;IACjE,oBAAoB;IACpB,iBAAiB;IACjB,mBAAmB,UAClB,IAAI,SACJ,KAAK,YACL,IACD;IACA,IAAI,IAAI,QAAQ,IAAI,iBAAiB,KAAK,IAAI,QAAQ,IAAI,WAAW,KAAK,KAAA;IAC1E,MAAM,OAAO;IACb,QAAQ,QAAQ,WAAW,QAAQ,KAAA,IAAY;IAC/C,WAAW,IAAI,YAAY;IAC3B,QAAQ;KAAE,YAAY;KAAY,OAAO;IAAS;IAClD,aACC,OAAO,SAAS,UAAU,WACvB,SAAS,QACT,OAAO,OAAO,KAAK,UAAU,WAC5B,OAAO,KAAK,QACZ,KAAA;IACL,cAAc,QAAQ,SAAS,SAAS;IACxC,WAAW,OAAO;IAClB,aAAa,UAAU,IAAI,SAAS,YAAY,QAAQ;IACxD,WAAW,IAAI,QAAQ,IAAI,YAAY,KAAK,KAAA;GAC7C;GACA,OAAO;GACP,gBAAgB;GAChB;EACD,CAAC;EAED,MAAM,WAAW,MAAM,IAAI,QAAQ,KAAK;GACvC,YAAY,QAAQ;GACpB,OAAO;GACP,OAAO;GACP,gBAAgB;GAChB,YAAY;GACZ;GACA,MAAM;GACN,OAAO,sBAAsB,GAAG;EACjC,CAAC;EACD,MAAM,SAAS,CAAC,GAAI,SAAS,IAAyC,EAAE,MAAM,MAAM,UAAU;GAC7F,MAAM,SAAS,OAAO,KAAK,SAAS,EAAE,cAAc,OAAO,MAAM,SAAS,CAAC;GAC3E,OAAO,WAAW,IAAI,SAAS,OAAO,KAAK,EAAE,EAAE,cAAc,OAAO,MAAM,EAAE,CAAC;EAC9E,CAAC,EAAE;EACH,IAAI,SAAS,YAAY,KAAK,UAAU,CAAC,SAAS,OAAO,IAAI,IAAI,EAAE,GAAG;GACrE,MAAM,YAAY;IAAE,SAAS;IAAU;IAAS,SAAS,IAAI;IAAS,QAAQ;IAAK;GAAI,CAAC;GACxF,MAAM,OAAO;IACA;IACZ,SAAS,IAAI;IACb;IACA,KAAK,OAAO;IACZ,QAAQ;GACT,CAAC;GACD,OAAO,KAAK;IAAE,OAAO;IAAwB;IAAK,QAAQ;GAAI,CAAC;EAChE;EAEA,MAAM,QAAQ,UAAU;GAAE,SAAS,IAAI;GAAS,QAAQ;GAAK;EAAI,CAAC;EAElE,MAAM,aAAa,WAAW,OAAO;EACrC,MAAM,eAAe,IAAI,QAAQ,OAAO;EACxC,MAAM,UAAU;GACf,OAAO;GACP,mBAAmB;IAClB;IACA,MAAM,QAAQ;IACd,OAAO,OAAO,IAAI,EAAE;GACrB,CAAC;GACD,GAAG,qBAAqB;IACvB;IACA;IACA,QAAQ,CAAC,OAAO,UAAU;IAC1B,MAAM,OAAO;IACb,OAAO,QAAQ,QAAQ;GACxB,CAAC;EACF;EAEA,MAAM,WAAW,aAAa,KAAK,UAAU,gBAAgB,KAAK,UAAU,CAAC;EAC7E,MAAM,UAAmC;GACxC,KAAK,OAAO;GACZ;GACA,MAAM;EACP;EACA,IAAI,CAAC,WAAW,0BACf,QAAQ,QAAQ,OAAO;EAGxB,OAAO,KAAK;GAAE,MAAM;GAAS;GAAS;GAAK,QAAQ;EAAI,CAAC;CACzD,SAAS,OAAO;EACf,IAAI,QAAQ,OAAO,MAAM;GACxB,KAAK;GACL,KAAK;EACN,CAAC;EACD,IAAI;GACH,MAAM,OAAO;IACA;IACZ,SAAS,IAAI;IACb;IACA,KAAK,OAAO;IACZ,QAAQ;GACT,CAAC;EACF,SAAS,aAAa;GACrB,IAAI,QAAQ,OAAO,MAAM;IACxB,KAAK;IACL,KAAK;GACN,CAAC;EACF;EACA,OAAO,KAAK;GAAE,OAAO;GAAU;GAAK,QAAQ;EAAI,CAAC;CAClD;AACD;AAEA,MAAa,iBAAiB,UAA4B;CACzD,SAAS;CACT,QAAQ;CACR;AACD"}
|
package/dist/plugin/constants.js
CHANGED
|
@@ -7,7 +7,11 @@ const DEFAULT_COLLECTION_SLUG = "impersonation-sessions";
|
|
|
7
7
|
const DEFAULT_API_PATH = "/impersonation";
|
|
8
8
|
const DEFAULT_HINT_COOKIE = "impersonation-hint";
|
|
9
9
|
const DEFAULT_COOKIE_PREFIX = "payload";
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `@payloadcms/plugin-multi-tenant` hardcodes `payload-tenant` and ignores
|
|
12
|
+
* `cookiePrefix`, so the default does not follow the prefix either.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_CLEAR_ON_SWITCH = ["payload-tenant"];
|
|
11
15
|
/** Client POSTs that busy-gate a control abort after this. */
|
|
12
16
|
const CLIENT_FETCH_TIMEOUT_MS = 15e3;
|
|
13
17
|
const ADMIN_GROUP = "System";
|
|
@@ -15,6 +19,6 @@ const ADMIN_GROUP = "System";
|
|
|
15
19
|
const IMPERSONATION_SID_PREFIX = "imp_";
|
|
16
20
|
const RESERVED_API_SEGMENTS = ["graphql", "graphql-playground"];
|
|
17
21
|
//#endregion
|
|
18
|
-
export { ADMIN_GROUP, CLIENT_FETCH_TIMEOUT_MS, DEFAULT_API_PATH, DEFAULT_COLLECTION_SLUG, DEFAULT_COOKIE_PREFIX, DEFAULT_HINT_COOKIE, IMPERSONATION_SID_PREFIX, PLUGIN_SLUG, REGISTRY_KEY, RESERVED_API_SEGMENTS
|
|
22
|
+
export { ADMIN_GROUP, CLIENT_FETCH_TIMEOUT_MS, DEFAULT_API_PATH, DEFAULT_CLEAR_ON_SWITCH, DEFAULT_COLLECTION_SLUG, DEFAULT_COOKIE_PREFIX, DEFAULT_HINT_COOKIE, IMPERSONATION_SID_PREFIX, PLUGIN_SLUG, REGISTRY_KEY, RESERVED_API_SEGMENTS };
|
|
19
23
|
|
|
20
24
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../src/plugin/constants.ts"],"sourcesContent":["/** This plugin's registered slug, used to find its options on a booted config. */\nexport const PLUGIN_SLUG = '@10x-media/impersonation'\n\n/** `config.custom` key the resolved options live under. */\nexport const REGISTRY_KEY = PLUGIN_SLUG\n\nexport const DEFAULT_COLLECTION_SLUG = 'impersonation-sessions'\nexport const DEFAULT_API_PATH = '/impersonation'\nexport const DEFAULT_HINT_COOKIE = 'impersonation-hint'\nexport const DEFAULT_COOKIE_PREFIX = 'payload'\
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/plugin/constants.ts"],"sourcesContent":["/** This plugin's registered slug, used to find its options on a booted config. */\nexport const PLUGIN_SLUG = '@10x-media/impersonation'\n\n/** `config.custom` key the resolved options live under. */\nexport const REGISTRY_KEY = PLUGIN_SLUG\n\nexport const DEFAULT_COLLECTION_SLUG = 'impersonation-sessions'\nexport const DEFAULT_API_PATH = '/impersonation'\nexport const DEFAULT_HINT_COOKIE = 'impersonation-hint'\nexport const DEFAULT_COOKIE_PREFIX = 'payload'\n/**\n * `@payloadcms/plugin-multi-tenant` hardcodes `payload-tenant` and ignores\n * `cookiePrefix`, so the default does not follow the prefix either.\n */\nexport const DEFAULT_CLEAR_ON_SWITCH = ['payload-tenant']\nexport const REASON_MAX_LENGTH = 500\n/** Client POSTs that busy-gate a control abort after this. */\nexport const CLIENT_FETCH_TIMEOUT_MS = 15_000\nexport const ADMIN_GROUP = 'System'\n/** Prefixed onto minted target sids so decorateAuth can skip the DB on normal traffic. */\nexport const IMPERSONATION_SID_PREFIX = 'imp_'\nexport const SWITCHER_PAGE_SIZE = 20\n\nexport const RESERVED_API_SEGMENTS = ['graphql', 'graphql-playground'] as const\n"],"mappings":";;AACA,MAAa,cAAc;;AAG3B,MAAa,eAAe;AAE5B,MAAa,0BAA0B;AACvC,MAAa,mBAAmB;AAChC,MAAa,sBAAsB;AACnC,MAAa,wBAAwB;;;;;AAKrC,MAAa,0BAA0B,CAAC,gBAAgB;;AAGxD,MAAa,0BAA0B;AACvC,MAAa,cAAc;;AAE3B,MAAa,2BAA2B;AAGxC,MAAa,wBAAwB,CAAC,WAAW,oBAAoB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_CLEAR_ON_SWITCH, RESERVED_API_SEGMENTS } from "./constants.js";
|
|
2
2
|
//#region src/plugin/normalizeOptions.ts
|
|
3
3
|
const PREFIX = "@10x-media/impersonation:";
|
|
4
4
|
const resolveAdminUserSlug = (config) => config.admin?.user ?? (config.collections ?? []).find(({ auth }) => Boolean(auth))?.slug ?? "users";
|
|
@@ -63,7 +63,7 @@ const normalizeOptions = (options, config) => {
|
|
|
63
63
|
apiPath,
|
|
64
64
|
collectionSlug,
|
|
65
65
|
collectionOverrides: options.collection?.overrides,
|
|
66
|
-
cookies: { clearOnSwitch: options.cookies?.clearOnSwitch ??
|
|
66
|
+
cookies: { clearOnSwitch: options.cookies?.clearOnSwitch ?? [...DEFAULT_CLEAR_ON_SWITCH] },
|
|
67
67
|
decorateRequests: options.decorateRequests !== false,
|
|
68
68
|
hintCookieName,
|
|
69
69
|
maxDuration: options.maxDuration,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalizeOptions.js","names":[],"sources":["../../src/plugin/normalizeOptions.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Config } from 'payload'\n\nimport type { EnabledOptions, ResolvedOptions, ResolvedUi } from '../types'\nimport {\n\tDEFAULT_API_PATH,\n\tDEFAULT_COLLECTION_SLUG,\n\tDEFAULT_COOKIE_PREFIX,\n\tDEFAULT_HINT_COOKIE,\n\
|
|
1
|
+
{"version":3,"file":"normalizeOptions.js","names":[],"sources":["../../src/plugin/normalizeOptions.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Config } from 'payload'\n\nimport type { EnabledOptions, ResolvedOptions, ResolvedUi } from '../types'\nimport {\n\tDEFAULT_API_PATH,\n\tDEFAULT_CLEAR_ON_SWITCH,\n\tDEFAULT_COLLECTION_SLUG,\n\tDEFAULT_COOKIE_PREFIX,\n\tDEFAULT_HINT_COOKIE,\n\tRESERVED_API_SEGMENTS,\n} from './constants'\n\nconst PREFIX = '@10x-media/impersonation:'\n\nexport const resolveAdminUserSlug = (config: Config): string =>\n\tconfig.admin?.user ??\n\t(config.collections ?? []).find(({ auth }) => Boolean(auth))?.slug ??\n\t'users'\n\nconst authObject = (collection: CollectionConfig | undefined) => {\n\tif (!collection?.auth) {\n\t\treturn null\n\t}\n\treturn typeof collection.auth === 'object' ? collection.auth : {}\n}\n\nconst firstApiSegment = (apiPath: string): string =>\n\tapiPath.replace(/^\\//, '').split('/').filter(Boolean)[0] ?? ''\n\nconst resolveUi = (ui: EnabledOptions['ui']): ResolvedUi => {\n\tif (ui === false) {\n\t\treturn {\n\t\t\tbar: false,\n\t\t\tcardEmail: false,\n\t\t\tdocumentAction: false,\n\t\t\theaderAction: false,\n\t\t\trecordAction: false,\n\t\t\tsessionsCollection: false,\n\t\t\tuserLabel: 'title',\n\t\t}\n\t}\n\treturn {\n\t\tbar: ui?.bar !== false,\n\t\tcard: ui?.card,\n\t\tcardEmail: ui?.cardEmail !== false,\n\t\tdocumentAction: ui?.documentAction !== false,\n\t\theaderAction: ui?.headerAction !== false,\n\t\trecordAction: ui?.recordAction !== false,\n\t\tsessionsCollection: ui?.sessionsCollection === true,\n\t\tuserLabel: ui?.userLabel === 'email' ? 'email' : 'title',\n\t}\n}\n\n/**\n * Fill defaults and refuse a config that cannot bind or revoke sessions.\n * `disabled: true` never reaches here.\n */\nexport const normalizeOptions = (options: EnabledOptions, config: Config): ResolvedOptions => {\n\tif (typeof options.access?.impersonate !== 'function') {\n\t\tthrow new Error(`${PREFIX} access.impersonate must be a function`)\n\t}\n\n\tconst cookiePrefix = config.cookiePrefix ?? DEFAULT_COOKIE_PREFIX\n\tconst collectionSlug = (options.collectionSlug ?? DEFAULT_COLLECTION_SLUG) as CollectionSlug\n\tconst apiPath = options.apiPath ?? DEFAULT_API_PATH\n\tconst hintCookieName = options.hintCookieName ?? DEFAULT_HINT_COOKIE\n\n\tif (hintCookieName.startsWith(cookiePrefix)) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} hintCookieName \"${hintCookieName}\" must not start with cookiePrefix \"${cookiePrefix}\". @payloadcms/next refresh/logout take the first cookie whose name starts with the prefix.`\n\t\t)\n\t}\n\n\tconst segment = firstApiSegment(apiPath)\n\tconst occupied = new Set<string>([\n\t\t...RESERVED_API_SEGMENTS,\n\t\tcollectionSlug,\n\t\t...(config.collections ?? []).map(({ slug }) => slug),\n\t\t...(config.globals ?? []).map(({ slug }) => slug),\n\t])\n\tif (!segment || occupied.has(segment)) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} apiPath \"${apiPath}\" collides with a collection, global, or reserved route segment \"${segment}\". handleEndpoints would treat it as that collection and drop these endpoints.`\n\t\t)\n\t}\n\n\tconst adminSlug = resolveAdminUserSlug(config)\n\tconst adminCollection = (config.collections ?? []).find(({ slug }) => slug === adminSlug)\n\tconst adminAuth = authObject(adminCollection)\n\tif (adminAuth?.useSessions === false) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} the admin collection \"${adminSlug}\" has useSessions: false, so there is no sid to bind or revoke.`\n\t\t)\n\t}\n\tif (adminAuth?.disableLocalStrategy) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} the admin collection \"${adminSlug}\" has disableLocalStrategy, so the plugin cannot mint or restore a session.`\n\t\t)\n\t}\n\n\tconst deleteAfterDays = options.retention ? options.retention.deleteAfterDays : undefined\n\tif (\n\t\toptions.retention &&\n\t\t(typeof deleteAfterDays !== 'number' ||\n\t\t\t!Number.isFinite(deleteAfterDays) ||\n\t\t\tdeleteAfterDays <= 0)\n\t) {\n\t\tthrow new Error(`${PREFIX} retention.deleteAfterDays must be a positive finite number.`)\n\t}\n\n\treturn {\n\t\taccess: {\n\t\t\tfilterTargets: options.access.filterTargets,\n\t\t\timpersonate: options.access.impersonate,\n\t\t\treadRecords: options.access.readRecords ?? (() => false),\n\t\t\tterminate: options.access.terminate ?? (() => false),\n\t\t},\n\t\tapiPath,\n\t\tcollectionSlug,\n\t\tcollectionOverrides: options.collection?.overrides,\n\t\tcookies: {\n\t\t\tclearOnSwitch: options.cookies?.clearOnSwitch ?? [...DEFAULT_CLEAR_ON_SWITCH],\n\t\t},\n\t\tdecorateRequests: options.decorateRequests !== false,\n\t\thintCookieName,\n\t\tmaxDuration: options.maxDuration,\n\t\tonEnd: options.onEnd,\n\t\tonStart: options.onStart,\n\t\treason: options.reason ?? 'off',\n\t\tretention: options.retention ? options.retention : undefined,\n\t\tsecurity: { trustedOrigins: options.security?.trustedOrigins ?? [] },\n\t\tsession: options.session ?? {},\n\t\ttargets: options.targets,\n\t\tui: resolveUi(options.ui),\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registerCollection.js","names":[],"sources":["../../src/plugin/registerCollection.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Config } from 'payload'\n\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ResolvedOptions } from '../types'\nimport { ADMIN_GROUP } from './constants'\n\nconst deny = () => false\n\nexport const buildRecordsCollection = ({\n\tauthSlugs,\n\toptions,\n}: {\n\tauthSlugs: CollectionSlug[]\n\toptions: ResolvedOptions\n}): CollectionConfig => {\n\tconst relationTo: CollectionSlug[] = authSlugs.length > 0 ? authSlugs : ['users']\n\n\treturn {\n\t\tslug: options.collectionSlug,\n\t\taccess: {\n\t\t\tcreate: deny,\n\t\t\tdelete: deny,\n\t\t\tread: options.access.readRecords,\n\t\t\tupdate: deny,\n\t\t},\n\t\tadmin: {\n\t\t\tdefaultColumns: ['target', 'impersonator', 'startedAt', 'endedAt', 'endedBy'],\n\t\t\tgroup: ADMIN_GROUP,\n\t\t\thidden: !options.ui.sessionsCollection,\n\t\t\tuseAsTitle: 'targetTitle',\n\t\t\t...(options.ui.recordAction\n\t\t\t\t? {\n\t\t\t\t\t\tcomponents: {\n\t\t\t\t\t\t\tedit: {\n\t\t\t\t\t\t\t\tbeforeDocumentControls: ['@10x-media/impersonation/client#EndSessionMenuItem'],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t},\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'impersonator',\n\t\t\t\ttype: 'relationship',\n\t\t\t\tlabel: labelForKey(keys.fieldImpersonator),\n\t\t\t\trelationTo,\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'target',\n\t\t\t\ttype: 'relationship',\n\t\t\t\tlabel: labelForKey(keys.fieldTarget),\n\t\t\t\trelationTo,\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{ name: 'impersonatorEmail', type: 'text', index: true },\n\t\t\t{ name: 'targetEmail', type: 'text', index: true },\n\t\t\t{ name: 'impersonatorTitle', type: 'text' },\n\t\t\t{ name: 'targetTitle', type: 'text' },\n\t\t\t{\n\t\t\t\tname: 'mode',\n\t\t\t\ttype: 'select',\n\t\t\t\tlabel: labelForKey(keys.fieldMode),\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: 'swap', value: 'swap' },\n\t\t\t\t\t{ label: 'parallel', value: 'parallel' },\n\t\t\t\t],\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{ name: 'targetSid', type: 'text', index: true, required: true },\n\t\t\t{ name: 'impersonatorSid', type: 'text', index: true, required: true },\n\t\t\t{
|
|
1
|
+
{"version":3,"file":"registerCollection.js","names":[],"sources":["../../src/plugin/registerCollection.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Config } from 'payload'\n\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ResolvedOptions } from '../types'\nimport { ADMIN_GROUP } from './constants'\n\nconst deny = () => false\n\nexport const buildRecordsCollection = ({\n\tauthSlugs,\n\toptions,\n}: {\n\tauthSlugs: CollectionSlug[]\n\toptions: ResolvedOptions\n}): CollectionConfig => {\n\tconst relationTo: CollectionSlug[] = authSlugs.length > 0 ? authSlugs : ['users']\n\n\treturn {\n\t\tslug: options.collectionSlug,\n\t\taccess: {\n\t\t\tcreate: deny,\n\t\t\tdelete: deny,\n\t\t\tread: options.access.readRecords,\n\t\t\tupdate: deny,\n\t\t},\n\t\tadmin: {\n\t\t\tdefaultColumns: ['target', 'impersonator', 'startedAt', 'endedAt', 'endedBy'],\n\t\t\tgroup: ADMIN_GROUP,\n\t\t\thidden: !options.ui.sessionsCollection,\n\t\t\tuseAsTitle: 'targetTitle',\n\t\t\t...(options.ui.recordAction\n\t\t\t\t? {\n\t\t\t\t\t\tcomponents: {\n\t\t\t\t\t\t\tedit: {\n\t\t\t\t\t\t\t\tbeforeDocumentControls: ['@10x-media/impersonation/client#EndSessionMenuItem'],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t},\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'impersonator',\n\t\t\t\ttype: 'relationship',\n\t\t\t\tlabel: labelForKey(keys.fieldImpersonator),\n\t\t\t\trelationTo,\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'target',\n\t\t\t\ttype: 'relationship',\n\t\t\t\tlabel: labelForKey(keys.fieldTarget),\n\t\t\t\trelationTo,\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{ name: 'impersonatorEmail', type: 'text', index: true },\n\t\t\t{ name: 'targetEmail', type: 'text', index: true },\n\t\t\t{ name: 'impersonatorTitle', type: 'text' },\n\t\t\t{ name: 'targetTitle', type: 'text' },\n\t\t\t{\n\t\t\t\tname: 'mode',\n\t\t\t\ttype: 'select',\n\t\t\t\tlabel: labelForKey(keys.fieldMode),\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: 'swap', value: 'swap' },\n\t\t\t\t\t{ label: 'parallel', value: 'parallel' },\n\t\t\t\t],\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{ name: 'targetSid', type: 'text', index: true, required: true },\n\t\t\t{ name: 'impersonatorSid', type: 'text', index: true, required: true },\n\t\t\t{\n\t\t\t\tname: 'startedAt',\n\t\t\t\ttype: 'date',\n\t\t\t\tlabel: labelForKey(keys.fieldStartedAt),\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{ name: 'endedAt', type: 'date', index: true, label: labelForKey(keys.fieldEndedAt) },\n\t\t\t{\n\t\t\t\tname: 'endedBy',\n\t\t\t\ttype: 'select',\n\t\t\t\tlabel: labelForKey(keys.fieldEndedBy),\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: 'exit', value: 'exit' },\n\t\t\t\t\t{ label: 'logout', value: 'logout' },\n\t\t\t\t\t{ label: 'terminated', value: 'terminated' },\n\t\t\t\t\t{ label: 'expired', value: 'expired' },\n\t\t\t\t\t{ label: 'impersonatorGone', value: 'impersonatorGone' },\n\t\t\t\t\t{ label: 'targetGone', value: 'targetGone' },\n\t\t\t\t\t{ label: 'failed', value: 'failed' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'reason',\n\t\t\t\tmaxLength: 500,\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.fieldReason),\n\t\t\t},\n\t\t\t{ name: 'ip', type: 'text' },\n\t\t\t{ name: 'userAgent', type: 'text' },\n\t\t\t{ name: 'impersonatorLocale', type: 'text' },\n\t\t\t{ name: 'absoluteExpiresAt', type: 'date' },\n\t\t\t{ name: 'targetLocked', type: 'checkbox' },\n\t\t],\n\t\tindexes: [{ fields: ['targetSid', 'endedAt'] }, { fields: ['impersonatorSid', 'endedAt'] }],\n\t\tlabels: {\n\t\t\tplural: labelForKey(keys.collectionPlural),\n\t\t\tsingular: labelForKey(keys.collectionSingular),\n\t\t},\n\t\ttimestamps: true,\n\t}\n}\n\nexport const registerCollection = (config: Config, options: ResolvedOptions): void => {\n\tconst authSlugs = (config.collections ?? [])\n\t\t.filter((collection) => Boolean(collection.auth))\n\t\t.map(({ slug }) => slug as CollectionSlug)\n\tconst built = buildRecordsCollection({ authSlugs, options })\n\tconfig.collections = [\n\t\t...(config.collections ?? []),\n\t\toptions.collectionOverrides ? options.collectionOverrides(built) : built,\n\t]\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registerRetention.js","names":[],"sources":["../../src/plugin/registerRetention.ts"],"sourcesContent":["import type { Config, TaskConfig } from 'payload'\n\nimport { closeStaleImpersonations } from '../session/closeStale'\nimport type { ResolvedOptions } from '../types'\n\nconst DEFAULT_CRON = '0 3 * * *'\nconst DEFAULT_QUEUE = 'impersonation-retention'\nconst PAGE = 100\n\n/**\n * Opt-in job: close stale open rows, then delete closed rows older than\n * `deleteAfterDays`. The host must run the queue.\n */\nexport const registerRetention = (config: Config, options: ResolvedOptions): void => {\n\tconst retention = options.retention\n\tif (!retention) {\n\t\treturn\n\t}\n\n\tconst task = {\n\t\tslug: 'impersonationRetention',\n\t\tschedule: [{ cron: retention.cron ?? DEFAULT_CRON, queue: retention.queue ?? DEFAULT_QUEUE }],\n\t\thandler: async ({ req }) => {\n\t\t\tconst closed = await closeStaleImpersonations(req.payload)\n\t\t\tconst cutoff = new Date(Date.now() - retention.deleteAfterDays * 86_400_000).toISOString()\n\t\t\tlet deleted = 0\n\t\t\tfor (;;) {\n\t\t\t\tconst { docs } = await req.payload.find({\n\t\t\t\t\tcollection: options.collectionSlug,\n\t\t\t\t\tdepth: 0,\n\t\t\t\t\tlimit: PAGE,\n\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\tpagination: false,\n\t\t\t\t\twhere: {\n\t\t\t\t\t\tand: [{ endedAt: { exists: true } }, { endedAt: { less_than_equal: cutoff } }],\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif (docs.length === 0) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfor (const doc of docs) {\n\t\t\t\t\tawait req.payload.delete({\n\t\t\t\t\t\tcollection: options.collectionSlug,\n\t\t\t\t\t\tid: doc.id,\n\t\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\t})\n\t\t\t\t\tdeleted += 1\n\t\t\t\t}\n\t\t\t\tif (docs.length < PAGE) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { output: { closed, deleted } }\n\t\t},\n\t} as TaskConfig\n\n\tconfig.jobs = {\n\t\t...config.jobs,\n\t\ttasks: [...(config.jobs?.tasks ?? []), task],\n\t}\n}\n"],"mappings":";;AAKA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AACtB,MAAM,OAAO;;;;;AAMb,MAAa,qBAAqB,QAAgB,YAAmC;CACpF,MAAM,YAAY,QAAQ;CAC1B,IAAI,CAAC,WACJ;CAGD,MAAM,OAAO;EACZ,MAAM;EACN,UAAU,CAAC;GAAE,MAAM,UAAU,QAAQ;GAAc,OAAO,UAAU,SAAS;EAAc,CAAC;EAC5F,SAAS,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"registerRetention.js","names":[],"sources":["../../src/plugin/registerRetention.ts"],"sourcesContent":["import type { Config, PayloadRequest, TaskConfig } from 'payload'\n\nimport { closeStaleImpersonations } from '../session/closeStale'\nimport type { ResolvedOptions } from '../types'\n\nconst DEFAULT_CRON = '0 3 * * *'\nconst DEFAULT_QUEUE = 'impersonation-retention'\nconst PAGE = 100\n\n/**\n * Opt-in job: close stale open rows, then delete closed rows older than\n * `deleteAfterDays`. The host must run the queue.\n */\nexport const registerRetention = (config: Config, options: ResolvedOptions): void => {\n\tconst retention = options.retention\n\tif (!retention) {\n\t\treturn\n\t}\n\n\tconst task = {\n\t\tslug: 'impersonationRetention',\n\t\tschedule: [{ cron: retention.cron ?? DEFAULT_CRON, queue: retention.queue ?? DEFAULT_QUEUE }],\n\t\thandler: async ({ req }: { req: PayloadRequest }) => {\n\t\t\tconst closed = await closeStaleImpersonations(req.payload)\n\t\t\tconst cutoff = new Date(Date.now() - retention.deleteAfterDays * 86_400_000).toISOString()\n\t\t\tlet deleted = 0\n\t\t\tfor (;;) {\n\t\t\t\tconst { docs } = await req.payload.find({\n\t\t\t\t\tcollection: options.collectionSlug,\n\t\t\t\t\tdepth: 0,\n\t\t\t\t\tlimit: PAGE,\n\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\tpagination: false,\n\t\t\t\t\twhere: {\n\t\t\t\t\t\tand: [{ endedAt: { exists: true } }, { endedAt: { less_than_equal: cutoff } }],\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif (docs.length === 0) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfor (const doc of docs) {\n\t\t\t\t\tawait req.payload.delete({\n\t\t\t\t\t\tcollection: options.collectionSlug,\n\t\t\t\t\t\tid: doc.id,\n\t\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\t})\n\t\t\t\t\tdeleted += 1\n\t\t\t\t}\n\t\t\t\tif (docs.length < PAGE) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { output: { closed, deleted } }\n\t\t},\n\t\t// Through unknown: a host app compiling this source types its tasks from its own\n\t\t// generated TaskType, which does not know this slug unless retention is on there.\n\t} as unknown as TaskConfig\n\n\tconfig.jobs = {\n\t\t...config.jobs,\n\t\ttasks: [...(config.jobs?.tasks ?? []), task],\n\t}\n}\n"],"mappings":";;AAKA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AACtB,MAAM,OAAO;;;;;AAMb,MAAa,qBAAqB,QAAgB,YAAmC;CACpF,MAAM,YAAY,QAAQ;CAC1B,IAAI,CAAC,WACJ;CAGD,MAAM,OAAO;EACZ,MAAM;EACN,UAAU,CAAC;GAAE,MAAM,UAAU,QAAQ;GAAc,OAAO,UAAU,SAAS;EAAc,CAAC;EAC5F,SAAS,OAAO,EAAE,UAAmC;GACpD,MAAM,SAAS,MAAM,yBAAyB,IAAI,OAAO;GACzD,MAAM,0BAAS,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,kBAAkB,KAAU,GAAE,YAAY;GACzF,IAAI,UAAU;GACd,SAAS;IACR,MAAM,EAAE,SAAS,MAAM,IAAI,QAAQ,KAAK;KACvC,YAAY,QAAQ;KACpB,OAAO;KACP,OAAO;KACP,gBAAgB;KAChB,YAAY;KACZ,OAAO,EACN,KAAK,CAAC,EAAE,SAAS,EAAE,QAAQ,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,iBAAiB,OAAO,EAAE,CAAC,EAC9E;IACD,CAAC;IACD,IAAI,KAAK,WAAW,GACnB;IAED,KAAK,MAAM,OAAO,MAAM;KACvB,MAAM,IAAI,QAAQ,OAAO;MACxB,YAAY,QAAQ;MACpB,IAAI,IAAI;MACR,gBAAgB;KACjB,CAAC;KACD,WAAW;IACZ;IACA,IAAI,KAAK,SAAS,MACjB;GAEF;GACA,OAAO,EAAE,QAAQ;IAAE;IAAQ;GAAQ,EAAE;EACtC;CAGD;CAEA,OAAO,OAAO;EACb,GAAG,OAAO;EACV,OAAO,CAAC,GAAI,OAAO,MAAM,SAAS,CAAC,GAAI,IAAI;CAC5C;AACD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/errorKey.ts
|
|
3
|
+
/** The message key for a failure code an endpoint answers with. */
|
|
4
|
+
const errorKey = (code) => {
|
|
5
|
+
return {
|
|
6
|
+
alreadyImpersonating: keys.errorAlreadyImpersonating,
|
|
7
|
+
failed: keys.errorFailed,
|
|
8
|
+
forbidden: keys.errorForbidden,
|
|
9
|
+
impersonatorGone: keys.errorImpersonatorGone,
|
|
10
|
+
impersonatorSessionExpired: keys.errorImpersonatorSessionExpired,
|
|
11
|
+
invalidBody: keys.errorInvalidBody,
|
|
12
|
+
notImpersonating: keys.errorNotImpersonating,
|
|
13
|
+
origin: keys.errorOrigin,
|
|
14
|
+
reasonRequired: keys.errorReasonRequired,
|
|
15
|
+
selfTarget: keys.errorSelfTarget,
|
|
16
|
+
targetNotFound: keys.errorTargetNotFound,
|
|
17
|
+
targetTrashed: keys.errorTargetTrashed,
|
|
18
|
+
targetUnverified: keys.errorTargetUnverified,
|
|
19
|
+
unsupportedAuth: keys.errorUnsupportedAuth,
|
|
20
|
+
unsupportedCollection: keys.errorUnsupportedCollection
|
|
21
|
+
}[code] ?? keys.errorFailed;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { errorKey };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=errorKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorKey.js","names":[],"sources":["../../src/translations/errorKey.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\n/** The message key for a failure code an endpoint answers with. */\nexport const errorKey = (code: string) => {\n\tconst map: Record<string, TranslationKey> = {\n\t\talreadyImpersonating: keys.errorAlreadyImpersonating,\n\t\tfailed: keys.errorFailed,\n\t\tforbidden: keys.errorForbidden,\n\t\timpersonatorGone: keys.errorImpersonatorGone,\n\t\timpersonatorSessionExpired: keys.errorImpersonatorSessionExpired,\n\t\tinvalidBody: keys.errorInvalidBody,\n\t\tnotImpersonating: keys.errorNotImpersonating,\n\t\torigin: keys.errorOrigin,\n\t\treasonRequired: keys.errorReasonRequired,\n\t\tselfTarget: keys.errorSelfTarget,\n\t\ttargetNotFound: keys.errorTargetNotFound,\n\t\ttargetTrashed: keys.errorTargetTrashed,\n\t\ttargetUnverified: keys.errorTargetUnverified,\n\t\tunsupportedAuth: keys.errorUnsupportedAuth,\n\t\tunsupportedCollection: keys.errorUnsupportedCollection,\n\t}\n\treturn map[code] ?? keys.errorFailed\n}\n"],"mappings":";;;AAGA,MAAa,YAAY,SAAiB;CAkBzC,OAAO;EAhBN,sBAAsB,KAAK;EAC3B,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,kBAAkB,KAAK;EACvB,4BAA4B,KAAK;EACjC,aAAa,KAAK;EAClB,kBAAkB,KAAK;EACvB,QAAQ,KAAK;EACb,gBAAgB,KAAK;EACrB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACrB,eAAe,KAAK;EACpB,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,uBAAuB,KAAK;CAEpB,EAAE,SAAS,KAAK;AAC1B"}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { translations } from "./index.js";
|
|
2
2
|
//#region src/translations/lookup.ts
|
|
3
|
-
const
|
|
4
|
-
/** Resolve a plugin string in `locale`, falling back to English. */
|
|
5
|
-
const messageFor = (locale, key) => {
|
|
6
|
-
const code = (locale ?? "en").split("-")[0] ?? "en";
|
|
3
|
+
const valueIn = (source, locale, key) => {
|
|
7
4
|
const separator = key.indexOf(":");
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const value = source?.[locale]?.[key.slice(0, separator)]?.[key.slice(separator + 1)];
|
|
6
|
+
return typeof value === "string" ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a plugin string in `locale`, falling back to English. Pass the config's
|
|
10
|
+
* `i18n.translations` as `source` so host and `translations` option overrides apply; the plugin's
|
|
11
|
+
* bundled strings back it.
|
|
12
|
+
*/
|
|
13
|
+
const messageFor = (locale, key, source) => {
|
|
14
|
+
const code = (locale ?? "en").split("-")[0] ?? "en";
|
|
15
|
+
return valueIn(source, code, key) ?? valueIn(translations, code, key) ?? valueIn(source, "en", key) ?? valueIn(translations, "en", key) ?? key;
|
|
11
16
|
};
|
|
12
17
|
/** Replace every `token` without interpreting `$` in `value`. */
|
|
13
18
|
const fillTemplate = (template, token, value) => template.split(token).join(value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lookup.js","names":[],"sources":["../../src/translations/lookup.ts"],"sourcesContent":["import { translations } from './index'\nimport type { TranslationKey } from './keys'\n\nconst
|
|
1
|
+
{"version":3,"file":"lookup.js","names":[],"sources":["../../src/translations/lookup.ts"],"sourcesContent":["import { translations } from './index'\nimport type { TranslationKey } from './keys'\n\ntype Nested = Record<string, Record<string, Record<string, unknown> | undefined> | undefined>\n\nconst valueIn = (source: unknown, locale: string, key: TranslationKey): string | undefined => {\n\tconst separator = key.indexOf(':')\n\tconst value = (source as Nested | null | undefined)?.[locale]?.[key.slice(0, separator)]?.[\n\t\tkey.slice(separator + 1)\n\t]\n\treturn typeof value === 'string' ? value : undefined\n}\n\n/**\n * Resolve a plugin string in `locale`, falling back to English. Pass the config's\n * `i18n.translations` as `source` so host and `translations` option overrides apply; the plugin's\n * bundled strings back it.\n */\nexport const messageFor = (\n\tlocale: null | string | undefined,\n\tkey: TranslationKey,\n\tsource?: unknown\n): string => {\n\tconst code = (locale ?? 'en').split('-')[0] ?? 'en'\n\treturn (\n\t\tvalueIn(source, code, key) ??\n\t\tvalueIn(translations, code, key) ??\n\t\tvalueIn(source, 'en', key) ??\n\t\tvalueIn(translations, 'en', key) ??\n\t\tkey\n\t)\n}\n\n/** Replace every `token` without interpreting `$` in `value`. */\nexport const fillTemplate = (template: string, token: string, value: string): string =>\n\ttemplate.split(token).join(value)\n"],"mappings":";;AAKA,MAAM,WAAW,QAAiB,QAAgB,QAA4C;CAC7F,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,QAAS,SAAuC,UAAU,IAAI,MAAM,GAAG,SAAS,KACrF,IAAI,MAAM,YAAY,CAAC;CAExB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC5C;;;;;;AAOA,MAAa,cACZ,QACA,KACA,WACY;CACZ,MAAM,QAAQ,UAAU,MAAM,MAAM,GAAG,EAAE,MAAM;CAC/C,OACC,QAAQ,QAAQ,MAAM,GAAG,KACzB,QAAQ,cAAc,MAAM,GAAG,KAC/B,QAAQ,QAAQ,MAAM,GAAG,KACzB,QAAQ,cAAc,MAAM,GAAG,KAC/B;AAEF;;AAGA,MAAa,gBAAgB,UAAkB,OAAe,UAC7D,SAAS,MAAM,KAAK,EAAE,KAAK,KAAK"}
|