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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/access/filterTargets.js +51 -0
  5. package/dist/access/filterTargets.js.map +1 -0
  6. package/dist/auth/cookies.js +49 -0
  7. package/dist/auth/cookies.js.map +1 -0
  8. package/dist/auth/expiryCookies.js +59 -0
  9. package/dist/auth/expiryCookies.js.map +1 -0
  10. package/dist/auth/issue.js +126 -0
  11. package/dist/auth/issue.js.map +1 -0
  12. package/dist/auth/mode.js +69 -0
  13. package/dist/auth/mode.js.map +1 -0
  14. package/dist/auth/origin.js +55 -0
  15. package/dist/auth/origin.js.map +1 -0
  16. package/dist/auth/revoke.js +24 -0
  17. package/dist/auth/revoke.js.map +1 -0
  18. package/dist/auth/tenantCookie.js +74 -0
  19. package/dist/auth/tenantCookie.js.map +1 -0
  20. package/dist/endpoints/codes.d.ts +6 -0
  21. package/dist/endpoints/current.js +66 -0
  22. package/dist/endpoints/current.js.map +1 -0
  23. package/dist/endpoints/exit.js +242 -0
  24. package/dist/endpoints/exit.js.map +1 -0
  25. package/dist/endpoints/shared.js +76 -0
  26. package/dist/endpoints/shared.js.map +1 -0
  27. package/dist/endpoints/start.js +332 -0
  28. package/dist/endpoints/start.js.map +1 -0
  29. package/dist/endpoints/terminate.js +73 -0
  30. package/dist/endpoints/terminate.js.map +1 -0
  31. package/dist/exports/client.d.ts +9 -0
  32. package/dist/exports/client.js +10 -0
  33. package/dist/exports/i18n.d.ts +3 -0
  34. package/dist/exports/i18n.js +3 -0
  35. package/dist/exports/rsc.d.ts +4 -0
  36. package/dist/exports/rsc.js +4 -0
  37. package/dist/exports/types.d.ts +3 -0
  38. package/dist/exports/types.js +1 -0
  39. package/dist/getImpersonation.d.ts +49 -0
  40. package/dist/getImpersonation.js +78 -0
  41. package/dist/getImpersonation.js.map +1 -0
  42. package/dist/ids.js +27 -0
  43. package/dist/ids.js.map +1 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +55 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/plugin/assertTargets.js +23 -0
  48. package/dist/plugin/assertTargets.js.map +1 -0
  49. package/dist/plugin/constants.d.ts +6 -0
  50. package/dist/plugin/constants.js +20 -0
  51. package/dist/plugin/constants.js.map +1 -0
  52. package/dist/plugin/decorateAuth.js +99 -0
  53. package/dist/plugin/decorateAuth.js.map +1 -0
  54. package/dist/plugin/lookup.js +23 -0
  55. package/dist/plugin/lookup.js.map +1 -0
  56. package/dist/plugin/normalizeOptions.js +83 -0
  57. package/dist/plugin/normalizeOptions.js.map +1 -0
  58. package/dist/plugin/registerAdmin.js +49 -0
  59. package/dist/plugin/registerAdmin.js.map +1 -0
  60. package/dist/plugin/registerCollection.js +183 -0
  61. package/dist/plugin/registerCollection.js.map +1 -0
  62. package/dist/plugin/registerEndpoints.js +19 -0
  63. package/dist/plugin/registerEndpoints.js.map +1 -0
  64. package/dist/plugin/registerHooks.js +59 -0
  65. package/dist/plugin/registerHooks.js.map +1 -0
  66. package/dist/plugin/registerRetention.js +57 -0
  67. package/dist/plugin/registerRetention.js.map +1 -0
  68. package/dist/plugin/registerTranslations.js +19 -0
  69. package/dist/plugin/registerTranslations.js.map +1 -0
  70. package/dist/plugin/registry.js +11 -0
  71. package/dist/plugin/registry.js.map +1 -0
  72. package/dist/plugin/startable.js +21 -0
  73. package/dist/plugin/startable.js.map +1 -0
  74. package/dist/session/close.js +65 -0
  75. package/dist/session/close.js.map +1 -0
  76. package/dist/session/closeStale.d.ts +11 -0
  77. package/dist/session/closeStale.js +65 -0
  78. package/dist/session/closeStale.js.map +1 -0
  79. package/dist/session/locks.js +32 -0
  80. package/dist/session/locks.js.map +1 -0
  81. package/dist/session/resolve.js +91 -0
  82. package/dist/session/resolve.js.map +1 -0
  83. package/dist/translations/ar.js +52 -0
  84. package/dist/translations/ar.js.map +1 -0
  85. package/dist/translations/de.js +52 -0
  86. package/dist/translations/de.js.map +1 -0
  87. package/dist/translations/en.js +52 -0
  88. package/dist/translations/en.js.map +1 -0
  89. package/dist/translations/es.js +52 -0
  90. package/dist/translations/es.js.map +1 -0
  91. package/dist/translations/fr.js +52 -0
  92. package/dist/translations/fr.js.map +1 -0
  93. package/dist/translations/id.js +52 -0
  94. package/dist/translations/id.js.map +1 -0
  95. package/dist/translations/index.d.ts +24 -0
  96. package/dist/translations/index.js +49 -0
  97. package/dist/translations/index.js.map +1 -0
  98. package/dist/translations/keys.d.ts +56 -0
  99. package/dist/translations/keys.js +56 -0
  100. package/dist/translations/keys.js.map +1 -0
  101. package/dist/translations/ko.js +52 -0
  102. package/dist/translations/ko.js.map +1 -0
  103. package/dist/translations/lookup.js +17 -0
  104. package/dist/translations/lookup.js.map +1 -0
  105. package/dist/translations/pt.js +52 -0
  106. package/dist/translations/pt.js.map +1 -0
  107. package/dist/translations/ru.js +52 -0
  108. package/dist/translations/ru.js.map +1 -0
  109. package/dist/translations/server.js +13 -0
  110. package/dist/translations/server.js.map +1 -0
  111. package/dist/translations/uk.js +52 -0
  112. package/dist/translations/uk.js.map +1 -0
  113. package/dist/translations/useTranslation.js +12 -0
  114. package/dist/translations/useTranslation.js.map +1 -0
  115. package/dist/translations/zh.js +52 -0
  116. package/dist/translations/zh.js.map +1 -0
  117. package/dist/types.d.ts +214 -0
  118. package/dist/types.js +11 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/ui/EndSessionMenuItem.d.ts +5 -0
  121. package/dist/ui/EndSessionMenuItem.js +45 -0
  122. package/dist/ui/EndSessionMenuItem.js.map +1 -0
  123. package/dist/ui/ImpersonateIcon.js +20 -0
  124. package/dist/ui/ImpersonateIcon.js.map +1 -0
  125. package/dist/ui/ImpersonationAction.d.ts +10 -0
  126. package/dist/ui/ImpersonationAction.js +48 -0
  127. package/dist/ui/ImpersonationAction.js.map +1 -0
  128. package/dist/ui/ImpersonationBar.d.ts +36 -0
  129. package/dist/ui/ImpersonationBar.js +357 -0
  130. package/dist/ui/ImpersonationBar.js.map +1 -0
  131. package/dist/ui/ImpersonationConfig.js +25 -0
  132. package/dist/ui/ImpersonationConfig.js.map +1 -0
  133. package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
  134. package/dist/ui/ImpersonationDocumentButton.js +85 -0
  135. package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
  136. package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
  137. package/dist/ui/ImpersonationFrontendBanner.js +58 -0
  138. package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
  139. package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
  140. package/dist/ui/ImpersonationFrontendExit.js +45 -0
  141. package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
  142. package/dist/ui/ImpersonationProvider.d.ts +16 -0
  143. package/dist/ui/ImpersonationProvider.js +127 -0
  144. package/dist/ui/ImpersonationProvider.js.map +1 -0
  145. package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
  146. package/dist/ui/ImpersonationSwitcher.js +251 -0
  147. package/dist/ui/ImpersonationSwitcher.js.map +1 -0
  148. package/dist/ui/ImpersonationUserCard.d.ts +31 -0
  149. package/dist/ui/ImpersonationUserCard.js +51 -0
  150. package/dist/ui/ImpersonationUserCard.js.map +1 -0
  151. package/dist/ui/StartConfirmModal.d.ts +28 -0
  152. package/dist/ui/StartConfirmModal.js +65 -0
  153. package/dist/ui/StartConfirmModal.js.map +1 -0
  154. package/dist/ui/api.js +66 -0
  155. package/dist/ui/api.js.map +1 -0
  156. package/dist/ui/barPlacement.js +107 -0
  157. package/dist/ui/barPlacement.js.map +1 -0
  158. package/dist/ui/frontendBanner.css +22 -0
  159. package/dist/ui/impersonation.css +244 -0
  160. package/dist/ui/useImpersonation.d.ts +43 -0
  161. package/dist/ui/useImpersonation.js +36 -0
  162. package/dist/ui/useImpersonation.js.map +1 -0
  163. package/package.json +126 -0
