@better-auth-ui/core 1.6.30 → 1.6.31

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.
@@ -143,6 +143,7 @@ function o(e, t) {
143
143
  //#region src/lib/view-paths.ts
144
144
  var s = {
145
145
  auth: {
146
+ redirect: "redirect",
146
147
  signIn: "sign-in",
147
148
  signUp: "sign-up",
148
149
  forgotPassword: "forgot-password",
package/dist/index.d.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';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c } from "./create-auth-plugin-DcxR23rZ.js";
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c } from "./create-auth-plugin-D4wsYnHD.js";
2
2
  //#region src/config/additional-fields-config.ts
3
3
  function l(e, t) {
4
4
  if (e.type === "boolean") return t === "on" || t === "true";
@@ -59,7 +59,38 @@ var d = {
59
59
  revokeSession: ["auth", "revokeSession"],
60
60
  unlinkAccount: ["auth", "unlinkAccount"],
61
61
  updateUser: ["auth", "updateUser"]
62
- }, f = {
62
+ }, f = /^https?:\/\//i;
63
+ function p(e) {
64
+ for (let t of e) {
65
+ let e = t.codePointAt(0);
66
+ if (t === "\\" || e === void 0 || e <= 31 || e === 127) return !0;
67
+ }
68
+ return !1;
69
+ }
70
+ function m(e, t) {
71
+ if (!e || p(e)) return "/";
72
+ let n = e.trim();
73
+ if (!n || n.startsWith("//") || !n.startsWith("/") && !f.test(n)) return "/";
74
+ try {
75
+ let e = new URL(t), r = new URL(n, e);
76
+ return r.origin !== e.origin || r.username || r.password ? "/" : `${r.pathname}${r.search}${r.hash}`;
77
+ } catch {
78
+ return "/";
79
+ }
80
+ }
81
+ function h(e, t, n) {
82
+ let r = m(e.searchParams.get("redirectTo"), e.origin), i = new URL(r, e.origin).pathname === e.pathname ? "/" : r;
83
+ if (t) return {
84
+ type: "redirect",
85
+ to: i
86
+ };
87
+ let a = new URL(n, e.origin);
88
+ return a.searchParams.set("redirectTo", `${e.pathname}${e.search}${e.hash}`), {
89
+ type: "signIn",
90
+ to: `${a.pathname}${a.search}${a.hash}`
91
+ };
92
+ }
93
+ var g = {
63
94
  version: "2.0",
64
95
  providers: [
65
96
  {
@@ -1476,26 +1507,26 @@ var d = {
1476
1507
  domains: 223,
1477
1508
  generated: "2025-12-09T10:38:50.000Z"
1478
1509
  }
1479
- }, p;
1480
- function m() {
1481
- if (p) return p;
1482
- p = /* @__PURE__ */ new Map();
1483
- let { providers: e } = f;
1484
- for (let t of e) if (t.loginUrl) for (let e of t.domains ?? []) p.set(e.toLowerCase(), {
1510
+ }, _;
1511
+ function v() {
1512
+ if (_) return _;
1513
+ _ = /* @__PURE__ */ new Map();
1514
+ let { providers: e } = g;
1515
+ for (let t of e) if (t.loginUrl) for (let e of t.domains ?? []) _.set(e.toLowerCase(), {
1485
1516
  companyProvider: t.companyProvider,
1486
1517
  loginUrl: t.loginUrl
1487
1518
  });
1488
- return p;
1519
+ return _;
1489
1520
  }
1490
- function h(e) {
1521
+ function y(e) {
1491
1522
  let t = e.lastIndexOf("@");
1492
1523
  if (t === -1) return null;
1493
1524
  let n = e.slice(t + 1).trim().toLowerCase();
1494
- return n ? m().get(n) ?? null : null;
1525
+ return n ? v().get(n) ?? null : null;
1495
1526
  }
1496
1527
  //#endregion
1497
1528
  //#region src/lib/provider-names.ts
1498
- var g = {
1529
+ var b = {
1499
1530
  apple: "Apple",
1500
1531
  atlassian: "Atlassian",
1501
1532
  cognito: "Cognito",
@@ -1532,8 +1563,8 @@ var g = {
1532
1563
  wechat: "WeChat",
1533
1564
  zoom: "Zoom"
1534
1565
  };
1535
- function _(e) {
1536
- return g[e] || e.charAt(0).toUpperCase() + e.slice(1);
1566
+ function x(e) {
1567
+ return b[e] || e.charAt(0).toUpperCase() + e.slice(1);
1537
1568
  }
1538
1569
  //#endregion
1539
- export { d as authMutationKeys, i as authQueryKeys, r as basePaths, c as createAuthPlugin, e as deepmerge, o as defaultAuthConfig, a as fileToBase64, h as getEmailProviderLink, _ as getProviderName, t as localization, l as parseAdditionalFieldValue, g as providerNames, s as resizeAvatar, u as resolveInputType, n as viewPaths };
1570
+ export { d as authMutationKeys, i as authQueryKeys, r as basePaths, c as createAuthPlugin, e as deepmerge, o as defaultAuthConfig, a as fileToBase64, h as getAuthRedirectAction, y as getEmailProviderLink, x as getProviderName, m as getSafeRedirectTo, t as localization, l as parseAdditionalFieldValue, b as providerNames, s as resizeAvatar, u as resolveInputType, n as viewPaths };
@@ -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,31 @@
1
+ /**
2
+ * Normalize a redirect target to a same-origin path.
3
+ *
4
+ * Root-relative paths and same-origin HTTP(S) URLs are accepted. Invalid,
5
+ * cross-origin, protocol-relative, and non-HTTP targets fall back to `/`.
6
+ *
7
+ * @param redirectTo - Requested redirect target
8
+ * @param origin - Origin used to validate and normalize the target
9
+ * @returns A same-origin path including its query string and hash
10
+ */
11
+ export declare function getSafeRedirectTo(redirectTo: string | null | undefined, origin: string): string;
12
+ export type AuthRedirectAction = {
13
+ type: "redirect";
14
+ to: string;
15
+ } | {
16
+ type: "signIn";
17
+ to: string;
18
+ };
19
+ /**
20
+ * Resolve the next action for the authenticated redirect view.
21
+ *
22
+ * Signed-in users continue to the validated target. Signed-out users are sent
23
+ * to sign in with the current redirect-view URL preserved, allowing the view
24
+ * to perform a full-page redirect after authentication.
25
+ *
26
+ * @param currentURL - Current redirect-view URL
27
+ * @param authenticated - Whether the current user has a session
28
+ * @param signInPath - Same-origin path to the sign-in view
29
+ * @returns The redirect or sign-in action to perform
30
+ */
31
+ export declare function getAuthRedirectAction(currentURL: URL, authenticated: boolean, signInPath: string): AuthRedirectAction;
@@ -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"
@@ -0,0 +1,33 @@
1
+ export declare const deviceAuthorizationLocalization: {
2
+ /** @remarks `"Device Authorization"` */
3
+ deviceAuthorization: string;
4
+ /** @remarks `"Enter the code displayed on your device."` */
5
+ deviceAuthorizationDescription: string;
6
+ /** @remarks `"Device code"` */
7
+ deviceCode: string;
8
+ /** @remarks `"The code is invalid or has expired."` */
9
+ invalidDeviceCode: string;
10
+ /** @remarks `"Continue"` */
11
+ continue: string;
12
+ /** @remarks `"Approve Device"` */
13
+ approveDevice: string;
14
+ /** @remarks `"A device is requesting access to your account."` */
15
+ approveDeviceDescription: string;
16
+ /** @remarks `"Signed in as"` */
17
+ signedInAs: string;
18
+ /** @remarks `"Approve"` */
19
+ approve: string;
20
+ /** @remarks `"Deny"` */
21
+ deny: string;
22
+ /** @remarks `"Device Approved"` */
23
+ deviceApproved: string;
24
+ /** @remarks `"The device can now access your account. You can return to it and continue."` */
25
+ deviceApprovedDescription: string;
26
+ /** @remarks `"Device Denied"` */
27
+ deviceDenied: string;
28
+ /** @remarks `"The device was not granted access to your account."` */
29
+ deviceDeniedDescription: string;
30
+ /** @remarks `"Return to application"` */
31
+ returnToApplication: string;
32
+ };
33
+ export type DeviceAuthorizationLocalization = 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 declare const deviceAuthorizationMutationKeys: {
8
+ /** Prefix matching every device-authorization mutation. */
9
+ readonly all: readonly ["auth", "deviceAuthorization"];
10
+ /** Key for verifying and claiming a user code. */
11
+ readonly verify: readonly ["auth", "deviceAuthorization", "verify"];
12
+ /** Key for approving a pending device request. */
13
+ readonly approve: readonly ["auth", "deviceAuthorization", "approve"];
14
+ /** Key for denying a pending device request. */
15
+ readonly deny: readonly ["auth", "deviceAuthorization", "deny"];
16
+ };
@@ -0,0 +1,59 @@
1
+ import { DeviceAuthorizationLocalization } from './device-authorization-localization';
2
+ declare module "../../lib/view-paths" {
3
+ /** Widens `AuthViewPaths` with the device-authorization path when this plugin is imported. */
4
+ interface AuthViewPaths {
5
+ /** @default "device" */
6
+ deviceAuthorization?: string;
7
+ }
8
+ }
9
+ export type DeviceAuthorizationPluginOptions = {
10
+ /**
11
+ * Override the plugin's default localization strings.
12
+ * @remarks `DeviceAuthorizationLocalization`
13
+ */
14
+ localization?: Partial<DeviceAuthorizationLocalization>;
15
+ /**
16
+ * URL segment for the device-authorization view.
17
+ * @remarks `string`
18
+ * @default "device"
19
+ */
20
+ path?: string;
21
+ /**
22
+ * Number of characters rendered by the device-code input.
23
+ *
24
+ * Keep this in sync with Better Auth's `deviceAuthorization({ userCodeLength })`
25
+ * server option.
26
+ *
27
+ * @default 8
28
+ */
29
+ userCodeLength?: number;
30
+ };
31
+ export declare const deviceAuthorizationPlugin: ((options?: DeviceAuthorizationPluginOptions | undefined) => {
32
+ localization: {
33
+ deviceAuthorization: string;
34
+ deviceAuthorizationDescription: string;
35
+ deviceCode: string;
36
+ invalidDeviceCode: string;
37
+ continue: string;
38
+ approveDevice: string;
39
+ approveDeviceDescription: string;
40
+ signedInAs: string;
41
+ approve: string;
42
+ deny: string;
43
+ deviceApproved: string;
44
+ deviceApprovedDescription: string;
45
+ deviceDenied: string;
46
+ deviceDeniedDescription: string;
47
+ returnToApplication: string;
48
+ };
49
+ userCodeLength: number;
50
+ viewPaths: {
51
+ auth: {
52
+ deviceAuthorization: string;
53
+ };
54
+ };
55
+ } & {
56
+ id: "deviceAuthorization";
57
+ }) & {
58
+ id: "deviceAuthorization";
59
+ };
package/dist/plugins.d.ts CHANGED
@@ -5,6 +5,9 @@ export * from './plugins/api-key/api-key-query-keys';
5
5
  export * from './plugins/delete-user/delete-user-localization';
6
6
  export * from './plugins/delete-user/delete-user-mutation-keys';
7
7
  export * from './plugins/delete-user/delete-user-plugin';
8
+ export * from './plugins/device-authorization/device-authorization-localization';
9
+ export * from './plugins/device-authorization/device-authorization-mutation-keys';
10
+ export * from './plugins/device-authorization/device-authorization-plugin';
8
11
  export * from './plugins/last-login-method/last-login-method-localization';
9
12
  export * from './plugins/last-login-method/last-login-method-plugin';
10
13
  export * from './plugins/magic-link/magic-link-localization';
package/dist/plugins.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./create-auth-plugin-DcxR23rZ.js";
1
+ import { n as e, r as t, t as n } from "./create-auth-plugin-D4wsYnHD.js";
2
2
  //#region src/plugins/api-key/api-key-localization.ts
3
3
  var r = {
4
4
  apiKey: "API key",
@@ -46,35 +46,79 @@ var r = {
46
46
  },
47
47
  sendDeleteAccountVerification: e.sendDeleteAccountVerification ?? !1
48
48
  })), u = {
49
+ deviceAuthorization: "Device Authorization",
50
+ deviceAuthorizationDescription: "Enter the code displayed on your device.",
51
+ deviceCode: "Device code",
52
+ invalidDeviceCode: "The code is invalid or has expired.",
53
+ continue: "Continue",
54
+ approveDevice: "Approve Device",
55
+ approveDeviceDescription: "A device is requesting access to your account.",
56
+ signedInAs: "Signed in as",
57
+ approve: "Approve",
58
+ deny: "Deny",
59
+ deviceApproved: "Device Approved",
60
+ deviceApprovedDescription: "The device can now access your account. You can return to it and continue.",
61
+ deviceDenied: "Device Denied",
62
+ deviceDeniedDescription: "The device was not granted access to your account.",
63
+ returnToApplication: "Return to application"
64
+ }, d = {
65
+ all: ["auth", "deviceAuthorization"],
66
+ verify: [
67
+ "auth",
68
+ "deviceAuthorization",
69
+ "verify"
70
+ ],
71
+ approve: [
72
+ "auth",
73
+ "deviceAuthorization",
74
+ "approve"
75
+ ],
76
+ deny: [
77
+ "auth",
78
+ "deviceAuthorization",
79
+ "deny"
80
+ ]
81
+ }, f = 8;
82
+ function p(e) {
83
+ return e === void 0 || !Number.isFinite(e) ? f : Math.max(1, Math.floor(e));
84
+ }
85
+ var m = n("deviceAuthorization", (e = {}) => ({
86
+ localization: {
87
+ ...u,
88
+ ...e.localization
89
+ },
90
+ userCodeLength: p(e.userCodeLength),
91
+ viewPaths: { auth: { deviceAuthorization: e.path ?? "device" } }
92
+ })), h = {
49
93
  lastUsed: "Last used",
50
94
  lastUsedShort: "Last"
51
- }, d = n("lastLoginMethod", (e = {}) => ({ localization: {
52
- ...u,
95
+ }, g = n("lastLoginMethod", (e = {}) => ({ localization: {
96
+ ...h,
53
97
  ...e.localization
54
- } })), f = {
98
+ } })), _ = {
55
99
  magicLink: "Magic Link",
56
100
  sendMagicLink: "Send Magic Link",
57
101
  magicLinkSent: "Check your email for the magic link",
58
102
  magicLinkSentTo: "We sent a magic link to {{email}}"
59
- }, p = { signIn: [
103
+ }, v = { signIn: [
60
104
  "auth",
61
105
  "signIn",
62
106
  "magicLink"
63
- ] }, m = n("magicLink", (e = {}) => ({
107
+ ] }, y = n("magicLink", (e = {}) => ({
64
108
  localization: {
65
- ...f,
109
+ ..._,
66
110
  ...e.localization
67
111
  },
68
112
  viewPaths: { auth: {
69
113
  magicLink: e.path ?? "magic-link",
70
114
  magicLinkSent: e.sentPath ?? "magic-link-sent"
71
115
  } }
72
- })), h = {
116
+ })), b = {
73
117
  switchAccount: "Switch Account",
74
118
  addAccount: "Add Account",
75
119
  manageAccounts: "Manage accounts",
76
120
  manageAccountsDescription: "Manage your accounts for secure access."
77
- }, g = {
121
+ }, x = {
78
122
  all: ["auth", "multiSession"],
79
123
  revoke: [
80
124
  "auth",
@@ -86,14 +130,14 @@ var r = {
86
130
  "multiSession",
87
131
  "setActive"
88
132
  ]
89
- }, _ = n("multiSession", (e = {}) => ({ localization: {
90
- ...h,
133
+ }, S = n("multiSession", (e = {}) => ({ localization: {
134
+ ...b,
91
135
  ...e.localization
92
- } })), v = {
136
+ } })), C = {
93
137
  all: (t) => [...e.user(t), "multiSession"],
94
- lists: (e) => [...v.all(e), "list"],
95
- list: (e, t) => [...v.lists(e), t ?? null]
96
- }, y = {
138
+ lists: (e) => [...C.all(e), "list"],
139
+ list: (e, t) => [...C.lists(e), t ?? null]
140
+ }, w = {
97
141
  accept: "Accept",
98
142
  accepted: "Accepted",
99
143
  actions: "Actions",
@@ -150,7 +194,7 @@ var r = {
150
194
  status: "Status",
151
195
  uploadLogo: "Upload logo",
152
196
  userInvitationsEmptyDescription: "Invitations to join an organization will show up here."
153
- }, b = {
197
+ }, T = {
154
198
  all: ["auth", "organization"],
155
199
  create: [
156
200
  "auth",
@@ -212,9 +256,9 @@ var r = {
212
256
  "organization",
213
257
  "checkSlug"
214
258
  ]
215
- }, x = n("organization", (e = {}) => {
259
+ }, E = n("organization", (e = {}) => {
216
260
  let n = {
217
- ...y,
261
+ ...w,
218
262
  ...e.localization
219
263
  };
220
264
  return {
@@ -242,38 +286,38 @@ var r = {
242
286
  }
243
287
  }
244
288
  };
245
- }), S = {
289
+ }), D = {
246
290
  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],
291
+ lists: (e) => [...D.all(e), "list"],
292
+ list: (e, t) => [...D.lists(e), t ?? null],
293
+ fullDetails: (e) => [...D.all(e), "fullDetails"],
294
+ fullDetail: (e, t) => [...D.fullDetails(e), t ?? null],
295
+ activeOrganizations: (e) => [...D.all(e), "active"],
296
+ activeOrganization: (e, t) => [...D.activeOrganizations(e), t ?? null],
253
297
  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]
298
+ all: (e) => [...D.all(e), "members"],
299
+ lists: (e) => [...D.members.all(e), "list"],
300
+ list: (e, t) => [...D.members.lists(e), t ?? null]
257
301
  },
258
302
  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]
303
+ all: (e) => [...D.all(e), "invitations"],
304
+ lists: (e) => [...D.invitations.all(e), "list"],
305
+ list: (e, t) => [...D.invitations.lists(e), t ?? null]
262
306
  },
263
307
  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]
308
+ all: (e) => [...D.all(e), "userInvitations"],
309
+ lists: (e) => [...D.userInvitations.all(e), "list"],
310
+ list: (e, t) => [...D.userInvitations.lists(e), t ?? null]
267
311
  },
268
312
  permissions: {
269
- all: (e) => [...S.all(e), "permissions"],
313
+ all: (e) => [...D.all(e), "permissions"],
270
314
  has: (e, t) => [
271
- ...S.permissions.all(e),
315
+ ...D.permissions.all(e),
272
316
  "has",
273
317
  t ?? null
274
318
  ]
275
319
  }
276
- }, C = {
320
+ }, O = {
277
321
  passkey: "Passkey",
278
322
  addPasskey: "Add passkey",
279
323
  deletePasskey: "Delete passkey {{name}}",
@@ -283,7 +327,7 @@ var r = {
283
327
  passkeysDescription: "Create a passkey to securely access your account.",
284
328
  noPasskeys: "No passkeys",
285
329
  name: "Name"
286
- }, w = {
330
+ }, k = {
287
331
  signIn: [
288
332
  "auth",
289
333
  "signIn",
@@ -299,22 +343,22 @@ var r = {
299
343
  "passkey",
300
344
  "deletePasskey"
301
345
  ]
302
- }, T = n("passkey", (e = {}) => ({ localization: {
303
- ...C,
346
+ }, A = n("passkey", (e = {}) => ({ localization: {
347
+ ...O,
304
348
  ...e.localization
305
- } })), E = {
349
+ } })), j = {
306
350
  all: (t) => [...e.user(t), "passkey"],
307
- lists: (e) => [...E.all(e), "list"],
308
- list: (e, t) => [...E.lists(e), t ?? null]
309
- }, D = {
351
+ lists: (e) => [...j.all(e), "list"],
352
+ list: (e, t) => [...j.lists(e), t ?? null]
353
+ }, M = {
310
354
  appearance: "Appearance",
311
355
  theme: "Theme",
312
356
  system: "System",
313
357
  light: "Light",
314
358
  dark: "Dark"
315
- }, O = n("theme", (e) => ({
359
+ }, N = n("theme", (e) => ({
316
360
  localization: {
317
- ...D,
361
+ ...M,
318
362
  ...e.localization
319
363
  },
320
364
  theme: e.theme ?? "system",
@@ -324,7 +368,7 @@ var r = {
324
368
  "light",
325
369
  "dark"
326
370
  ]
327
- })), k = {
371
+ })), P = {
328
372
  username: "Username",
329
373
  usernamePlaceholder: "Username",
330
374
  usernameOrEmailPlaceholder: "Username or email",
@@ -332,16 +376,16 @@ var r = {
332
376
  usernameTaken: "Username is already taken. Please try another.",
333
377
  displayUsername: "Display Username",
334
378
  displayUsernamePlaceholder: "Display Username"
335
- }, A = {
379
+ }, F = {
336
380
  signIn: [
337
381
  "auth",
338
382
  "signIn",
339
383
  "username"
340
384
  ],
341
385
  isUsernameAvailable: ["auth", "isUsernameAvailable"]
342
- }, j = n("username", (e = {}) => {
386
+ }, I = n("username", (e = {}) => {
343
387
  let t = e.minUsernameLength ?? 3, n = e.maxUsernameLength ?? 30, r = {
344
- ...k,
388
+ ...P,
345
389
  ...e.localization
346
390
  };
347
391
  return {
@@ -369,4 +413,4 @@ var r = {
369
413
  };
370
414
  });
371
415
  //#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 };
416
+ export { r as apiKeyLocalization, i as apiKeyMutationKeys, a as apiKeyPlugin, o as apiKeyQueryKeys, s as deleteUserLocalization, c as deleteUserMutationKeys, l as deleteUserPlugin, u as deviceAuthorizationLocalization, d as deviceAuthorizationMutationKeys, m as deviceAuthorizationPlugin, h as lastLoginMethodLocalization, g as lastLoginMethodPlugin, _ as magicLinkLocalization, v as magicLinkMutationKeys, y as magicLinkPlugin, b as multiSessionLocalization, x as multiSessionMutationKeys, S as multiSessionPlugin, C as multiSessionQueryKeys, w as organizationLocalization, T as organizationMutationKeys, E as organizationPlugin, D as organizationQueryKeys, O as passkeyLocalization, k as passkeyMutationKeys, A as passkeyPlugin, j as passkeyQueryKeys, M as themeLocalization, N as themePlugin, P as usernameLocalization, F as usernameMutationKeys, I 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.31",
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,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
@@ -0,0 +1,65 @@
1
+ import { createAuthPlugin } from "../../lib/create-auth-plugin"
2
+ // Side-effect import so this file participates in declaration merging on the
3
+ // same module instance that external consumers reach via `@better-auth-ui/core`.
4
+ import type {} from "../../lib/view-paths"
5
+ import {
6
+ type DeviceAuthorizationLocalization,
7
+ deviceAuthorizationLocalization
8
+ } from "./device-authorization-localization"
9
+
10
+ declare module "../../lib/view-paths" {
11
+ /** Widens `AuthViewPaths` with the device-authorization path when this plugin is imported. */
12
+ interface AuthViewPaths {
13
+ /** @default "device" */
14
+ deviceAuthorization?: string
15
+ }
16
+ }
17
+
18
+ export type DeviceAuthorizationPluginOptions = {
19
+ /**
20
+ * Override the plugin's default localization strings.
21
+ * @remarks `DeviceAuthorizationLocalization`
22
+ */
23
+ localization?: Partial<DeviceAuthorizationLocalization>
24
+ /**
25
+ * URL segment for the device-authorization view.
26
+ * @remarks `string`
27
+ * @default "device"
28
+ */
29
+ path?: string
30
+ /**
31
+ * Number of characters rendered by the device-code input.
32
+ *
33
+ * Keep this in sync with Better Auth's `deviceAuthorization({ userCodeLength })`
34
+ * server option.
35
+ *
36
+ * @default 8
37
+ */
38
+ userCodeLength?: number
39
+ }
40
+
41
+ const DEFAULT_USER_CODE_LENGTH = 8
42
+
43
+ function resolveUserCodeLength(value?: number) {
44
+ if (value === undefined || !Number.isFinite(value)) {
45
+ return DEFAULT_USER_CODE_LENGTH
46
+ }
47
+
48
+ return Math.max(1, Math.floor(value))
49
+ }
50
+
51
+ export const deviceAuthorizationPlugin = createAuthPlugin(
52
+ "deviceAuthorization",
53
+ (options: DeviceAuthorizationPluginOptions = {}) => ({
54
+ localization: {
55
+ ...deviceAuthorizationLocalization,
56
+ ...options.localization
57
+ },
58
+ userCodeLength: resolveUserCodeLength(options.userCodeLength),
59
+ viewPaths: {
60
+ auth: {
61
+ deviceAuthorization: options.path ?? "device"
62
+ }
63
+ }
64
+ })
65
+ )
package/src/plugins.ts CHANGED
@@ -5,6 +5,9 @@ export * from "./plugins/api-key/api-key-query-keys"
5
5
  export * from "./plugins/delete-user/delete-user-localization"
6
6
  export * from "./plugins/delete-user/delete-user-mutation-keys"
7
7
  export * from "./plugins/delete-user/delete-user-plugin"
8
+ export * from "./plugins/device-authorization/device-authorization-localization"
9
+ export * from "./plugins/device-authorization/device-authorization-mutation-keys"
10
+ export * from "./plugins/device-authorization/device-authorization-plugin"
8
11
  export * from "./plugins/last-login-method/last-login-method-localization"
9
12
  export * from "./plugins/last-login-method/last-login-method-plugin"
10
13
  export * from "./plugins/magic-link/magic-link-localization"