@10x-media/impersonation 0.1.0-beta.0 → 0.1.0-beta.1

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @10x-media/impersonation
2
2
 
3
+ ## 0.1.0-beta.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
3
9
  ## 0.1.0-beta.0
4
10
 
5
11
  ### Minor Changes
@@ -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, writeCookie };
56
+ export { clearOnSwitchOnStart, expireCookie, expireCookies, expirePayloadCookie, generateHintCookie, sharedCookieName };
48
57
 
49
58
  //# sourceMappingURL=cookies.js.map
@@ -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":";;AAKA,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;AAErF,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"}
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"}
@@ -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: [...extraClearOnSwitch, options.hintCookieName]
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
- ...isolatedName ? [expireCookie({
150
- authConfig,
151
- name: isolatedName
152
- })] : [],
153
- expireCookie({
154
- authConfig,
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: extraClearOnSwitch
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 { clearOnSwitchWithoutTenant, exitTenantCookies } from '../auth/tenantCookie'\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 extraClearOnSwitch = clearOnSwitchWithoutTenant(options.cookies.clearOnSwitch, cookiePrefix)\n\tconst expireHintAndClear = expireCookies({\n\t\tauthConfig,\n\t\tcookiePrefix,\n\t\tnames: [...extraClearOnSwitch, 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\t...expireCookies({\n\t\t\t\t\tauthConfig,\n\t\t\t\t\tcookiePrefix,\n\t\t\t\t\tnames: extraClearOnSwitch,\n\t\t\t\t}),\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...exitTenantCookies({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\tmode: closed.mode,\n\t\t\t\toptions,\n\t\t\t\tsnapshot: row.impersonatorTenantCookie,\n\t\t\t}),\n\t\t\t...expireCookies({\n\t\t\t\tauthConfig,\n\t\t\t\tcookiePrefix,\n\t\t\t\tnames: extraClearOnSwitch,\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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"}
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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"}
@@ -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 { expireCookies, generateHintCookie } from "../auth/cookies.js";
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
- ...startTenantCookies({
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":"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"}
@@ -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
- const defaultClearOnSwitch = (cookiePrefix) => [`${cookiePrefix}-tenant`];
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, defaultClearOnSwitch };
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'\nexport const defaultClearOnSwitch = (cookiePrefix: string) => [`${cookiePrefix}-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;AACrC,MAAa,wBAAwB,iBAAyB,CAAC,GAAG,aAAa,QAAQ;;AAGvF,MAAa,0BAA0B;AACvC,MAAa,cAAc;;AAE3B,MAAa,2BAA2B;AAGxC,MAAa,wBAAwB,CAAC,WAAW,oBAAoB"}
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 { RESERVED_API_SEGMENTS, defaultClearOnSwitch } from "./constants.js";
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 ?? defaultClearOnSwitch(cookiePrefix) },
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\tdefaultClearOnSwitch,\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 ?? defaultClearOnSwitch(cookiePrefix),\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
+ {"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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"}
@@ -84,10 +84,6 @@ const buildRecordsCollection = ({ authSlugs, options }) => {
84
84
  index: true,
85
85
  required: true
86
86
  },
87
- {
88
- name: "impersonatorTenantCookie",
89
- type: "text"
90
- },
91
87
  {
92
88
  name: "startedAt",
93
89
  type: "date",
@@ -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{ name: 'impersonatorTenantCookie', type: 'text' },\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
+ {"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,UAAU;GAC3B,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;CACD;CAEA,OAAO,OAAO;EACb,GAAG,OAAO;EACV,OAAO,CAAC,GAAI,OAAO,MAAM,SAAS,CAAC,GAAI,IAAI;CAC5C;AACD"}
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"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type {\n\tAccess,\n\tCollectionConfig,\n\tCollectionSlug,\n\tPayload,\n\tPayloadComponent,\n\tPayloadRequest,\n\tTypedUser,\n\tWhere,\n} from 'payload'\n\nimport type { TranslationsOption } from './translations'\n\nexport type ImpersonationMode = 'parallel' | 'swap'\n\nexport type EndedBy =\n\t| 'exit'\n\t| 'expired'\n\t| 'failed'\n\t| 'impersonatorGone'\n\t| 'logout'\n\t| 'targetGone'\n\t| 'terminated'\n\nexport type { FailureCode } from './endpoints/codes'\n\nexport type ImpersonationActor = {\n\tabsoluteExpiresAt?: null | string\n\tid: number | string\n\timpersonator: { collection: string; id: number | string }\n\timpersonatorEmail?: null | string\n\timpersonatorLocale?: null | string\n\timpersonatorTitle?: null | string\n\tmode: ImpersonationMode\n\treason?: null | string\n\tstartedAt: string\n\ttarget: { collection: string; id: number | string }\n\ttargetEmail?: null | string\n\ttargetTitle?: null | string\n}\n\nexport type ImpersonateAccessArgs = {\n\treq: PayloadRequest\n\ttarget: Record<string, unknown>\n\ttargetCollection: CollectionSlug\n}\n\n/**\n * Host gate for starting a session. Only the literal `true` allows. `false`,\n * `undefined`, and a Payload `Where` object all deny. A `Where` is never treated\n * as allow-all.\n */\nexport type ImpersonateAccess = (args: ImpersonateAccessArgs) => boolean | Promise<boolean>\n\nexport type BooleanAccess = (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n/**\n * Which users the switcher (and document action) should offer. This is UX only.\n * `access.impersonate` remains the start check. The result is sent to the client,\n * so a caller can still POST `/start` for someone outside the filter; start\n * re-runs this function and rejects a miss.\n *\n * - `true`: list the whole collection\n * - `false`: hide the collection\n * - `Where`: AND this query onto the list fetch\n */\nexport type FilterTargets = (args: {\n\treq: PayloadRequest\n\ttargetCollection: CollectionSlug\n}) => boolean | Promise<boolean | Where> | Where\n\nexport type IssuedSession = {\n\tcookie: string\n\tcookieName: string\n\texp: number\n\tmode: ImpersonationMode\n\tsid: string\n\ttoken: string\n\tuser: TypedUser\n}\n\nexport type SessionIssueArgs = {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\treq: PayloadRequest\n\tuserId: number | string\n}\n\nexport type SessionRevokeArgs = {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\treq?: PayloadRequest\n\tsid: string\n\tuserId: number | string\n}\n\nexport type SessionSeams = {\n\t/** Defaults to `user._sid`. */\n\tbinding?: (user: TypedUser) => string | undefined\n\tissue?: (args: SessionIssueArgs) => Promise<IssuedSession>\n\trevoke?: (args: SessionRevokeArgs) => Promise<void>\n}\n\nexport type UiOptions = {\n\t/**\n\t * Admin bar while a session is open. Target side: \"Acting as\" / \"Return to\".\n\t * Parallel impersonator side: \"session is active on the website\" / \"End session\".\n\t * @default true\n\t */\n\tbar?: boolean\n\t/**\n\t * Replacement for the default switcher row. Receives the same props as\n\t * `ImpersonationUserCard` (`collectionSlug`, `doc`, and the pick/drawer/document\n\t * actions). A string path is registered on `admin.dependencies` so the import\n\t * map can load it. Omit to keep the default card.\n\t */\n\tcard?: PayloadComponent\n\t/**\n\t * Show email under `useAsTitle` on default switcher cards. Hidden when the\n\t * title is already the email.\n\t * @default true\n\t */\n\tcardEmail?: boolean\n\t/**\n\t * Impersonate button on the target document. Rendered in\n\t * `BeforeDocumentControls` so it stays visible when the edit-menu dots are\n\t * hidden (no create/delete access).\n\t * @default true\n\t */\n\tdocumentAction?: boolean\n\t/**\n\t * How the bar, the collapsed chip, and the frontend banner name users:\n\t * `'title'` is the collection's `useAsTitle` (falls back to the email),\n\t * `'email'` is the email.\n\t * @default 'title'\n\t */\n\tuserLabel?: UserLabel\n\t/**\n\t * Header control that opens the user switcher drawer.\n\t * @default true\n\t */\n\theaderAction?: boolean\n\t/**\n\t * \"End session\" beside Save on an `impersonation-sessions` document\n\t * (`beforeDocumentControls`). Hidden when the row already has `endedAt`.\n\t * Create, update, and delete stay denied.\n\t * @default true\n\t */\n\trecordAction?: boolean\n\t/**\n\t * Put `impersonation-sessions` in the admin nav. `access.readRecords` still\n\t * decides who can read a row. Omit to keep the collection hidden.\n\t * @default false\n\t */\n\tsessionsCollection?: boolean\n}\n\nexport type RetentionOptions = {\n\t/** Closed rows older than this many days are deleted. Open rows are never deleted. */\n\tdeleteAfterDays: number\n\t/** @default `0 3 * * *` */\n\tcron?: string\n\t/** @default `impersonation-retention` */\n\tqueue?: string\n}\n\nexport type EnabledOptions = {\n\taccess: {\n\t\t/**\n\t\t * Gate for POST `/start`. Only the literal `true` allows. A `Where` object\n\t\t * is deny, never allow-all.\n\t\t */\n\t\timpersonate: ImpersonateAccess\n\t\t/** Who can read `impersonation-sessions`. Omit to deny. Does not un-hide the collection. */\n\t\treadRecords?: Access\n\t\t/** Who can POST `/end` on someone else's session. Default deny. */\n\t\tterminate?: BooleanAccess\n\t\t/**\n\t\t * Who the switcher lists. Omit to show everyone the viewer can read.\n\t\t * See {@link FilterTargets}.\n\t\t */\n\t\tfilterTargets?: FilterTargets\n\t}\n\tapiPath?: string\n\tcollectionSlug?: string\n\t/**\n\t * Last pass over the built `impersonation-sessions` collection. Whatever\n\t * this returns is registered as-is, including access. Do not rename fields\n\t * the plugin reads (`impersonator`, `target`, `targetSid`, `impersonatorSid`,\n\t * `startedAt`, `endedAt`, `endedBy`, `mode`, `absoluteExpiresAt`) and do not\n\t * change the slug here. Use `collectionSlug` for that.\n\t */\n\tcollection?: {\n\t\toverrides?: (collection: CollectionConfig) => CollectionConfig\n\t}\n\tcookies?: { clearOnSwitch?: string[] }\n\tdecorateRequests?: boolean\n\thintCookieName?: string\n\tmaxDuration?: number\n\t/**\n\t * Opt-in cleanup of closed rows, plus `closeStaleImpersonations` on the same\n\t * cron. Off until you set this. The host must run the queue (`jobs.autoRun`\n\t * or a bin). Open rows are never deleted.\n\t */\n\tretention?: false | RetentionOptions\n\tonEnd?: (args: {\n\t\tendedBy: EndedBy\n\t\tpayload: Payload\n\t\trecord: Record<string, unknown>\n\t\treq?: PayloadRequest\n\t}) => Promise<void> | void\n\tonStart?: (args: {\n\t\tpayload: Payload\n\t\trecord: Record<string, unknown>\n\t\treq: PayloadRequest\n\t}) => Promise<void> | void\n\treason?: 'off' | 'optional' | 'required'\n\tsecurity?: { trustedOrigins?: string[] }\n\tsession?: SessionSeams\n\ttargets?: CollectionSlug[]\n\ttranslations?: TranslationsOption\n\tui?: false | UiOptions\n}\n\nexport type ImpersonationPluginOptions =\n\t| ({ disabled: true } & Partial<EnabledOptions>)\n\t| ({ disabled?: false } & EnabledOptions)\n\nexport type UserLabel = 'email' | 'title'\n\nexport type ResolvedUi = {\n\tbar: boolean\n\tcard?: PayloadComponent\n\tcardEmail: boolean\n\tdocumentAction: boolean\n\theaderAction: boolean\n\trecordAction: boolean\n\tsessionsCollection: boolean\n\tuserLabel: UserLabel\n}\n\nexport type ResolvedOptions = {\n\taccess: {\n\t\tfilterTargets?: FilterTargets\n\t\timpersonate: ImpersonateAccess\n\t\treadRecords: Access\n\t\tterminate: BooleanAccess\n\t}\n\tapiPath: string\n\tcollectionSlug: CollectionSlug\n\tcollectionOverrides?: (collection: CollectionConfig) => CollectionConfig\n\tcookies: { clearOnSwitch: string[] }\n\tdecorateRequests: boolean\n\thintCookieName: string\n\tmaxDuration: number | undefined\n\tonEnd?: EnabledOptions['onEnd']\n\tonStart?: EnabledOptions['onStart']\n\treason: 'off' | 'optional' | 'required'\n\tretention: RetentionOptions | undefined\n\tsecurity: { trustedOrigins: string[] }\n\tsession: SessionSeams\n\ttargets: CollectionSlug[] | undefined\n\tui: ResolvedUi\n}\n\n/** Runtime fields Payload paints on `req.user` but generated collection types omit. */\nexport type ImpersonatedUser = TypedUser & {\n\t_impersonation?: ImpersonationActor\n\t_sid?: string\n\t_strategy?: string\n}\n\nexport const asAuthUser = (user: TypedUser): ImpersonatedUser => user as ImpersonatedUser\n\n/** Sid the current request is bound to. Host `session.binding` wins over `_sid`. */\nexport const boundSid = (\n\tuser: null | TypedUser | undefined,\n\tbinding?: (user: TypedUser) => string | undefined\n): string | undefined => {\n\tif (!user) {\n\t\treturn undefined\n\t}\n\treturn binding?.(user) ?? asAuthUser(user)._sid\n}\n\nexport type ImpersonationRecord = {\n\tabsoluteExpiresAt?: null | string\n\tendedAt?: null | string\n\tendedBy?: EndedBy | null\n\tid: number | string\n\timpersonator: { relationTo: string; value: number | string } | number | string\n\timpersonatorEmail?: null | string\n\timpersonatorLocale?: null | string\n\timpersonatorSid: string\n\timpersonatorTenantCookie?: null | string\n\timpersonatorTitle?: null | string\n\tip?: null | string\n\tmode: ImpersonationMode\n\treason?: null | string\n\tstartedAt: string\n\ttarget: { relationTo: string; value: number | string } | number | string\n\ttargetEmail?: null | string\n\ttargetLocked?: boolean | null\n\ttargetSid: string\n\ttargetTitle?: null | string\n\tuserAgent?: null | string\n}\n"],"mappings":";AAgRA,MAAa,cAAc,SAAsC;;AAGjE,MAAa,YACZ,MACA,YACwB;CACxB,IAAI,CAAC,MACJ;CAED,OAAO,UAAU,IAAI,KAAK,WAAW,IAAI,EAAE;AAC5C"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type {\n\tAccess,\n\tCollectionConfig,\n\tCollectionSlug,\n\tPayload,\n\tPayloadComponent,\n\tPayloadRequest,\n\tTypedUser,\n\tWhere,\n} from 'payload'\n\nimport type { TranslationsOption } from './translations'\n\nexport type ImpersonationMode = 'parallel' | 'swap'\n\nexport type EndedBy =\n\t| 'exit'\n\t| 'expired'\n\t| 'failed'\n\t| 'impersonatorGone'\n\t| 'logout'\n\t| 'targetGone'\n\t| 'terminated'\n\nexport type { FailureCode } from './endpoints/codes'\n\nexport type ImpersonationActor = {\n\tabsoluteExpiresAt?: null | string\n\tid: number | string\n\timpersonator: { collection: string; id: number | string }\n\timpersonatorEmail?: null | string\n\timpersonatorLocale?: null | string\n\timpersonatorTitle?: null | string\n\tmode: ImpersonationMode\n\treason?: null | string\n\tstartedAt: string\n\ttarget: { collection: string; id: number | string }\n\ttargetEmail?: null | string\n\ttargetTitle?: null | string\n}\n\nexport type ImpersonateAccessArgs = {\n\treq: PayloadRequest\n\ttarget: Record<string, unknown>\n\ttargetCollection: CollectionSlug\n}\n\n/**\n * Host gate for starting a session. Only the literal `true` allows. `false`,\n * `undefined`, and a Payload `Where` object all deny. A `Where` is never treated\n * as allow-all.\n */\nexport type ImpersonateAccess = (args: ImpersonateAccessArgs) => boolean | Promise<boolean>\n\nexport type BooleanAccess = (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n/**\n * Which users the switcher (and document action) should offer. This is UX only.\n * `access.impersonate` remains the start check. The result is sent to the client,\n * so a caller can still POST `/start` for someone outside the filter; start\n * re-runs this function and rejects a miss.\n *\n * - `true`: list the whole collection\n * - `false`: hide the collection\n * - `Where`: AND this query onto the list fetch\n */\nexport type FilterTargets = (args: {\n\treq: PayloadRequest\n\ttargetCollection: CollectionSlug\n}) => boolean | Promise<boolean | Where> | Where\n\nexport type IssuedSession = {\n\tcookie: string\n\tcookieName: string\n\texp: number\n\tmode: ImpersonationMode\n\tsid: string\n\ttoken: string\n\tuser: TypedUser\n}\n\nexport type SessionIssueArgs = {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\treq: PayloadRequest\n\tuserId: number | string\n}\n\nexport type SessionRevokeArgs = {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\treq?: PayloadRequest\n\tsid: string\n\tuserId: number | string\n}\n\nexport type SessionSeams = {\n\t/** Defaults to `user._sid`. */\n\tbinding?: (user: TypedUser) => string | undefined\n\tissue?: (args: SessionIssueArgs) => Promise<IssuedSession>\n\trevoke?: (args: SessionRevokeArgs) => Promise<void>\n}\n\nexport type UiOptions = {\n\t/**\n\t * Admin bar while a session is open. Target side: \"Acting as\" / \"Return to\".\n\t * Parallel impersonator side: \"session is active on the website\" / \"End session\".\n\t * @default true\n\t */\n\tbar?: boolean\n\t/**\n\t * Replacement for the default switcher row. Receives the same props as\n\t * `ImpersonationUserCard` (`collectionSlug`, `doc`, and the pick/drawer/document\n\t * actions). A string path is registered on `admin.dependencies` so the import\n\t * map can load it. Omit to keep the default card.\n\t */\n\tcard?: PayloadComponent\n\t/**\n\t * Show email under `useAsTitle` on default switcher cards. Hidden when the\n\t * title is already the email.\n\t * @default true\n\t */\n\tcardEmail?: boolean\n\t/**\n\t * Impersonate button on the target document. Rendered in\n\t * `BeforeDocumentControls` so it stays visible when the edit-menu dots are\n\t * hidden (no create/delete access).\n\t * @default true\n\t */\n\tdocumentAction?: boolean\n\t/**\n\t * How the bar, the collapsed chip, and the frontend banner name users:\n\t * `'title'` is the collection's `useAsTitle` (falls back to the email),\n\t * `'email'` is the email.\n\t * @default 'title'\n\t */\n\tuserLabel?: UserLabel\n\t/**\n\t * Header control that opens the user switcher drawer.\n\t * @default true\n\t */\n\theaderAction?: boolean\n\t/**\n\t * \"End session\" beside Save on an `impersonation-sessions` document\n\t * (`beforeDocumentControls`). Hidden when the row already has `endedAt`.\n\t * Create, update, and delete stay denied.\n\t * @default true\n\t */\n\trecordAction?: boolean\n\t/**\n\t * Put `impersonation-sessions` in the admin nav. `access.readRecords` still\n\t * decides who can read a row. Omit to keep the collection hidden.\n\t * @default false\n\t */\n\tsessionsCollection?: boolean\n}\n\nexport type RetentionOptions = {\n\t/** Closed rows older than this many days are deleted. Open rows are never deleted. */\n\tdeleteAfterDays: number\n\t/** @default `0 3 * * *` */\n\tcron?: string\n\t/** @default `impersonation-retention` */\n\tqueue?: string\n}\n\nexport type EnabledOptions = {\n\taccess: {\n\t\t/**\n\t\t * Gate for POST `/start`. Only the literal `true` allows. A `Where` object\n\t\t * is deny, never allow-all.\n\t\t */\n\t\timpersonate: ImpersonateAccess\n\t\t/** Who can read `impersonation-sessions`. Omit to deny. Does not un-hide the collection. */\n\t\treadRecords?: Access\n\t\t/** Who can POST `/end` on someone else's session. Default deny. */\n\t\tterminate?: BooleanAccess\n\t\t/**\n\t\t * Who the switcher lists. Omit to show everyone the viewer can read.\n\t\t * See {@link FilterTargets}.\n\t\t */\n\t\tfilterTargets?: FilterTargets\n\t}\n\tapiPath?: string\n\tcollectionSlug?: string\n\t/**\n\t * Last pass over the built `impersonation-sessions` collection. Whatever\n\t * this returns is registered as-is, including access. Do not rename fields\n\t * the plugin reads (`impersonator`, `target`, `targetSid`, `impersonatorSid`,\n\t * `startedAt`, `endedAt`, `endedBy`, `mode`, `absoluteExpiresAt`) and do not\n\t * change the slug here. Use `collectionSlug` for that.\n\t */\n\tcollection?: {\n\t\toverrides?: (collection: CollectionConfig) => CollectionConfig\n\t}\n\tcookies?: { clearOnSwitch?: string[] }\n\tdecorateRequests?: boolean\n\thintCookieName?: string\n\tmaxDuration?: number\n\t/**\n\t * Opt-in cleanup of closed rows, plus `closeStaleImpersonations` on the same\n\t * cron. Off until you set this. The host must run the queue (`jobs.autoRun`\n\t * or a bin). Open rows are never deleted.\n\t */\n\tretention?: false | RetentionOptions\n\tonEnd?: (args: {\n\t\tendedBy: EndedBy\n\t\tpayload: Payload\n\t\trecord: Record<string, unknown>\n\t\treq?: PayloadRequest\n\t}) => Promise<void> | void\n\tonStart?: (args: {\n\t\tpayload: Payload\n\t\trecord: Record<string, unknown>\n\t\treq: PayloadRequest\n\t}) => Promise<void> | void\n\treason?: 'off' | 'optional' | 'required'\n\tsecurity?: { trustedOrigins?: string[] }\n\tsession?: SessionSeams\n\ttargets?: CollectionSlug[]\n\ttranslations?: TranslationsOption\n\tui?: false | UiOptions\n}\n\nexport type ImpersonationPluginOptions =\n\t| ({ disabled: true } & Partial<EnabledOptions>)\n\t| ({ disabled?: false } & EnabledOptions)\n\nexport type UserLabel = 'email' | 'title'\n\nexport type ResolvedUi = {\n\tbar: boolean\n\tcard?: PayloadComponent\n\tcardEmail: boolean\n\tdocumentAction: boolean\n\theaderAction: boolean\n\trecordAction: boolean\n\tsessionsCollection: boolean\n\tuserLabel: UserLabel\n}\n\nexport type ResolvedOptions = {\n\taccess: {\n\t\tfilterTargets?: FilterTargets\n\t\timpersonate: ImpersonateAccess\n\t\treadRecords: Access\n\t\tterminate: BooleanAccess\n\t}\n\tapiPath: string\n\tcollectionSlug: CollectionSlug\n\tcollectionOverrides?: (collection: CollectionConfig) => CollectionConfig\n\tcookies: { clearOnSwitch: string[] }\n\tdecorateRequests: boolean\n\thintCookieName: string\n\tmaxDuration: number | undefined\n\tonEnd?: EnabledOptions['onEnd']\n\tonStart?: EnabledOptions['onStart']\n\treason: 'off' | 'optional' | 'required'\n\tretention: RetentionOptions | undefined\n\tsecurity: { trustedOrigins: string[] }\n\tsession: SessionSeams\n\ttargets: CollectionSlug[] | undefined\n\tui: ResolvedUi\n}\n\n/** Runtime fields Payload paints on `req.user` but generated collection types omit. */\nexport type ImpersonatedUser = TypedUser & {\n\t_impersonation?: ImpersonationActor\n\t_sid?: string\n\t_strategy?: string\n}\n\nexport const asAuthUser = (user: TypedUser): ImpersonatedUser => user as ImpersonatedUser\n\n/** Sid the current request is bound to. Host `session.binding` wins over `_sid`. */\nexport const boundSid = (\n\tuser: null | TypedUser | undefined,\n\tbinding?: (user: TypedUser) => string | undefined\n): string | undefined => {\n\tif (!user) {\n\t\treturn undefined\n\t}\n\treturn binding?.(user) ?? asAuthUser(user)._sid\n}\n\nexport type ImpersonationRecord = {\n\tabsoluteExpiresAt?: null | string\n\tendedAt?: null | string\n\tendedBy?: EndedBy | null\n\tid: number | string\n\timpersonator: { relationTo: string; value: number | string } | number | string\n\timpersonatorEmail?: null | string\n\timpersonatorLocale?: null | string\n\timpersonatorSid: string\n\timpersonatorTitle?: null | string\n\tip?: null | string\n\tmode: ImpersonationMode\n\treason?: null | string\n\tstartedAt: string\n\ttarget: { relationTo: string; value: number | string } | number | string\n\ttargetEmail?: null | string\n\ttargetLocked?: boolean | null\n\ttargetSid: string\n\ttargetTitle?: null | string\n\tuserAgent?: null | string\n}\n"],"mappings":";AAgRA,MAAa,cAAc,SAAsC;;AAGjE,MAAa,YACZ,MACA,YACwB;CACxB,IAAI,CAAC,MACJ;CAED,OAAO,UAAU,IAAI,KAAK,WAAW,IAAI,EAAE;AAC5C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10x-media/impersonation",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.1",
4
4
  "description": "Let an authorised account sign in as another user without their password, then return. Every session is recorded.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -89,11 +89,11 @@
89
89
  "tsdown": "0.22.1",
90
90
  "typescript": "5.9.3",
91
91
  "vitest": "4.1.7",
92
- "@10x-media/payload-test-harness": "0.0.0",
93
- "@10x-media/tsconfig": "0.0.0",
94
92
  "@10x-media/tsdown-config": "0.0.0",
95
- "@10x-media/vitest-config": "0.0.0",
96
- "@10x-media/dual-session": "0.1.0-beta.2"
93
+ "@10x-media/dual-session": "0.1.0-beta.2",
94
+ "@10x-media/tsconfig": "0.0.0",
95
+ "@10x-media/payload-test-harness": "0.0.0",
96
+ "@10x-media/vitest-config": "0.0.0"
97
97
  },
98
98
  "publishConfig": {
99
99
  "access": "public"
@@ -1,74 +0,0 @@
1
- import { expireCookie, writeCookie } from "./cookies.js";
2
- import { defaultClearOnSwitch } from "../plugin/constants.js";
3
- import { parseCookies } from "payload/shared";
4
- //#region src/auth/tenantCookie.ts
5
- /** Same charset Payload tenant ids use (ObjectId, int, uuid). Rejects cookie injection. */
6
- const TENANT_COOKIE_VALUE = /^[\w.-]{1,128}$/;
7
- const tenantCookieName = (cookiePrefix) => defaultClearOnSwitch(cookiePrefix)[0];
8
- const isManagedTenantCookie = (clearOnSwitch, cookiePrefix) => clearOnSwitch.includes(tenantCookieName(cookiePrefix));
9
- const clearOnSwitchWithoutTenant = (clearOnSwitch, cookiePrefix) => clearOnSwitch.filter((name) => name !== tenantCookieName(cookiePrefix));
10
- const readTenantCookie = (headers, cookiePrefix) => {
11
- const raw = parseCookies(headers).get(tenantCookieName(cookiePrefix));
12
- return raw && TENANT_COOKIE_VALUE.test(raw) ? raw : void 0;
13
- };
14
- const setTenantCookie = ({ authConfig, cookiePrefix, value }) => writeCookie({
15
- authConfig,
16
- httpOnly: false,
17
- name: tenantCookieName(cookiePrefix),
18
- value
19
- });
20
- const expireTenantCookie = ({ authConfig, cookiePrefix }) => expireCookie({
21
- authConfig,
22
- httpOnly: false,
23
- name: tenantCookieName(cookiePrefix)
24
- });
25
- const idsFrom = (value) => {
26
- if (value == null) return [];
27
- if (Array.isArray(value)) return value.flatMap(idsFrom);
28
- if (typeof value === "string" || typeof value === "number") {
29
- const id = String(value);
30
- return TENANT_COOKIE_VALUE.test(id) ? [id] : [];
31
- }
32
- if (typeof value === "object") {
33
- if ("id" in value) return idsFrom(value.id);
34
- if ("value" in value) return idsFrom(value.value);
35
- }
36
- return [];
37
- };
38
- /**
39
- * `@payloadcms/plugin-multi-tenant` puts assignments on `tenants` (hasMany) and
40
- * sometimes a single `tenant`. Only a unique readable id is safe to write as the selector.
41
- */
42
- const uniqueAssignedTenantId = (user) => {
43
- const ids = [...new Set([...idsFrom(user.tenants), ...idsFrom(user.tenant)])];
44
- return ids.length === 1 ? ids[0] : void 0;
45
- };
46
- const startTenantCookies = ({ authConfig, cookiePrefix, mode, options, target }) => {
47
- if (!isManagedTenantCookie(options.cookies.clearOnSwitch, cookiePrefix) || mode === "parallel") return [];
48
- const assigned = uniqueAssignedTenantId(target);
49
- if (assigned) return [setTenantCookie({
50
- authConfig,
51
- cookiePrefix,
52
- value: assigned
53
- })];
54
- return [expireTenantCookie({
55
- authConfig,
56
- cookiePrefix
57
- })];
58
- };
59
- const exitTenantCookies = ({ authConfig, cookiePrefix, mode, options, snapshot }) => {
60
- if (!isManagedTenantCookie(options.cookies.clearOnSwitch, cookiePrefix) || mode === "parallel") return [];
61
- if (snapshot && TENANT_COOKIE_VALUE.test(snapshot)) return [setTenantCookie({
62
- authConfig,
63
- cookiePrefix,
64
- value: snapshot
65
- })];
66
- return [expireTenantCookie({
67
- authConfig,
68
- cookiePrefix
69
- })];
70
- };
71
- //#endregion
72
- export { clearOnSwitchWithoutTenant, exitTenantCookies, readTenantCookie, startTenantCookies };
73
-
74
- //# sourceMappingURL=tenantCookie.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tenantCookie.js","names":[],"sources":["../../src/auth/tenantCookie.ts"],"sourcesContent":["import type { SanitizedCollectionConfig } from 'payload'\nimport { parseCookies } from 'payload/shared'\n\nimport { defaultClearOnSwitch } from '../plugin/constants'\nimport type { ImpersonationMode, ResolvedOptions } from '../types'\nimport { expireCookie, writeCookie } from './cookies'\n\ntype AuthConfig = SanitizedCollectionConfig['auth']\n\n/** Same charset Payload tenant ids use (ObjectId, int, uuid). Rejects cookie injection. */\nconst TENANT_COOKIE_VALUE = /^[\\w.-]{1,128}$/\n\nexport const tenantCookieName = (cookiePrefix: string): string =>\n\tdefaultClearOnSwitch(cookiePrefix)[0] as string\n\nexport const isManagedTenantCookie = (clearOnSwitch: string[], cookiePrefix: string): boolean =>\n\tclearOnSwitch.includes(tenantCookieName(cookiePrefix))\n\nexport const clearOnSwitchWithoutTenant = (\n\tclearOnSwitch: string[],\n\tcookiePrefix: string\n): string[] => clearOnSwitch.filter((name) => name !== tenantCookieName(cookiePrefix))\n\nexport const readTenantCookie = (headers: Headers, cookiePrefix: string): string | undefined => {\n\tconst raw = parseCookies(headers).get(tenantCookieName(cookiePrefix))\n\treturn raw && TENANT_COOKIE_VALUE.test(raw) ? raw : undefined\n}\n\nexport const setTenantCookie = ({\n\tauthConfig,\n\tcookiePrefix,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\tvalue: string\n}): string =>\n\twriteCookie({\n\t\tauthConfig,\n\t\thttpOnly: false,\n\t\tname: tenantCookieName(cookiePrefix),\n\t\tvalue,\n\t})\n\nexport const expireTenantCookie = ({\n\tauthConfig,\n\tcookiePrefix,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n}): string =>\n\texpireCookie({\n\t\tauthConfig,\n\t\thttpOnly: false,\n\t\tname: tenantCookieName(cookiePrefix),\n\t})\n\nconst idsFrom = (value: unknown): string[] => {\n\tif (value == null) {\n\t\treturn []\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.flatMap(idsFrom)\n\t}\n\tif (typeof value === 'string' || typeof value === 'number') {\n\t\tconst id = String(value)\n\t\treturn TENANT_COOKIE_VALUE.test(id) ? [id] : []\n\t}\n\tif (typeof value === 'object') {\n\t\tif ('id' in value) {\n\t\t\treturn idsFrom((value as { id: unknown }).id)\n\t\t}\n\t\tif ('value' in value) {\n\t\t\treturn idsFrom((value as { value: unknown }).value)\n\t\t}\n\t}\n\treturn []\n}\n\n/**\n * `@payloadcms/plugin-multi-tenant` puts assignments on `tenants` (hasMany) and\n * sometimes a single `tenant`. Only a unique readable id is safe to write as the selector.\n */\nexport const uniqueAssignedTenantId = (user: Record<string, unknown>): string | undefined => {\n\tconst ids = [...new Set([...idsFrom(user.tenants), ...idsFrom(user.tenant)])]\n\treturn ids.length === 1 ? ids[0] : undefined\n}\n\nexport const startTenantCookies = ({\n\tauthConfig,\n\tcookiePrefix,\n\tmode,\n\toptions,\n\ttarget,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\tmode: ImpersonationMode\n\toptions: ResolvedOptions\n\ttarget: Record<string, unknown>\n}): string[] => {\n\tif (!isManagedTenantCookie(options.cookies.clearOnSwitch, cookiePrefix) || mode === 'parallel') {\n\t\treturn []\n\t}\n\tconst assigned = uniqueAssignedTenantId(target)\n\tif (assigned) {\n\t\treturn [setTenantCookie({ authConfig, cookiePrefix, value: assigned })]\n\t}\n\treturn [expireTenantCookie({ authConfig, cookiePrefix })]\n}\n\nexport const exitTenantCookies = ({\n\tauthConfig,\n\tcookiePrefix,\n\tmode,\n\toptions,\n\tsnapshot,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\tmode: ImpersonationMode\n\toptions: ResolvedOptions\n\tsnapshot?: null | string\n}): string[] => {\n\tif (!isManagedTenantCookie(options.cookies.clearOnSwitch, cookiePrefix) || mode === 'parallel') {\n\t\treturn []\n\t}\n\tif (snapshot && TENANT_COOKIE_VALUE.test(snapshot)) {\n\t\treturn [setTenantCookie({ authConfig, cookiePrefix, value: snapshot })]\n\t}\n\treturn [expireTenantCookie({ authConfig, cookiePrefix })]\n}\n"],"mappings":";;;;;AAUA,MAAM,sBAAsB;AAE5B,MAAa,oBAAoB,iBAChC,qBAAqB,YAAY,EAAE;AAEpC,MAAa,yBAAyB,eAAyB,iBAC9D,cAAc,SAAS,iBAAiB,YAAY,CAAC;AAEtD,MAAa,8BACZ,eACA,iBACc,cAAc,QAAQ,SAAS,SAAS,iBAAiB,YAAY,CAAC;AAErF,MAAa,oBAAoB,SAAkB,iBAA6C;CAC/F,MAAM,MAAM,aAAa,OAAO,EAAE,IAAI,iBAAiB,YAAY,CAAC;CACpE,OAAO,OAAO,oBAAoB,KAAK,GAAG,IAAI,MAAM,KAAA;AACrD;AAEA,MAAa,mBAAmB,EAC/B,YACA,cACA,YAMA,YAAY;CACX;CACA,UAAU;CACV,MAAM,iBAAiB,YAAY;CACnC;AACD,CAAC;AAEF,MAAa,sBAAsB,EAClC,YACA,mBAKA,aAAa;CACZ;CACA,UAAU;CACV,MAAM,iBAAiB,YAAY;AACpC,CAAC;AAEF,MAAM,WAAW,UAA6B;CAC7C,IAAI,SAAS,MACZ,OAAO,CAAC;CAET,IAAI,MAAM,QAAQ,KAAK,GACtB,OAAO,MAAM,QAAQ,OAAO;CAE7B,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAU;EAC3D,MAAM,KAAK,OAAO,KAAK;EACvB,OAAO,oBAAoB,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;CAC/C;CACA,IAAI,OAAO,UAAU,UAAU;EAC9B,IAAI,QAAQ,OACX,OAAO,QAAS,MAA0B,EAAE;EAE7C,IAAI,WAAW,OACd,OAAO,QAAS,MAA6B,KAAK;CAEpD;CACA,OAAO,CAAC;AACT;;;;;AAMA,MAAa,0BAA0B,SAAsD;CAC5F,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,QAAQ,KAAK,OAAO,GAAG,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC;CAC5E,OAAO,IAAI,WAAW,IAAI,IAAI,KAAK,KAAA;AACpC;AAEA,MAAa,sBAAsB,EAClC,YACA,cACA,MACA,SACA,aAOe;CACf,IAAI,CAAC,sBAAsB,QAAQ,QAAQ,eAAe,YAAY,KAAK,SAAS,YACnF,OAAO,CAAC;CAET,MAAM,WAAW,uBAAuB,MAAM;CAC9C,IAAI,UACH,OAAO,CAAC,gBAAgB;EAAE;EAAY;EAAc,OAAO;CAAS,CAAC,CAAC;CAEvE,OAAO,CAAC,mBAAmB;EAAE;EAAY;CAAa,CAAC,CAAC;AACzD;AAEA,MAAa,qBAAqB,EACjC,YACA,cACA,MACA,SACA,eAOe;CACf,IAAI,CAAC,sBAAsB,QAAQ,QAAQ,eAAe,YAAY,KAAK,SAAS,YACnF,OAAO,CAAC;CAET,IAAI,YAAY,oBAAoB,KAAK,QAAQ,GAChD,OAAO,CAAC,gBAAgB;EAAE;EAAY;EAAc,OAAO;CAAS,CAAC,CAAC;CAEvE,OAAO,CAAC,mBAAmB;EAAE;EAAY;CAAa,CAAC,CAAC;AACzD"}