@better-auth-ui/core 1.6.30 → 1.6.32

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 (34) hide show
  1. package/dist/{create-auth-plugin-DcxR23rZ.js → create-auth-plugin-D4wsYnHD.js} +1 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +46 -15
  4. package/dist/lib/auth-mutation-keys.d.ts +5 -4
  5. package/dist/lib/auth-redirect.d.ts +31 -0
  6. package/dist/lib/view-paths.d.ts +5 -0
  7. package/dist/plugins/admin/admin-localization.d.ts +5 -0
  8. package/dist/plugins/admin/admin-mutation-keys.d.ts +9 -0
  9. package/dist/plugins/admin/admin-plugin.d.ts +32 -0
  10. package/dist/plugins/device-authorization/device-authorization-localization.d.ts +33 -0
  11. package/dist/plugins/device-authorization/device-authorization-mutation-keys.d.ts +16 -0
  12. package/dist/plugins/device-authorization/device-authorization-plugin.d.ts +59 -0
  13. package/dist/plugins/oauth-provider/oauth-provider-localization.d.ts +25 -0
  14. package/dist/plugins/oauth-provider/oauth-provider-mutation-keys.d.ts +12 -0
  15. package/dist/plugins/oauth-provider/oauth-provider-plugin.d.ts +78 -0
  16. package/dist/plugins/oauth-provider/oauth-provider-query-keys.d.ts +9 -0
  17. package/dist/plugins.d.ts +10 -0
  18. package/dist/plugins.js +193 -63
  19. package/package.json +1 -1
  20. package/src/index.ts +1 -0
  21. package/src/lib/auth-mutation-keys.ts +5 -4
  22. package/src/lib/auth-redirect.ts +105 -0
  23. package/src/lib/view-paths.ts +6 -0
  24. package/src/plugins/admin/admin-localization.ts +6 -0
  25. package/src/plugins/admin/admin-mutation-keys.ts +9 -0
  26. package/src/plugins/admin/admin-plugin.ts +52 -0
  27. package/src/plugins/device-authorization/device-authorization-localization.ts +36 -0
  28. package/src/plugins/device-authorization/device-authorization-mutation-keys.ts +16 -0
  29. package/src/plugins/device-authorization/device-authorization-plugin.ts +65 -0
  30. package/src/plugins/oauth-provider/oauth-provider-localization.ts +27 -0
  31. package/src/plugins/oauth-provider/oauth-provider-mutation-keys.ts +12 -0
  32. package/src/plugins/oauth-provider/oauth-provider-plugin.ts +132 -0
  33. package/src/plugins/oauth-provider/oauth-provider-query-keys.ts +10 -0
  34. package/src/plugins.ts +10 -0