@@ -0,0 +1,332 @@
1
+ import { asId, collectionBySlug, idsEqual, userTitle } from "../ids.js";
2
+ import { findOpenByImpersonatorSid, findOpenByTargetSid, openByImpersonatorSid, sameUser } from "../session/resolve.js";
3
+ import { expireCookies, generateHintCookie } from "../auth/cookies.js";
4
+ import { revokeSession } from "../auth/revoke.js";
5
+ import { closeRecord } from "../session/close.js";
6
+ import { boundSid } from "../types.js";
7
+ import { isStartableAuthCollection } from "../plugin/startable.js";
8
+ import { callerSid, defaultRedirect, fail, getOptions, isApiKeyStrategy, isLiteralTrue, json, prepareMutation, rejectGate, safeRedirect, trimReason } from "./shared.js";
9
+ import { issueSession } from "../auth/issue.js";
10
+ import { clearOnSwitchWithoutTenant, readTenantCookie, startTenantCookies } from "../auth/tenantCookie.js";
11
+ import { resolveTargetFilters, targetMatchesFilter } from "../access/filterTargets.js";
12
+ //#region src/endpoints/start.ts
13
+ const allowedTargets = (req, options) => new Set(Object.values(req.payload.collections).filter((collection) => isStartableAuthCollection(collection.config, options)).map((collection) => collection.config.slug));
14
+ const startHandler = async (req) => {
15
+ const gate = await prepareMutation(req);
16
+ if (gate) return rejectGate(req, gate);
17
+ const options = getOptions(req);
18
+ const user = req.user;
19
+ if (!user) return fail({
20
+ error: "forbidden",
21
+ req,
22
+ status: 403
23
+ });
24
+ if (isApiKeyStrategy(user) || !callerSid(user)) return fail({
25
+ error: "unsupportedAuth",
26
+ req,
27
+ status: 403
28
+ });
29
+ const sid = boundSid(user, options.session.binding);
30
+ if (!sid) return fail({
31
+ error: "unsupportedAuth",
32
+ req,
33
+ status: 403
34
+ });
35
+ if (await findOpenByTargetSid({
36
+ options,
37
+ payload: req.payload,
38
+ req,
39
+ sid
40
+ }) ?? await findOpenByImpersonatorSid({
41
+ options,
42
+ payload: req.payload,
43
+ req,
44
+ sid
45
+ })) return fail({
46
+ error: "alreadyImpersonating",
47
+ req,
48
+ status: 409
49
+ });
50
+ const body = req.data ?? {};
51
+ const collection = typeof body.collection === "string" ? body.collection : "";
52
+ const targetId = asId(body.id);
53
+ if (!collection || targetId == null) return fail({
54
+ error: "invalidBody",
55
+ req,
56
+ status: 400
57
+ });
58
+ if (!allowedTargets(req, options).has(collection)) return fail({
59
+ error: "unsupportedCollection",
60
+ req,
61
+ status: 400
62
+ });
63
+ if (options.access.filterTargets) {
64
+ const filter = (await resolveTargetFilters({
65
+ collections: [collection],
66
+ options,
67
+ req
68
+ }))[collection];
69
+ if (filter === void 0) return fail({
70
+ error: "forbidden",
71
+ req,
72
+ status: 403
73
+ });
74
+ if (!await targetMatchesFilter({
75
+ collection,
76
+ filter,
77
+ req,
78
+ targetId
79
+ })) return fail({
80
+ error: "forbidden",
81
+ req,
82
+ status: 403
83
+ });
84
+ }
85
+ const registered = collectionBySlug(req.payload, collection);
86
+ if (!registered) return fail({
87
+ error: "targetNotFound",
88
+ req,
89
+ status: 404
90
+ });
91
+ if (sameUser(user, collection, targetId)) return fail({
92
+ error: "selfTarget",
93
+ req,
94
+ status: 400
95
+ });
96
+ const supportsTrash = Boolean(registered.config.trash);
97
+ let existing = null;
98
+ try {
99
+ existing = await req.payload.findByID({
100
+ id: targetId,
101
+ collection,
102
+ depth: 0,
103
+ overrideAccess: true,
104
+ req,
105
+ ...supportsTrash ? { trash: true } : {}
106
+ });
107
+ } catch {
108
+ existing = null;
109
+ }
110
+ if (!existing) return fail({
111
+ error: "forbidden",
112
+ req,
113
+ status: 403
114
+ });
115
+ let readable = null;
116
+ try {
117
+ readable = await req.payload.findByID({
118
+ id: targetId,
119
+ collection,
120
+ depth: 0,
121
+ overrideAccess: false,
122
+ req
123
+ });
124
+ } catch {
125
+ readable = null;
126
+ }
127
+ let allowed;
128
+ try {
129
+ allowed = await options.access.impersonate({
130
+ req,
131
+ target: readable ?? existing,
132
+ targetCollection: collection
133
+ });
134
+ } catch {
135
+ return fail({
136
+ error: "failed",
137
+ req,
138
+ status: 500
139
+ });
140
+ }
141
+ if (!isLiteralTrue(allowed)) return fail({
142
+ error: "forbidden",
143
+ req,
144
+ status: 403
145
+ });
146
+ if (existing.deletedAt) return fail({
147
+ error: "targetTrashed",
148
+ req,
149
+ status: 403
150
+ });
151
+ if (!readable) return fail({
152
+ error: "forbidden",
153
+ req,
154
+ status: 403
155
+ });
156
+ if (registered.config.auth.verify && readable._verified !== true) return fail({
157
+ error: "targetUnverified",
158
+ req,
159
+ status: 403
160
+ });
161
+ const reason = trimReason(body.reason, 500);
162
+ if (options.reason === "required" && !reason) return fail({
163
+ error: "reasonRequired",
164
+ req,
165
+ status: 400
166
+ });
167
+ const issue = options.session.issue ?? issueSession;
168
+ const revoke = options.session.revoke ?? revokeSession;
169
+ let minted;
170
+ try {
171
+ minted = await issue({
172
+ collection,
173
+ payload: req.payload,
174
+ req,
175
+ userId: targetId
176
+ });
177
+ } catch (error) {
178
+ req.payload.logger.error({
179
+ err: error,
180
+ msg: "@10x-media/impersonation: start failed to mint"
181
+ });
182
+ return fail({
183
+ error: "failed",
184
+ req,
185
+ status: 500
186
+ });
187
+ }
188
+ const now = /* @__PURE__ */ new Date();
189
+ const absoluteExpiresAt = typeof options.maxDuration === "number" ? new Date(now.getTime() + options.maxDuration * 1e3).toISOString() : void 0;
190
+ const locale = req.i18n?.language ?? req.payload.config.i18n?.fallbackLanguage ?? "en";
191
+ try {
192
+ const row = await req.payload.create({
193
+ collection: options.collectionSlug,
194
+ data: {
195
+ absoluteExpiresAt,
196
+ impersonator: {
197
+ relationTo: user.collection,
198
+ value: user.id
199
+ },
200
+ impersonatorEmail: typeof user.email === "string" ? user.email : void 0,
201
+ impersonatorLocale: locale,
202
+ impersonatorSid: sid,
203
+ impersonatorTitle: userTitle(req.payload, user.collection, user),
204
+ impersonatorTenantCookie: readTenantCookie(req.headers, req.payload.config.cookiePrefix),
205
+ ip: req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? void 0,
206
+ mode: minted.mode,
207
+ reason: options.reason === "off" ? void 0 : reason,
208
+ startedAt: now.toISOString(),
209
+ target: {
210
+ relationTo: collection,
211
+ value: targetId
212
+ },
213
+ targetEmail: typeof readable.email === "string" ? readable.email : typeof minted.user.email === "string" ? minted.user.email : void 0,
214
+ targetLocked: Boolean(readable.lockUntil),
215
+ targetSid: minted.sid,
216
+ targetTitle: userTitle(req.payload, collection, readable),
217
+ userAgent: req.headers.get("user-agent") ?? void 0
218
+ },
219
+ depth: 0,
220
+ overrideAccess: true,
221
+ req
222
+ });
223
+ const siblings = await req.payload.find({
224
+ collection: options.collectionSlug,
225
+ depth: 0,
226
+ limit: 2,
227
+ overrideAccess: true,
228
+ pagination: false,
229
+ req,
230
+ sort: "startedAt",
231
+ where: openByImpersonatorSid(sid)
232
+ });
233
+ const winner = [...siblings.docs].sort((left, right) => {
234
+ const byTime = String(left.startedAt).localeCompare(String(right.startedAt));
235
+ return byTime !== 0 ? byTime : String(left.id).localeCompare(String(right.id));
236
+ })[0];
237
+ if (siblings.totalDocs > 1 && winner && !idsEqual(winner.id, row.id)) {
238
+ await closeRecord({
239
+ endedBy: "failed",
240
+ options,
241
+ payload: req.payload,
242
+ record: row,
243
+ req
244
+ });
245
+ await revoke({
246
+ collection,
247
+ payload: req.payload,
248
+ req,
249
+ sid: minted.sid,
250
+ userId: targetId
251
+ });
252
+ return fail({
253
+ error: "alreadyImpersonating",
254
+ req,
255
+ status: 409
256
+ });
257
+ }
258
+ await options.onStart?.({
259
+ payload: req.payload,
260
+ record: row,
261
+ req
262
+ });
263
+ const authConfig = registered.config.auth;
264
+ const cookiePrefix = req.payload.config.cookiePrefix;
265
+ const cookies = [
266
+ minted.cookie,
267
+ generateHintCookie({
268
+ authConfig,
269
+ name: options.hintCookieName,
270
+ value: String(row.id)
271
+ }),
272
+ ...startTenantCookies({
273
+ authConfig,
274
+ cookiePrefix,
275
+ 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)
283
+ })
284
+ ];
285
+ const redirect = safeRedirect(body.redirect, defaultRedirect(req, collection));
286
+ const bodyOut = {
287
+ exp: minted.exp,
288
+ redirect,
289
+ user: readable
290
+ };
291
+ if (!authConfig.removeTokenFromResponses) bodyOut.token = minted.token;
292
+ return json({
293
+ body: bodyOut,
294
+ cookies,
295
+ req,
296
+ status: 200
297
+ });
298
+ } catch (error) {
299
+ req.payload.logger.error({
300
+ err: error,
301
+ msg: "@10x-media/impersonation: start failed after mint"
302
+ });
303
+ try {
304
+ await revoke({
305
+ collection,
306
+ payload: req.payload,
307
+ req,
308
+ sid: minted.sid,
309
+ userId: targetId
310
+ });
311
+ } catch (revokeError) {
312
+ req.payload.logger.error({
313
+ err: revokeError,
314
+ msg: "@10x-media/impersonation: compensating revoke failed"
315
+ });
316
+ }
317
+ return fail({
318
+ error: "failed",
319
+ req,
320
+ status: 500
321
+ });
322
+ }
323
+ };
324
+ const startEndpoint = (path) => ({
325
+ handler: startHandler,
326
+ method: "post",
327
+ path
328
+ });
329
+ //#endregion
330
+ export { startEndpoint };
331
+
332
+ //# sourceMappingURL=start.js.map
@@ -0,0 +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":";;;;;;;;;;;;AAoCA,MAAM,kBAAkB,KAAqB,YAC5C,IAAI,IACH,OAAO,OAAO,IAAI,QAAQ,WAAW,EACnC,QAAQ,eAAe,0BAA0B,WAAW,QAAQ,OAAO,CAAC,EAC5E,KAAK,eAAe,WAAW,OAAO,IAAI,CAC7C;AAED,MAAa,eAAe,OAAO,QAA2C;CAC7E,MAAM,OAAO,MAAM,gBAAgB,GAAG;CACtC,IAAI,MACH,OAAO,WAAW,KAAK,IAAI;CAG5B,MAAM,UAAU,WAAW,GAAG;CAC9B,MAAM,OAAO,IAAI;CACjB,IAAI,CAAC,MACJ,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAErD,IAAI,iBAAiB,IAAI,KAAK,CAAC,UAAU,IAAI,GAC5C,OAAO,KAAK;EAAE,OAAO;EAAmB;EAAK,QAAQ;CAAI,CAAC;CAG3D,MAAM,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO;CAClD,IAAI,CAAC,KACJ,OAAO,KAAK;EAAE,OAAO;EAAmB;EAAK,QAAQ;CAAI,CAAC;CAM3D,IAFE,MAAM,oBAAoB;EAAE;EAAS,SAAS,IAAI;EAAS;EAAK;CAAI,CAAC,KACrE,MAAM,0BAA0B;EAAE;EAAS,SAAS,IAAI;EAAS;EAAK;CAAI,CAAC,GAE5E,OAAO,KAAK;EAAE,OAAO;EAAwB;EAAK,QAAQ;CAAI,CAAC;CAGhE,MAAM,OAAQ,IAAI,QAAQ,CAAC;CAC3B,MAAM,aAAa,OAAO,KAAK,eAAe,WAAW,KAAK,aAAa;CAC3E,MAAM,WAAW,KAAK,KAAK,EAAE;CAC7B,IAAI,CAAC,cAAc,YAAY,MAC9B,OAAO,KAAK;EAAE,OAAO;EAAe;EAAK,QAAQ;CAAI,CAAC;CAGvD,IAAI,CAAC,eAAe,KAAK,OAAO,EAAE,IAAI,UAAU,GAC/C,OAAO,KAAK;EAAE,OAAO;EAAyB;EAAK,QAAQ;CAAI,CAAC;CAGjE,IAAI,QAAQ,OAAO,eAAe;EAMjC,MAAM,UAAS,MALO,qBAAqB;GAC1C,aAAa,CAAC,UAA4B;GAC1C;GACA;EACD,CAAC,GACsB;EACvB,IAAI,WAAW,KAAA,GACd,OAAO,KAAK;GAAE,OAAO;GAAa;GAAK,QAAQ;EAAI,CAAC;EAErD,IACC,CAAE,MAAM,oBAAoB;GACf;GACZ;GACA;GACA;EACD,CAAC,GAED,OAAO,KAAK;GAAE,OAAO;GAAa;GAAK,QAAQ;EAAI,CAAC;CAEtD;CAEA,MAAM,aAAa,iBAAiB,IAAI,SAAS,UAAU;CAC3D,IAAI,CAAC,YACJ,OAAO,KAAK;EAAE,OAAO;EAAkB;EAAK,QAAQ;CAAI,CAAC;CAG1D,IAAI,SAAS,MAAM,YAAY,QAAQ,GACtC,OAAO,KAAK;EAAE,OAAO;EAAc;EAAK,QAAQ;CAAI,CAAC;CAGtD,MAAM,gBAAgB,QAAS,WAAW,OAA+B,KAAK;CAC9E,IAAI,WAA2C;CAC/C,IAAI;EACH,WAAY,MAAM,IAAI,QAAQ,SAAS;GACtC,IAAI;GACQ;GACZ,OAAO;GACP,gBAAgB;GAChB;GACA,GAAI,gBAAgB,EAAE,OAAO,KAAK,IAAI,CAAC;EACxC,CAAC;CACF,QAAQ;EACP,WAAW;CACZ;CACA,IAAI,CAAC,UACJ,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,IAAI,WAA2C;CAC/C,IAAI;EACH,WAAY,MAAM,IAAI,QAAQ,SAAS;GACtC,IAAI;GACQ;GACZ,OAAO;GACP,gBAAgB;GAChB;EACD,CAAC;CACF,QAAQ;EACP,WAAW;CACZ;CAEA,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,QAAQ,OAAO,YAAY;GAC1C;GACA,QAAQ,YAAY;GACpB,kBAAkB;EACnB,CAAC;CACF,QAAQ;EACP,OAAO,KAAK;GAAE,OAAO;GAAU;GAAK,QAAQ;EAAI,CAAC;CAClD;CACA,IAAI,CAAC,cAAc,OAAO,GACzB,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,IAAI,SAAS,WACZ,OAAO,KAAK;EAAE,OAAO;EAAiB;EAAK,QAAQ;CAAI,CAAC;CAEzD,IAAI,CAAC,UACJ,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,IAAI,WAAW,OAAO,KAAK,UAAU,SAAS,cAAc,MAC3D,OAAO,KAAK;EAAE,OAAO;EAAoB;EAAK,QAAQ;CAAI,CAAC;CAG5D,MAAM,SAAS,WAAW,KAAK,QAAA,GAAyB;CACxD,IAAI,QAAQ,WAAW,cAAc,CAAC,QACrC,OAAO,KAAK;EAAE,OAAO;EAAkB;EAAK,QAAQ;CAAI,CAAC;CAG1D,MAAM,QAAQ,QAAQ,QAAQ,SAAS;CACvC,MAAM,SAAS,QAAQ,QAAQ,UAAU;CACzC,IAAI;CACJ,IAAI;EACH,SAAS,MAAM,MAAM;GACR;GACZ,SAAS,IAAI;GACb;GACA,QAAQ;EACT,CAAC;CACF,SAAS,OAAO;EACf,IAAI,QAAQ,OAAO,MAAM;GACxB,KAAK;GACL,KAAK;EACN,CAAC;EACD,OAAO,KAAK;GAAE,OAAO;GAAU;GAAK,QAAQ;EAAI,CAAC;CAClD;CAEA,MAAM,sBAAM,IAAI,KAAK;CACrB,MAAM,oBACL,OAAO,QAAQ,gBAAgB,WAC5B,IAAI,KAAK,IAAI,QAAQ,IAAI,QAAQ,cAAc,GAAI,EAAE,YAAY,IACjE,KAAA;CAEJ,MAAM,SACJ,IAA0D,MAAM,YACjE,IAAI,QAAQ,OAAO,MAAM,oBACzB;CAED,IAAI;EACH,MAAM,MAAO,MAAM,IAAI,QAAQ,OAAO;GACrC,YAAY,QAAQ;GACpB,MAAM;IACL;IACA,cAAc;KAAE,YAAY,KAAK;KAAY,OAAO,KAAK;IAAG;IAC5D,mBAAmB,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;IACjE,oBAAoB;IACpB,iBAAiB;IACjB,mBAAmB,UAClB,IAAI,SACJ,KAAK,YACL,IACD;IACA,0BAA0B,iBAAiB,IAAI,SAAS,IAAI,QAAQ,OAAO,YAAY;IACvF,IAAI,IAAI,QAAQ,IAAI,iBAAiB,KAAK,IAAI,QAAQ,IAAI,WAAW,KAAK,KAAA;IAC1E,MAAM,OAAO;IACb,QAAQ,QAAQ,WAAW,QAAQ,KAAA,IAAY;IAC/C,WAAW,IAAI,YAAY;IAC3B,QAAQ;KAAE,YAAY;KAAY,OAAO;IAAS;IAClD,aACC,OAAO,SAAS,UAAU,WACvB,SAAS,QACT,OAAO,OAAO,KAAK,UAAU,WAC5B,OAAO,KAAK,QACZ,KAAA;IACL,cAAc,QAAQ,SAAS,SAAS;IACxC,WAAW,OAAO;IAClB,aAAa,UAAU,IAAI,SAAS,YAAY,QAAQ;IACxD,WAAW,IAAI,QAAQ,IAAI,YAAY,KAAK,KAAA;GAC7C;GACA,OAAO;GACP,gBAAgB;GAChB;EACD,CAAC;EAED,MAAM,WAAW,MAAM,IAAI,QAAQ,KAAK;GACvC,YAAY,QAAQ;GACpB,OAAO;GACP,OAAO;GACP,gBAAgB;GAChB,YAAY;GACZ;GACA,MAAM;GACN,OAAO,sBAAsB,GAAG;EACjC,CAAC;EACD,MAAM,SAAS,CAAC,GAAI,SAAS,IAAyC,EAAE,MAAM,MAAM,UAAU;GAC7F,MAAM,SAAS,OAAO,KAAK,SAAS,EAAE,cAAc,OAAO,MAAM,SAAS,CAAC;GAC3E,OAAO,WAAW,IAAI,SAAS,OAAO,KAAK,EAAE,EAAE,cAAc,OAAO,MAAM,EAAE,CAAC;EAC9E,CAAC,EAAE;EACH,IAAI,SAAS,YAAY,KAAK,UAAU,CAAC,SAAS,OAAO,IAAI,IAAI,EAAE,GAAG;GACrE,MAAM,YAAY;IAAE,SAAS;IAAU;IAAS,SAAS,IAAI;IAAS,QAAQ;IAAK;GAAI,CAAC;GACxF,MAAM,OAAO;IACA;IACZ,SAAS,IAAI;IACb;IACA,KAAK,OAAO;IACZ,QAAQ;GACT,CAAC;GACD,OAAO,KAAK;IAAE,OAAO;IAAwB;IAAK,QAAQ;GAAI,CAAC;EAChE;EAEA,MAAM,QAAQ,UAAU;GAAE,SAAS,IAAI;GAAS,QAAQ;GAAK;EAAI,CAAC;EAElE,MAAM,aAAa,WAAW,OAAO;EACrC,MAAM,eAAe,IAAI,QAAQ,OAAO;EACxC,MAAM,UAAU;GACf,OAAO;GACP,mBAAmB;IAClB;IACA,MAAM,QAAQ;IACd,OAAO,OAAO,IAAI,EAAE;GACrB,CAAC;GACD,GAAG,mBAAmB;IACrB;IACA;IACA,MAAM,OAAO;IACb;IACA,QAAQ;GACT,CAAC;GACD,GAAG,cAAc;IAChB;IACA;IACA,OAAO,2BAA2B,QAAQ,QAAQ,eAAe,YAAY,EAAE,QAC7E,SAAS,SAAS,OAAO,UAC3B;GACD,CAAC;EACF;EAEA,MAAM,WAAW,aAAa,KAAK,UAAU,gBAAgB,KAAK,UAAU,CAAC;EAC7E,MAAM,UAAmC;GACxC,KAAK,OAAO;GACZ;GACA,MAAM;EACP;EACA,IAAI,CAAC,WAAW,0BACf,QAAQ,QAAQ,OAAO;EAGxB,OAAO,KAAK;GAAE,MAAM;GAAS;GAAS;GAAK,QAAQ;EAAI,CAAC;CACzD,SAAS,OAAO;EACf,IAAI,QAAQ,OAAO,MAAM;GACxB,KAAK;GACL,KAAK;EACN,CAAC;EACD,IAAI;GACH,MAAM,OAAO;IACA;IACZ,SAAS,IAAI;IACb;IACA,KAAK,OAAO;IACZ,QAAQ;GACT,CAAC;EACF,SAAS,aAAa;GACrB,IAAI,QAAQ,OAAO,MAAM;IACxB,KAAK;IACL,KAAK;GACN,CAAC;EACF;EACA,OAAO,KAAK;GAAE,OAAO;GAAU;GAAK,QAAQ;EAAI,CAAC;CAClD;AACD;AAEA,MAAa,iBAAiB,UAA4B;CACzD,SAAS;CACT,QAAQ;CACR;AACD"}
@@ -0,0 +1,73 @@
1
+ import { asId } from "../ids.js";
2
+ import { closeAndRevoke } from "../session/close.js";
3
+ import { fail, getOptions, isLiteralTrue, json, prepareMutation, rejectGate } from "./shared.js";
4
+ import { releaseLocks } from "../session/locks.js";
5
+ //#region src/endpoints/terminate.ts
6
+ const terminateHandler = async (req) => {
7
+ const gate = await prepareMutation(req);
8
+ if (gate) return rejectGate(req, gate);
9
+ const options = getOptions(req);
10
+ if (!req.user) return fail({
11
+ error: "forbidden",
12
+ req,
13
+ status: 403
14
+ });
15
+ if (!isLiteralTrue(await options.access.terminate({ req }))) return fail({
16
+ error: "forbidden",
17
+ req,
18
+ status: 403
19
+ });
20
+ const id = asId(req.routeParams?.id);
21
+ if (id == null) return fail({
22
+ error: "invalidBody",
23
+ req,
24
+ status: 400
25
+ });
26
+ let record;
27
+ try {
28
+ record = await req.payload.findByID({
29
+ id,
30
+ collection: options.collectionSlug,
31
+ depth: 0,
32
+ overrideAccess: true,
33
+ req
34
+ });
35
+ } catch {
36
+ return fail({
37
+ error: "targetNotFound",
38
+ req,
39
+ status: 404
40
+ });
41
+ }
42
+ if (record.endedAt) return json({
43
+ body: { ok: true },
44
+ req,
45
+ status: 200
46
+ });
47
+ const closed = await closeAndRevoke({
48
+ endedBy: "terminated",
49
+ options,
50
+ payload: req.payload,
51
+ record,
52
+ req
53
+ });
54
+ await releaseLocks({
55
+ payload: req.payload,
56
+ record: closed,
57
+ req
58
+ });
59
+ return json({
60
+ body: { ok: true },
61
+ req,
62
+ status: 200
63
+ });
64
+ };
65
+ const terminateEndpoint = (path) => ({
66
+ handler: terminateHandler,
67
+ method: "post",
68
+ path
69
+ });
70
+ //#endregion
71
+ export { terminateEndpoint };
72
+
73
+ //# sourceMappingURL=terminate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminate.js","names":[],"sources":["../../src/endpoints/terminate.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from 'payload'\n\nimport { asId } from '../ids'\nimport { closeAndRevoke } from '../session/close'\nimport { releaseLocks } from '../session/locks'\nimport type { ImpersonationRecord } from '../types'\nimport { fail, getOptions, isLiteralTrue, json, prepareMutation, rejectGate } from './shared'\n\nexport const terminateHandler = 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\tif (!req.user) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tconst allowed = await options.access.terminate({ req })\n\tif (!isLiteralTrue(allowed)) {\n\t\treturn fail({ error: 'forbidden', req, status: 403 })\n\t}\n\n\tconst id = asId(req.routeParams?.id)\n\tif (id == null) {\n\t\treturn fail({ error: 'invalidBody', req, status: 400 })\n\t}\n\n\tlet record: ImpersonationRecord\n\ttry {\n\t\trecord = (await req.payload.findByID({\n\t\t\tid,\n\t\t\tcollection: options.collectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})) as unknown as ImpersonationRecord\n\t} catch {\n\t\treturn fail({ error: 'targetNotFound', req, status: 404 })\n\t}\n\n\tif (record.endedAt) {\n\t\treturn json({ body: { ok: true }, req, status: 200 })\n\t}\n\n\tconst closed = await closeAndRevoke({\n\t\tendedBy: 'terminated',\n\t\toptions,\n\t\tpayload: req.payload,\n\t\trecord,\n\t\treq,\n\t})\n\tawait releaseLocks({ payload: req.payload, record: closed, req })\n\treturn json({ body: { ok: true }, req, status: 200 })\n}\n\nexport const terminateEndpoint = (path: string): Endpoint => ({\n\thandler: terminateHandler,\n\tmethod: 'post',\n\tpath,\n})\n"],"mappings":";;;;;AAQA,MAAa,mBAAmB,OAAO,QAA2C;CACjF,MAAM,OAAO,MAAM,gBAAgB,GAAG;CACtC,IAAI,MACH,OAAO,WAAW,KAAK,IAAI;CAG5B,MAAM,UAAU,WAAW,GAAG;CAC9B,IAAI,CAAC,IAAI,MACR,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAIrD,IAAI,CAAC,cAAc,MADG,QAAQ,OAAO,UAAU,EAAE,IAAI,CAAC,CAC5B,GACzB,OAAO,KAAK;EAAE,OAAO;EAAa;EAAK,QAAQ;CAAI,CAAC;CAGrD,MAAM,KAAK,KAAK,IAAI,aAAa,EAAE;CACnC,IAAI,MAAM,MACT,OAAO,KAAK;EAAE,OAAO;EAAe;EAAK,QAAQ;CAAI,CAAC;CAGvD,IAAI;CACJ,IAAI;EACH,SAAU,MAAM,IAAI,QAAQ,SAAS;GACpC;GACA,YAAY,QAAQ;GACpB,OAAO;GACP,gBAAgB;GAChB;EACD,CAAC;CACF,QAAQ;EACP,OAAO,KAAK;GAAE,OAAO;GAAkB;GAAK,QAAQ;EAAI,CAAC;CAC1D;CAEA,IAAI,OAAO,SACV,OAAO,KAAK;EAAE,MAAM,EAAE,IAAI,KAAK;EAAG;EAAK,QAAQ;CAAI,CAAC;CAGrD,MAAM,SAAS,MAAM,eAAe;EACnC,SAAS;EACT;EACA,SAAS,IAAI;EACb;EACA;CACD,CAAC;CACD,MAAM,aAAa;EAAE,SAAS,IAAI;EAAS,QAAQ;EAAQ;CAAI,CAAC;CAChE,OAAO,KAAK;EAAE,MAAM,EAAE,IAAI,KAAK;EAAG;EAAK,QAAQ;CAAI,CAAC;AACrD;AAEA,MAAa,qBAAqB,UAA4B;CAC7D,SAAS;CACT,QAAQ;CACR;AACD"}
@@ -0,0 +1,9 @@
1
+ import { EndSessionMenuItem } from "../ui/EndSessionMenuItem.js";
2
+ import { ImpersonationBar } from "../ui/ImpersonationBar.js";
3
+ import { ImpersonationDocumentButton } from "../ui/ImpersonationDocumentButton.js";
4
+ import { ImpersonationFrontendExit } from "../ui/ImpersonationFrontendExit.js";
5
+ import { ImpersonationUserCard } from "../ui/ImpersonationUserCard.js";
6
+ import { ImpersonationSwitcher } from "../ui/ImpersonationSwitcher.js";
7
+ import { StartConfirmModal } from "../ui/StartConfirmModal.js";
8
+ import { useImpersonation } from "../ui/useImpersonation.js";
9
+ export { EndSessionMenuItem, ImpersonationBar, ImpersonationDocumentButton, ImpersonationFrontendExit, ImpersonationSwitcher, ImpersonationUserCard, StartConfirmModal, useImpersonation };
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { useImpersonation } from "../ui/useImpersonation.js";
3
+ import { EndSessionMenuItem } from "../ui/EndSessionMenuItem.js";
4
+ import { ImpersonationBar } from "../ui/ImpersonationBar.js";
5
+ import { StartConfirmModal } from "../ui/StartConfirmModal.js";
6
+ import { ImpersonationDocumentButton } from "../ui/ImpersonationDocumentButton.js";
7
+ import { ImpersonationFrontendExit } from "../ui/ImpersonationFrontendExit.js";
8
+ import { ImpersonationUserCard } from "../ui/ImpersonationUserCard.js";
9
+ import { ImpersonationSwitcher } from "../ui/ImpersonationSwitcher.js";
10
+ export { EndSessionMenuItem, ImpersonationBar, ImpersonationDocumentButton, ImpersonationFrontendExit, ImpersonationSwitcher, ImpersonationUserCard, StartConfirmModal, useImpersonation };
@@ -0,0 +1,3 @@
1
+ import { TranslationKey, keys } from "../translations/keys.js";
2
+ import { TranslationsOption, translations } from "../translations/index.js";
3
+ export { type TranslationKey as ImpersonationTranslationKeys, type TranslationKey, type TranslationsOption, keys, translations };
@@ -0,0 +1,3 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { translations } from "../translations/index.js";
3
+ export { keys, translations };
@@ -0,0 +1,4 @@
1
+ import { ImpersonationAction } from "../ui/ImpersonationAction.js";
2
+ import { ImpersonationFrontendBanner } from "../ui/ImpersonationFrontendBanner.js";
3
+ import { ImpersonationProvider } from "../ui/ImpersonationProvider.js";
4
+ export { ImpersonationAction, ImpersonationFrontendBanner, ImpersonationProvider };
@@ -0,0 +1,4 @@
1
+ import { ImpersonationAction } from "../ui/ImpersonationAction.js";
2
+ import { ImpersonationFrontendBanner } from "../ui/ImpersonationFrontendBanner.js";
3
+ import { ImpersonationProvider } from "../ui/ImpersonationProvider.js";
4
+ export { ImpersonationAction, ImpersonationFrontendBanner, ImpersonationProvider };
@@ -0,0 +1,3 @@
1
+ import { FailureCode } from "../endpoints/codes.js";
2
+ import { EndedBy, FilterTargets, ImpersonateAccessArgs, ImpersonatedUser, ImpersonationActor, ImpersonationMode, ImpersonationPluginOptions, RetentionOptions, UiOptions, UserLabel } from "../types.js";
3
+ export type { EndedBy, FailureCode, FilterTargets, ImpersonateAccessArgs, ImpersonatedUser, ImpersonationActor, ImpersonationMode, ImpersonationPluginOptions, ImpersonationPluginOptions as PluginOptions, RetentionOptions, UiOptions, UserLabel };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { Payload, PayloadRequest, TypedUser } from "payload";
2
+
3
+ //#region src/getImpersonation.d.ts
4
+ type ImpersonationStatus = {
5
+ absoluteExpiresAt: null | string;
6
+ active: true;
7
+ impersonator: {
8
+ collection: string;
9
+ id: number | string;
10
+ } | null;
11
+ impersonatorLocale: null | string;
12
+ mode: 'parallel' | 'swap';
13
+ side: 'impersonator' | 'target';
14
+ startedAt: string;
15
+ target: {
16
+ collection: string;
17
+ id: number | string;
18
+ } | null;
19
+ targetEmail: null | string; /** `useAsTitle` of the target at start, else its email. */
20
+ targetTitle: null | string;
21
+ } | {
22
+ active: false;
23
+ };
24
+ /**
25
+ * Active impersonation for this request, if any.
26
+ *
27
+ * Pass `user` when you already called `payload.auth`, so this helper does not
28
+ * authenticate again. Omit `user` to authenticate from `headers`. Pass
29
+ * `user: null` when you already know the request is anonymous; that skips auth
30
+ * and returns `{ active: false }`.
31
+ *
32
+ * `side` is `target` when this request is the impersonated session, and
33
+ * `impersonator` when it is the admin side of a parallel session.
34
+ *
35
+ * Frontend (RSC):
36
+ *
37
+ * ```ts
38
+ * const { user } = await payload.auth({ headers })
39
+ * const impersonation = await getImpersonation({ headers, payload, user })
40
+ * ```
41
+ */
42
+ declare function getImpersonation(args: PayloadRequest | {
43
+ headers: Headers;
44
+ payload: Payload;
45
+ user?: null | TypedUser;
46
+ }): Promise<ImpersonationStatus>;
47
+ //#endregion
48
+ export { ImpersonationStatus, getImpersonation };
49
+ //# sourceMappingURL=getImpersonation.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { impersonationSide, isPastAbsoluteExpiry, relationOf, resolveCurrent } from "./session/resolve.js";
2
+ import { absoluteExpiryCookies } from "./auth/expiryCookies.js";
3
+ import { getRegistry } from "./plugin/registry.js";
4
+ import { closeAndRevoke } from "./session/close.js";
5
+ import { boundSid } from "./types.js";
6
+ //#region src/getImpersonation.ts
7
+ const inactive = () => ({ active: false });
8
+ const statusFromRow = (row, sid) => ({
9
+ absoluteExpiresAt: row.absoluteExpiresAt ?? null,
10
+ active: true,
11
+ impersonator: relationOf(row.impersonator),
12
+ impersonatorLocale: row.impersonatorLocale ?? null,
13
+ mode: row.mode,
14
+ side: impersonationSide(row, sid),
15
+ startedAt: row.startedAt,
16
+ target: relationOf(row.target),
17
+ targetEmail: row.targetEmail ?? null,
18
+ targetTitle: row.targetTitle ?? row.targetEmail ?? null
19
+ });
20
+ /**
21
+ * Active impersonation for this request, if any.
22
+ *
23
+ * Pass `user` when you already called `payload.auth`, so this helper does not
24
+ * authenticate again. Omit `user` to authenticate from `headers`. Pass
25
+ * `user: null` when you already know the request is anonymous; that skips auth
26
+ * and returns `{ active: false }`.
27
+ *
28
+ * `side` is `target` when this request is the impersonated session, and
29
+ * `impersonator` when it is the admin side of a parallel session.
30
+ *
31
+ * Frontend (RSC):
32
+ *
33
+ * ```ts
34
+ * const { user } = await payload.auth({ headers })
35
+ * const impersonation = await getImpersonation({ headers, payload, user })
36
+ * ```
37
+ */
38
+ async function getImpersonation(args) {
39
+ const payload = args.payload;
40
+ const headers = args.headers;
41
+ const options = getRegistry(payload.config);
42
+ if (!options) return inactive();
43
+ const user = "user" in args ? args.user : (await payload.auth({ headers })).user;
44
+ const sid = boundSid(user, options.session.binding);
45
+ if (!user || !sid) return inactive();
46
+ const row = await resolveCurrent({
47
+ headers,
48
+ options,
49
+ payload,
50
+ sid
51
+ });
52
+ if (!row) return inactive();
53
+ if (isPastAbsoluteExpiry(row)) {
54
+ await closeAndRevoke({
55
+ endedBy: "expired",
56
+ options,
57
+ payload,
58
+ record: row
59
+ });
60
+ if ("payloadAPI" in args && user) {
61
+ const cookies = await absoluteExpiryCookies({
62
+ options,
63
+ payload,
64
+ row,
65
+ sid,
66
+ user
67
+ });
68
+ args.responseHeaders ??= new Headers();
69
+ for (const cookie of cookies) args.responseHeaders.append("Set-Cookie", cookie);
70
+ }
71
+ return inactive();
72
+ }
73
+ return statusFromRow(row, sid);
74
+ }
75
+ //#endregion
76
+ export { getImpersonation, statusFromRow };
77
+
78
+ //# sourceMappingURL=getImpersonation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getImpersonation.js","names":[],"sources":["../src/getImpersonation.ts"],"sourcesContent":["import type { Payload, PayloadRequest, TypedUser } from 'payload'\n\nimport { absoluteExpiryCookies } from './auth/expiryCookies'\nimport { getRegistry } from './plugin/registry'\nimport { closeAndRevoke } from './session/close'\nimport {\n\timpersonationSide,\n\tisPastAbsoluteExpiry,\n\trelationOf,\n\tresolveCurrent,\n} from './session/resolve'\nimport type { ImpersonationRecord } from './types'\nimport { boundSid } from './types'\n\nexport type ImpersonationStatus =\n\t| {\n\t\t\tabsoluteExpiresAt: null | string\n\t\t\tactive: true\n\t\t\timpersonator: { collection: string; id: number | string } | null\n\t\t\timpersonatorLocale: null | string\n\t\t\tmode: 'parallel' | 'swap'\n\t\t\tside: 'impersonator' | 'target'\n\t\t\tstartedAt: string\n\t\t\ttarget: { collection: string; id: number | string } | null\n\t\t\ttargetEmail: null | string\n\t\t\t/** `useAsTitle` of the target at start, else its email. */\n\t\t\ttargetTitle: null | string\n\t }\n\t| { active: false }\n\nconst inactive = (): ImpersonationStatus => ({ active: false })\n\nexport const statusFromRow = (row: ImpersonationRecord, sid: string): ImpersonationStatus => ({\n\tabsoluteExpiresAt: row.absoluteExpiresAt ?? null,\n\tactive: true,\n\timpersonator: relationOf(row.impersonator),\n\timpersonatorLocale: row.impersonatorLocale ?? null,\n\tmode: row.mode,\n\tside: impersonationSide(row, sid),\n\tstartedAt: row.startedAt,\n\ttarget: relationOf(row.target),\n\ttargetEmail: row.targetEmail ?? null,\n\ttargetTitle: row.targetTitle ?? row.targetEmail ?? null,\n})\n\n/**\n * Active impersonation for this request, if any.\n *\n * Pass `user` when you already called `payload.auth`, so this helper does not\n * authenticate again. Omit `user` to authenticate from `headers`. Pass\n * `user: null` when you already know the request is anonymous; that skips auth\n * and returns `{ active: false }`.\n *\n * `side` is `target` when this request is the impersonated session, and\n * `impersonator` when it is the admin side of a parallel session.\n *\n * Frontend (RSC):\n *\n * ```ts\n * const { user } = await payload.auth({ headers })\n * const impersonation = await getImpersonation({ headers, payload, user })\n * ```\n */\nexport async function getImpersonation(\n\targs: PayloadRequest | { headers: Headers; payload: Payload; user?: null | TypedUser }\n): Promise<ImpersonationStatus> {\n\tconst payload = args.payload\n\tconst headers = args.headers\n\tconst options = getRegistry(payload.config)\n\tif (!options) {\n\t\treturn inactive()\n\t}\n\n\tconst user = 'user' in args ? args.user : (await payload.auth({ headers })).user\n\n\tconst sid = boundSid(user, options.session.binding)\n\tif (!user || !sid) {\n\t\treturn inactive()\n\t}\n\n\tconst row = await resolveCurrent({ headers, options, payload, sid })\n\tif (!row) {\n\t\treturn inactive()\n\t}\n\tif (isPastAbsoluteExpiry(row)) {\n\t\tawait closeAndRevoke({ endedBy: 'expired', options, payload, record: row })\n\t\tif ('payloadAPI' in args && user) {\n\t\t\tconst cookies = await absoluteExpiryCookies({ options, payload, row, sid, user })\n\t\t\targs.responseHeaders ??= new Headers()\n\t\t\tfor (const cookie of cookies) {\n\t\t\t\targs.responseHeaders.append('Set-Cookie', cookie)\n\t\t\t}\n\t\t}\n\t\treturn inactive()\n\t}\n\n\treturn statusFromRow(row, sid)\n}\n"],"mappings":";;;;;;AA8BA,MAAM,kBAAuC,EAAE,QAAQ,MAAM;AAE7D,MAAa,iBAAiB,KAA0B,SAAsC;CAC7F,mBAAmB,IAAI,qBAAqB;CAC5C,QAAQ;CACR,cAAc,WAAW,IAAI,YAAY;CACzC,oBAAoB,IAAI,sBAAsB;CAC9C,MAAM,IAAI;CACV,MAAM,kBAAkB,KAAK,GAAG;CAChC,WAAW,IAAI;CACf,QAAQ,WAAW,IAAI,MAAM;CAC7B,aAAa,IAAI,eAAe;CAChC,aAAa,IAAI,eAAe,IAAI,eAAe;AACpD;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,iBACrB,MAC+B;CAC/B,MAAM,UAAU,KAAK;CACrB,MAAM,UAAU,KAAK;CACrB,MAAM,UAAU,YAAY,QAAQ,MAAM;CAC1C,IAAI,CAAC,SACJ,OAAO,SAAS;CAGjB,MAAM,OAAO,UAAU,OAAO,KAAK,QAAQ,MAAM,QAAQ,KAAK,EAAE,QAAQ,CAAC,GAAG;CAE5E,MAAM,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO;CAClD,IAAI,CAAC,QAAQ,CAAC,KACb,OAAO,SAAS;CAGjB,MAAM,MAAM,MAAM,eAAe;EAAE;EAAS;EAAS;EAAS;CAAI,CAAC;CACnE,IAAI,CAAC,KACJ,OAAO,SAAS;CAEjB,IAAI,qBAAqB,GAAG,GAAG;EAC9B,MAAM,eAAe;GAAE,SAAS;GAAW;GAAS;GAAS,QAAQ;EAAI,CAAC;EAC1E,IAAI,gBAAgB,QAAQ,MAAM;GACjC,MAAM,UAAU,MAAM,sBAAsB;IAAE;IAAS;IAAS;IAAK;IAAK;GAAK,CAAC;GAChF,KAAK,oBAAoB,IAAI,QAAQ;GACrC,KAAK,MAAM,UAAU,SACpB,KAAK,gBAAgB,OAAO,cAAc,MAAM;EAElD;EACA,OAAO,SAAS;CACjB;CAEA,OAAO,cAAc,KAAK,GAAG;AAC9B"}
package/dist/ids.js ADDED
@@ -0,0 +1,27 @@
1
+ //#region src/ids.ts
2
+ /** Payload ids are strings on Mongo and numbers on Postgres; compare as text. */
3
+ const idsEqual = (a, b) => a != null && b != null && String(a) === String(b);
4
+ const asId = (value) => {
5
+ if (typeof value === "number" && Number.isFinite(value)) return value;
6
+ if (typeof value === "string" && value.length > 0) return value;
7
+ return null;
8
+ };
9
+ /** Index `payload.collections` with a runtime slug. Generated types are a closed Record. */
10
+ const collectionBySlug = (payload, slug) => payload.collections[slug];
11
+ /**
12
+ * Display name for a user doc: its collection's `useAsTitle` value, else the
13
+ * email. Snapshotted on the session row so every surface shows the same name.
14
+ */
15
+ const userTitle = (payload, slug, doc) => {
16
+ const field = collectionBySlug(payload, slug)?.config.admin?.useAsTitle;
17
+ const value = field && field !== "id" ? doc?.[field] : void 0;
18
+ if (typeof value === "string" && value.trim()) return value.trim();
19
+ if (typeof value === "number") return String(value);
20
+ return typeof doc?.email === "string" && doc.email ? doc.email : void 0;
21
+ };
22
+ /** Pick the snapshotted title or email per `ui.userLabel`; either falls back to the other. */
23
+ const labelUser = (mode, user) => (mode === "email" ? user.email || user.title : user.title || user.email) || void 0;
24
+ //#endregion
25
+ export { asId, collectionBySlug, idsEqual, labelUser, userTitle };
26
+
27
+ //# sourceMappingURL=ids.js.map