package/dist/plugins.js CHANGED
@@ -1,6 +1,22 @@
1
- import { n as e, r as t, t as n } from "./create-auth-plugin-DcxR23rZ.js";
2
- //#region src/plugins/api-key/api-key-localization.ts
3
- var r = {
1
+ import { n as e, r as t, t as n } from "./create-auth-plugin-D4wsYnHD.js";
2
+ //#region src/plugins/admin/admin-localization.ts
3
+ var r = { stopImpersonating: "Stop impersonating" }, i = {
4
+ all: ["auth", "admin"],
5
+ stopImpersonating: [
6
+ "auth",
7
+ "admin",
8
+ "stopImpersonating"
9
+ ]
10
+ };
11
+ //#endregion
12
+ //#region src/plugins/admin/admin-plugin.ts
13
+ function a(e) {
14
+ return !e || typeof e != "object" || !("session" in e) || !e.session || typeof e.session != "object" || !("impersonatedBy" in e.session) ? !1 : typeof e.session.impersonatedBy == "string" && e.session.impersonatedBy.length > 0;
15
+ }
16
+ var o = n("admin", (e = {}) => ({ localization: {
17
+ ...r,
18
+ ...e.localization
19
+ } })), s = {
4
20
  apiKey: "API key",
5
21
  apiKeys: "API keys",
6
22
  apiKeysDescription: "Create an API key for programmatic access to your account.",
@@ -12,7 +28,7 @@ var r = {
12
28
  deleteApiKey: "Delete API key",
13
29
  deleteApiKeyWarning: "This action cannot be undone. Any service using this API key will stop working immediately.",
14
30
  dismissNewKey: "I've saved my key"
15
- }, i = {
31
+ }, c = {
16
32
  all: ["auth", "apiKey"],
17
33
  create: [
18
34
  "auth",
@@ -24,57 +40,101 @@ var r = {
24
40
  "apiKey",
25
41
  "delete"
26
42
  ]
27
- }, a = n("apiKey", (e = {}) => ({
43
+ }, l = n("apiKey", (e = {}) => ({
28
44
  localization: {
29
- ...r,
45
+ ...s,
30
46
  ...e.localization
31
47
  },
32
48
  organization: e.organization ?? !1
33
- })), o = {
49
+ })), u = {
34
50
  all: (t) => [...e.user(t), "apiKey"],
35
- lists: (e) => [...o.all(e), "list"],
36
- list: (e, t) => [...o.lists(e), t ?? null]
37
- }, s = {
51
+ lists: (e) => [...u.all(e), "list"],
52
+ list: (e, t) => [...u.lists(e), t ?? null]
53
+ }, d = {
38
54
  deleteAccount: "Delete account",
39
55
  deleteAccountDescription: "Permanently remove your account and all associated data. This cannot be undone.",
40
56
  deleteUserVerificationSent: "Check your email to confirm account deletion.",
41
57
  deleteUserSuccess: "Your account has been deleted."
42
- }, c = { deleteUser: ["auth", "deleteUser"] }, l = n("deleteUser", (e = {}) => ({
58
+ }, f = { deleteUser: ["auth", "deleteUser"] }, p = n("deleteUser", (e = {}) => ({
43
59
  localization: {
44
- ...s,
60
+ ...d,
45
61
  ...e.localization
46
62
  },
47
63
  sendDeleteAccountVerification: e.sendDeleteAccountVerification ?? !1
48
- })), u = {
64
+ })), m = {
65
+ deviceAuthorization: "Device Authorization",
66
+ deviceAuthorizationDescription: "Enter the code displayed on your device.",
67
+ deviceCode: "Device code",
68
+ invalidDeviceCode: "The code is invalid or has expired.",
69
+ continue: "Continue",
70
+ approveDevice: "Approve Device",
71
+ approveDeviceDescription: "A device is requesting access to your account.",
72
+ signedInAs: "Signed in as",
73
+ approve: "Approve",
74
+ deny: "Deny",
75
+ deviceApproved: "Device Approved",
76
+ deviceApprovedDescription: "The device can now access your account. You can return to it and continue.",
77
+ deviceDenied: "Device Denied",
78
+ deviceDeniedDescription: "The device was not granted access to your account.",
79
+ returnToApplication: "Return to application"
80
+ }, h = {
81
+ all: ["auth", "deviceAuthorization"],
82
+ verify: [
83
+ "auth",
84
+ "deviceAuthorization",
85
+ "verify"
86
+ ],
87
+ approve: [
88
+ "auth",
89
+ "deviceAuthorization",
90
+ "approve"
91
+ ],
92
+ deny: [
93
+ "auth",
94
+ "deviceAuthorization",
95
+ "deny"
96
+ ]
97
+ }, g = 8;
98
+ function _(e) {
99
+ return e === void 0 || !Number.isFinite(e) ? g : Math.max(1, Math.floor(e));
100
+ }
101
+ var v = n("deviceAuthorization", (e = {}) => ({
102
+ localization: {
103
+ ...m,
104
+ ...e.localization
105
+ },
106
+ userCodeLength: _(e.userCodeLength),
107
+ viewPaths: { auth: { deviceAuthorization: e.path ?? "device" } }
108
+ })), y = {
49
109
  lastUsed: "Last used",
50
110
  lastUsedShort: "Last"
51
- }, d = n("lastLoginMethod", (e = {}) => ({ localization: {
52
- ...u,
111
+ }, b = n("lastLoginMethod", (e = {}) => ({ localization: {
112
+ ...y,
53
113
  ...e.localization
54
- } })), f = {
114
+ } })), x = {
55
115
  magicLink: "Magic Link",
56
116
  sendMagicLink: "Send Magic Link",
57
117
  magicLinkSent: "Check your email for the magic link",
58
118
  magicLinkSentTo: "We sent a magic link to {{email}}"
59
- }, p = { signIn: [
119
+ }, S = { signIn: [
60
120
  "auth",
61
121
  "signIn",
62
122
  "magicLink"
63
- ] }, m = n("magicLink", (e = {}) => ({
123
+ ] }, C = n("magicLink", (e = {}) => ({
64
124
  localization: {
65
- ...f,
125
+ ...x,
66
126
  ...e.localization
67
127
  },
68
128
  viewPaths: { auth: {
69
129
  magicLink: e.path ?? "magic-link",
70
130
  magicLinkSent: e.sentPath ?? "magic-link-sent"
71
131
  } }
72
- })), h = {
132
+ })), w = {
73
133
  switchAccount: "Switch Account",
74
134
  addAccount: "Add Account",
75
135
  manageAccounts: "Manage accounts",
76
136
  manageAccountsDescription: "Manage your accounts for secure access."
77
- }, g = {
137
+ }, T = {
78
138
  all: ["auth", "multiSession"],
79
139
  revoke: [
80
140
  "auth",
@@ -86,14 +146,84 @@ var r = {
86
146
  "multiSession",
87
147
  "setActive"
88
148
  ]
89
- }, _ = n("multiSession", (e = {}) => ({ localization: {
90
- ...h,
149
+ }, E = n("multiSession", (e = {}) => ({ localization: {
150
+ ...w,
91
151
  ...e.localization
92
- } })), v = {
152
+ } })), D = {
93
153
  all: (t) => [...e.user(t), "multiSession"],
94
- lists: (e) => [...v.all(e), "list"],
95
- list: (e, t) => [...v.lists(e), t ?? null]
96
- }, y = {
154
+ lists: (e) => [...D.all(e), "list"],
155
+ list: (e, t) => [...D.lists(e), t ?? null]
156
+ }, O = {
157
+ authorize: "Authorize {{client}}",
158
+ authorizationDescription: "{{client}} wants to access your account.",
159
+ requestedPermissions: "This will allow {{client}} to:",
160
+ signedInAs: "Signed in as",
161
+ allow: "Allow",
162
+ cancel: "Cancel",
163
+ privacyPolicy: "Privacy policy",
164
+ termsOfService: "Terms of service",
165
+ invalidRequest: "Invalid authorization request",
166
+ invalidRequestDescription: "This authorization request is missing required information or is no longer valid.",
167
+ application: "Application"
168
+ }, k = {
169
+ all: ["auth", "oauthProvider"],
170
+ consent: [
171
+ "auth",
172
+ "oauthProvider",
173
+ "consent"
174
+ ]
175
+ }, A = {
176
+ openid: {
177
+ label: "Verify your identity",
178
+ description: "Access your unique account identifier."
179
+ },
180
+ profile: {
181
+ label: "View your profile",
182
+ description: "View your name and profile picture."
183
+ },
184
+ email: {
185
+ label: "View your email address",
186
+ description: "View your email address and whether it is verified."
187
+ },
188
+ offline_access: {
189
+ label: "Maintain access",
190
+ description: "Access your data when you are not actively using the application."
191
+ }
192
+ };
193
+ function j(e) {
194
+ if (e) try {
195
+ let t = new URL(e);
196
+ return t.protocol === "https:" || t.protocol === "http:" ? t.href : void 0;
197
+ } catch {
198
+ return;
199
+ }
200
+ }
201
+ function M(e) {
202
+ let t = new URLSearchParams(e);
203
+ return {
204
+ clientId: t.get("client_id")?.trim() || void 0,
205
+ scopes: Array.from(new Set((t.get("scope") ?? "").split(/\s+/).map((e) => e.trim()).filter(Boolean)))
206
+ };
207
+ }
208
+ var N = n("oauthProvider", (e = {}) => ({
209
+ localization: {
210
+ ...O,
211
+ ...e.localization
212
+ },
213
+ scopeMetadata: {
214
+ ...A,
215
+ ...e.scopeMetadata
216
+ },
217
+ viewPaths: { auth: { oauthConsent: e.path ?? "consent" } }
218
+ })), P = {
219
+ all: ["auth", "oauthProvider"],
220
+ publicClients: [
221
+ "auth",
222
+ "oauthProvider",
223
+ "publicClient"
224
+ ],
225
+ publicClient: (e) => [...P.publicClients, e ?? null]
226
+ }, F = {
97
227
  accept: "Accept",
98
228
  accepted: "Accepted",
99
229
  actions: "Actions",
@@ -150,7 +280,7 @@ var r = {
150
280
  status: "Status",
151
281
  uploadLogo: "Upload logo",
152
282
  userInvitationsEmptyDescription: "Invitations to join an organization will show up here."
153
- }, b = {
283
+ }, I = {
154
284
  all: ["auth", "organization"],
155
285
  create: [
156
286
  "auth",
@@ -212,9 +342,9 @@ var r = {
212
342
  "organization",
213
343
  "checkSlug"
214
344
  ]
215
- }, x = n("organization", (e = {}) => {
345
+ }, L = n("organization", (e = {}) => {
216
346
  let n = {
217
- ...y,
347
+ ...F,
218
348
  ...e.localization
219
349
  };
220
350
  return {
@@ -242,38 +372,38 @@ var r = {
242
372
  }
243
373
  }
244
374
  };
245
- }), S = {
375
+ }), R = {
246
376
  all: (t) => [...e.user(t), "organization"],
247
- lists: (e) => [...S.all(e), "list"],
248
- list: (e, t) => [...S.lists(e), t ?? null],
249
- fullDetails: (e) => [...S.all(e), "fullDetails"],
250
- fullDetail: (e, t) => [...S.fullDetails(e), t ?? null],
251
- activeOrganizations: (e) => [...S.all(e), "active"],
252
- activeOrganization: (e, t) => [...S.activeOrganizations(e), t ?? null],
377
+ lists: (e) => [...R.all(e), "list"],
378
+ list: (e, t) => [...R.lists(e), t ?? null],
379
+ fullDetails: (e) => [...R.all(e), "fullDetails"],
380
+ fullDetail: (e, t) => [...R.fullDetails(e), t ?? null],
381
+ activeOrganizations: (e) => [...R.all(e), "active"],
382
+ activeOrganization: (e, t) => [...R.activeOrganizations(e), t ?? null],
253
383
  members: {
254
- all: (e) => [...S.all(e), "members"],
255
- lists: (e) => [...S.members.all(e), "list"],
256
- list: (e, t) => [...S.members.lists(e), t ?? null]
384
+ all: (e) => [...R.all(e), "members"],
385
+ lists: (e) => [...R.members.all(e), "list"],
386
+ list: (e, t) => [...R.members.lists(e), t ?? null]
257
387
  },
258
388
  invitations: {
259
- all: (e) => [...S.all(e), "invitations"],
260
- lists: (e) => [...S.invitations.all(e), "list"],
261
- list: (e, t) => [...S.invitations.lists(e), t ?? null]
389
+ all: (e) => [...R.all(e), "invitations"],
390
+ lists: (e) => [...R.invitations.all(e), "list"],
391
+ list: (e, t) => [...R.invitations.lists(e), t ?? null]
262
392
  },
263
393
  userInvitations: {
264
- all: (e) => [...S.all(e), "userInvitations"],
265
- lists: (e) => [...S.userInvitations.all(e), "list"],
266
- list: (e, t) => [...S.userInvitations.lists(e), t ?? null]
394
+ all: (e) => [...R.all(e), "userInvitations"],
395
+ lists: (e) => [...R.userInvitations.all(e), "list"],
396
+ list: (e, t) => [...R.userInvitations.lists(e), t ?? null]
267
397
  },
268
398
  permissions: {
269
- all: (e) => [...S.all(e), "permissions"],
399
+ all: (e) => [...R.all(e), "permissions"],
270
400
  has: (e, t) => [
271
- ...S.permissions.all(e),
401
+ ...R.permissions.all(e),
272
402
  "has",
273
403
  t ?? null
274
404
  ]
275
405
  }
276
- }, C = {
406
+ }, z = {
277
407
  passkey: "Passkey",
278
408
  addPasskey: "Add passkey",
279
409
  deletePasskey: "Delete passkey {{name}}",
@@ -283,7 +413,7 @@ var r = {
283
413
  passkeysDescription: "Create a passkey to securely access your account.",
284
414
  noPasskeys: "No passkeys",
285
415
  name: "Name"
286
- }, w = {
416
+ }, B = {
287
417
  signIn: [
288
418
  "auth",
289
419
  "signIn",
@@ -299,22 +429,22 @@ var r = {
299
429
  "passkey",
300
430
  "deletePasskey"
301
431
  ]
302
- }, T = n("passkey", (e = {}) => ({ localization: {
303
- ...C,
432
+ }, V = n("passkey", (e = {}) => ({ localization: {
433
+ ...z,
304
434
  ...e.localization
305
- } })), E = {
435
+ } })), H = {
306
436
  all: (t) => [...e.user(t), "passkey"],
307
- lists: (e) => [...E.all(e), "list"],
308
- list: (e, t) => [...E.lists(e), t ?? null]
309
- }, D = {
437
+ lists: (e) => [...H.all(e), "list"],
438
+ list: (e, t) => [...H.lists(e), t ?? null]
439
+ }, U = {
310
440
  appearance: "Appearance",
311
441
  theme: "Theme",
312
442
  system: "System",
313
443
  light: "Light",
314
444
  dark: "Dark"
315
- }, O = n("theme", (e) => ({
445
+ }, W = n("theme", (e) => ({
316
446
  localization: {
317
- ...D,
447
+ ...U,
318
448
  ...e.localization
319
449
  },
320
450
  theme: e.theme ?? "system",
@@ -324,7 +454,7 @@ var r = {
324
454
  "light",
325
455
  "dark"
326
456
  ]
327
- })), k = {
457
+ })), G = {
328
458
  username: "Username",
329
459
  usernamePlaceholder: "Username",
330
460
  usernameOrEmailPlaceholder: "Username or email",
@@ -332,16 +462,16 @@ var r = {
332
462
  usernameTaken: "Username is already taken. Please try another.",
333
463
  displayUsername: "Display Username",
334
464
  displayUsernamePlaceholder: "Display Username"
335
- }, A = {
465
+ }, K = {
336
466
  signIn: [
337
467
  "auth",
338
468
  "signIn",
339
469
  "username"
340
470
  ],
341
471
  isUsernameAvailable: ["auth", "isUsernameAvailable"]
342
- }, j = n("username", (e = {}) => {
472
+ }, q = n("username", (e = {}) => {
343
473
  let t = e.minUsernameLength ?? 3, n = e.maxUsernameLength ?? 30, r = {
344
- ...k,
474
+ ...G,
345
475
  ...e.localization
346
476
  };
347
477
  return {
@@ -369,4 +499,4 @@ var r = {
369
499
  };
370
500
  });
371
501
  //#endregion
372
- export { r as apiKeyLocalization, i as apiKeyMutationKeys, a as apiKeyPlugin, o as apiKeyQueryKeys, s as deleteUserLocalization, c as deleteUserMutationKeys, l as deleteUserPlugin, u as lastLoginMethodLocalization, d as lastLoginMethodPlugin, f as magicLinkLocalization, p as magicLinkMutationKeys, m as magicLinkPlugin, h as multiSessionLocalization, g as multiSessionMutationKeys, _ as multiSessionPlugin, v as multiSessionQueryKeys, y as organizationLocalization, b as organizationMutationKeys, x as organizationPlugin, S as organizationQueryKeys, C as passkeyLocalization, w as passkeyMutationKeys, T as passkeyPlugin, E as passkeyQueryKeys, D as themeLocalization, O as themePlugin, k as usernameLocalization, A as usernameMutationKeys, j as usernamePlugin };
502
+ export { r as adminLocalization, i as adminMutationKeys, o as adminPlugin, s as apiKeyLocalization, c as apiKeyMutationKeys, l as apiKeyPlugin, u as apiKeyQueryKeys, d as deleteUserLocalization, f as deleteUserMutationKeys, p as deleteUserPlugin, m as deviceAuthorizationLocalization, h as deviceAuthorizationMutationKeys, v as deviceAuthorizationPlugin, a as isImpersonatingSession, y as lastLoginMethodLocalization, b as lastLoginMethodPlugin, x as magicLinkLocalization, S as magicLinkMutationKeys, C as magicLinkPlugin, w as multiSessionLocalization, T as multiSessionMutationKeys, E as multiSessionPlugin, D as multiSessionQueryKeys, O as oauthProviderLocalization, k as oauthProviderMutationKeys, N as oauthProviderPlugin, P as oauthProviderQueryKeys, A as oauthProviderScopeMetadata, F as organizationLocalization, I as organizationMutationKeys, L as organizationPlugin, R as organizationQueryKeys, M as parseOAuthAuthorizationRequest, z as passkeyLocalization, B as passkeyMutationKeys, V as passkeyPlugin, H as passkeyQueryKeys, j as sanitizeOAuthClientUrl, U as themeLocalization, W as themePlugin, G as usernameLocalization, K as usernameMutationKeys, q as usernamePlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth-ui/core",
3
- "version": "1.6.30",
3
+ "version": "1.6.32",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "vite build",
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./config"
2
2
  export * from "./lib/auth-mutation-keys"
3
3
  export * from "./lib/auth-plugin"
4
4
  export * from "./lib/auth-query-keys"
5
+ export * from "./lib/auth-redirect"
5
6
  export * from "./lib/base-paths"
6
7
  export * from "./lib/create-auth-plugin"
7
8
  export * from "./lib/deep-partial"
@@ -19,10 +19,11 @@
19
19
  * package the mutation options factory came from.
20
20
  *
21
21
  * Plugin-specific mutation keys live alongside their plugin (e.g.
22
- * `apiKeyMutationKeys`, `organizationMutationKeys`, `passkeyMutationKeys`,
23
- * `magicLinkMutationKeys`, `multiSessionMutationKeys`, `usernameMutationKeys`,
24
- * `deleteUserMutationKeys`). Plugin-contributed sign-in strategies stay
25
- * under the shared `["auth", "signIn", ...]` namespace so
22
+ * `apiKeyMutationKeys`, `deviceAuthorizationMutationKeys`,
23
+ * `organizationMutationKeys`, `passkeyMutationKeys`, `magicLinkMutationKeys`,
24
+ * `multiSessionMutationKeys`, `usernameMutationKeys`, `deleteUserMutationKeys`).
25
+ * Plugin-contributed sign-in strategies stay under the shared
26
+ * `["auth", "signIn", ...]` namespace so
26
27
  * `useIsMutating({ mutationKey: authMutationKeys.signIn.all })` still
27
28
  * matches them.
28
29
  *
@@ -0,0 +1,105 @@
1
+ const ABSOLUTE_HTTP_URL = /^https?:\/\//i
2
+
3
+ function hasUnsafeRedirectCharacters(value: string): boolean {
4
+ for (const character of value) {
5
+ const codePoint = character.codePointAt(0)
6
+ if (
7
+ character === "\\" ||
8
+ codePoint === undefined ||
9
+ codePoint <= 31 ||
10
+ codePoint === 127
11
+ ) {
12
+ return true
13
+ }
14
+ }
15
+
16
+ return false
17
+ }
18
+
19
+ /**
20
+ * Normalize a redirect target to a same-origin path.
21
+ *
22
+ * Root-relative paths and same-origin HTTP(S) URLs are accepted. Invalid,
23
+ * cross-origin, protocol-relative, and non-HTTP targets fall back to `/`.
24
+ *
25
+ * @param redirectTo - Requested redirect target
26
+ * @param origin - Origin used to validate and normalize the target
27
+ * @returns A same-origin path including its query string and hash
28
+ */
29
+ export function getSafeRedirectTo(
30
+ redirectTo: string | null | undefined,
31
+ origin: string
32
+ ): string {
33
+ if (!redirectTo || hasUnsafeRedirectCharacters(redirectTo)) return "/"
34
+
35
+ const target = redirectTo.trim()
36
+ if (
37
+ !target ||
38
+ target.startsWith("//") ||
39
+ (!target.startsWith("/") && !ABSOLUTE_HTTP_URL.test(target))
40
+ ) {
41
+ return "/"
42
+ }
43
+
44
+ try {
45
+ const baseURL = new URL(origin)
46
+ const targetURL = new URL(target, baseURL)
47
+
48
+ if (
49
+ targetURL.origin !== baseURL.origin ||
50
+ targetURL.username ||
51
+ targetURL.password
52
+ ) {
53
+ return "/"
54
+ }
55
+
56
+ return `${targetURL.pathname}${targetURL.search}${targetURL.hash}`
57
+ } catch {
58
+ return "/"
59
+ }
60
+ }
61
+
62
+ export type AuthRedirectAction =
63
+ | { type: "redirect"; to: string }
64
+ | { type: "signIn"; to: string }
65
+
66
+ /**
67
+ * Resolve the next action for the authenticated redirect view.
68
+ *
69
+ * Signed-in users continue to the validated target. Signed-out users are sent
70
+ * to sign in with the current redirect-view URL preserved, allowing the view
71
+ * to perform a full-page redirect after authentication.
72
+ *
73
+ * @param currentURL - Current redirect-view URL
74
+ * @param authenticated - Whether the current user has a session
75
+ * @param signInPath - Same-origin path to the sign-in view
76
+ * @returns The redirect or sign-in action to perform
77
+ */
78
+ export function getAuthRedirectAction(
79
+ currentURL: URL,
80
+ authenticated: boolean,
81
+ signInPath: string
82
+ ): AuthRedirectAction {
83
+ const requestedTarget = getSafeRedirectTo(
84
+ currentURL.searchParams.get("redirectTo"),
85
+ currentURL.origin
86
+ )
87
+ const targetURL = new URL(requestedTarget, currentURL.origin)
88
+ const redirectTo =
89
+ targetURL.pathname === currentURL.pathname ? "/" : requestedTarget
90
+
91
+ if (authenticated) {
92
+ return { type: "redirect", to: redirectTo }
93
+ }
94
+
95
+ const signInURL = new URL(signInPath, currentURL.origin)
96
+ signInURL.searchParams.set(
97
+ "redirectTo",
98
+ `${currentURL.pathname}${currentURL.search}${currentURL.hash}`
99
+ )
100
+
101
+ return {
102
+ type: "signIn",
103
+ to: `${signInURL.pathname}${signInURL.search}${signInURL.hash}`
104
+ }
105
+ }
@@ -2,6 +2,11 @@
2
2
  * View path segments for authentication routes.
3
3
  */
4
4
  export interface AuthViewPaths {
5
+ /**
6
+ * Path segment for the authenticated redirect view
7
+ * @default "redirect"
8
+ */
9
+ redirect: string
5
10
  /**
6
11
  * Path segment for the sign-in view
7
12
  * @default "sign-in"
@@ -67,6 +72,7 @@ export type ViewPaths = {
67
72
 
68
73
  export const viewPaths: ViewPaths = {
69
74
  auth: {
75
+ redirect: "redirect",
70
76
  signIn: "sign-in",
71
77
  signUp: "sign-up",
72
78
  forgotPassword: "forgot-password",
@@ -0,0 +1,6 @@
1
+ export const adminLocalization = {
2
+ /** @remarks `"Stop impersonating"` */
3
+ stopImpersonating: "Stop impersonating"
4
+ }
5
+
6
+ export type AdminLocalization = typeof adminLocalization
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Mutation keys contributed by the admin plugin.
3
+ */
4
+ export const adminMutationKeys = {
5
+ /** Root key for every admin mutation. */
6
+ all: ["auth", "admin"] as const,
7
+ /** Key for `admin.stopImpersonating`. */
8
+ stopImpersonating: ["auth", "admin", "stopImpersonating"] as const
9
+ } as const
@@ -0,0 +1,52 @@
1
+ import { createAuthPlugin } from "../../lib/create-auth-plugin"
2
+ import { type AdminLocalization, adminLocalization } from "./admin-localization"
3
+
4
+ export type AdminPluginOptions = {
5
+ /**
6
+ * Override the plugin's default localization strings.
7
+ * @remarks `AdminLocalization`
8
+ */
9
+ localization?: Partial<AdminLocalization>
10
+ }
11
+
12
+ export type ImpersonatingSession = {
13
+ session: {
14
+ impersonatedBy: string
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Check whether a Better Auth session belongs to an impersonated user.
20
+ */
21
+ export function isImpersonatingSession(
22
+ session: unknown
23
+ ): session is ImpersonatingSession {
24
+ if (
25
+ !session ||
26
+ typeof session !== "object" ||
27
+ !("session" in session) ||
28
+ !session.session ||
29
+ typeof session.session !== "object" ||
30
+ !("impersonatedBy" in session.session)
31
+ ) {
32
+ return false
33
+ }
34
+
35
+ return (
36
+ typeof session.session.impersonatedBy === "string" &&
37
+ session.session.impersonatedBy.length > 0
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Adds UI integrations for Better Auth's admin plugin.
43
+ *
44
+ * Pair this UI plugin with Better Auth's `admin()` server plugin and
45
+ * `adminClient()` client plugin.
46
+ */
47
+ export const adminPlugin = createAuthPlugin(
48
+ "admin",
49
+ (options: AdminPluginOptions = {}) => ({
50
+ localization: { ...adminLocalization, ...options.localization }
51
+ })
52
+ )
@@ -0,0 +1,36 @@
1
+ export const deviceAuthorizationLocalization = {
2
+ /** @remarks `"Device Authorization"` */
3
+ deviceAuthorization: "Device Authorization",
4
+ /** @remarks `"Enter the code displayed on your device."` */
5
+ deviceAuthorizationDescription: "Enter the code displayed on your device.",
6
+ /** @remarks `"Device code"` */
7
+ deviceCode: "Device code",
8
+ /** @remarks `"The code is invalid or has expired."` */
9
+ invalidDeviceCode: "The code is invalid or has expired.",
10
+ /** @remarks `"Continue"` */
11
+ continue: "Continue",
12
+ /** @remarks `"Approve Device"` */
13
+ approveDevice: "Approve Device",
14
+ /** @remarks `"A device is requesting access to your account."` */
15
+ approveDeviceDescription: "A device is requesting access to your account.",
16
+ /** @remarks `"Signed in as"` */
17
+ signedInAs: "Signed in as",
18
+ /** @remarks `"Approve"` */
19
+ approve: "Approve",
20
+ /** @remarks `"Deny"` */
21
+ deny: "Deny",
22
+ /** @remarks `"Device Approved"` */
23
+ deviceApproved: "Device Approved",
24
+ /** @remarks `"The device can now access your account. You can return to it and continue."` */
25
+ deviceApprovedDescription:
26
+ "The device can now access your account. You can return to it and continue.",
27
+ /** @remarks `"Device Denied"` */
28
+ deviceDenied: "Device Denied",
29
+ /** @remarks `"The device was not granted access to your account."` */
30
+ deviceDeniedDescription: "The device was not granted access to your account.",
31
+ /** @remarks `"Return to application"` */
32
+ returnToApplication: "Return to application"
33
+ }
34
+
35
+ export type DeviceAuthorizationLocalization =
36
+ typeof deviceAuthorizationLocalization
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Mutation keys contributed by the device-authorization plugin.
3
+ *
4
+ * All keys live under the shared `["auth"]` namespace so global auth
5
+ * mutation observers handle errors consistently.
6
+ */
7
+ export const deviceAuthorizationMutationKeys = {
8
+ /** Prefix matching every device-authorization mutation. */
9
+ all: ["auth", "deviceAuthorization"] as const,
10
+ /** Key for verifying and claiming a user code. */
11
+ verify: ["auth", "deviceAuthorization", "verify"] as const,
12
+ /** Key for approving a pending device request. */
13
+ approve: ["auth", "deviceAuthorization", "approve"] as const,
14
+ /** Key for denying a pending device request. */
15
+ deny: ["auth", "deviceAuthorization", "deny"] as const
16
+ } as const