@lalternative/auth 0.9.5 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Shared [Better Auth](https://better-auth.com) wrapper for L'Alternative apps.
4
4
 
5
- Provides platform auth defaults (email-OTP + admin plugins), a React client,
6
- and the auth UI forms (login, register, verify-email, forgot/reset password,
7
- auth layout).
5
+ Provides platform auth defaults (email-OTP + admin plugins, opt-in magic link),
6
+ a React client, and the auth UI forms (login, register, verify-email,
7
+ forgot/reset password, magic link, auth layout).
8
8
 
9
9
  ## Install
10
10
 
@@ -36,6 +36,68 @@ export const authClient = createPlatformAuthClient({ baseURL })
36
36
  import { LoginForm, RegisterForm, SocialButtons, VerifyEmailForm, ForgotPasswordForm, ResetPasswordForm, AuthLayout, useSession, useLogout } from "@lalternative/auth"
37
37
  ```
38
38
 
39
+ ### Magic link
40
+
41
+ Passwordless sign-in by emailed link. Off unless `magicLink` is passed — the
42
+ `/sign-in/magic-link` route is only mounted when it is, so an app that does not
43
+ render the form does not expose the endpoint either.
44
+
45
+ ```ts
46
+ export const auth = createPlatformAuth({
47
+ // …
48
+ magicLink: {
49
+ expiresIn: 300, // default
50
+ allowSignUp: false, // default
51
+ },
52
+ })
53
+ ```
54
+
55
+ `allowSignUp` is off on purpose. `createPlatformAuth` requires a verified email
56
+ and can be put behind an invite-only beta, and both of those gates gate
57
+ `/sign-up/email` — a magic link that creates the account walks past them. Turn
58
+ it on only where sign-up is open anyway.
59
+
60
+ The mailer receives the ready-made URL rather than an OTP:
61
+
62
+ ```ts
63
+ const mailer: PlatformAuthMailer = async ({ to, subject, html, type, url }) => {
64
+ // type === "magic-link", url is signed and points at the app's callback
65
+ }
66
+ ```
67
+
68
+ ```tsx
69
+ import { MagicLinkForm } from "@lalternative/auth"
70
+
71
+ <MagicLinkForm authClient={authClient} callbackUrl="/dashboard" />
72
+ ```
73
+
74
+ The form confirms that a link was sent, never that the account exists: Better
75
+ Auth answers the send identically either way, and only refuses at
76
+ `/magic-link/verify`. Distinguishing the two in the form would tell an
77
+ anonymous caller which addresses are registered.
78
+
79
+ That means **every** failure past the send comes back on the callback as
80
+ `?error=`, with no component mounted to have caught it — the same shape as an
81
+ OAuth round-trip, and read the same way:
82
+
83
+ ```ts
84
+ import { initialMagicLinkError, isMagicLinkError } from "@lalternative/auth"
85
+
86
+ const error = initialMagicLinkError() // undefined unless the code is a magic-link one
87
+ ```
88
+
89
+ That `?error=` lands on `errorCallbackUrl`, which defaults to the page the form
90
+ is on — the one place asking for another link is possible. Better Auth would
91
+ otherwise fall back to `callbackUrl`, typically a signed-in destination, where
92
+ an auth guard bounces the visitor and drops the error on the way, leaving an
93
+ expired link looking like nothing happened at all.
94
+
95
+ `initialMagicLinkError` ignores OAuth's codes, and `initialOAuthError` is
96
+ unchanged, so a screen offering both flows reads the one `?error=` against each
97
+ vocabulary without either claiming the other's failures. `INVALID_TOKEN` covers
98
+ expiry and reuse alike: the token is consumed atomically on first use, so a link
99
+ followed twice is indistinguishable from one that timed out.
100
+
39
101
  ### Invitations
40
102
 
41
103
  An invitation link lands on the app's own sign-up page
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createAuthClient } from 'better-auth/react';
2
- import { d as AuthClientSurface, P as PlatformAuthClientConfig } from './types-B7Ranu_a.js';
2
+ import { d as AuthClientSurface, j as MagicLinkClientSurface, P as PlatformAuthClientConfig } from './types-D6eIAL25.js';
3
3
  import 'better-auth';
4
4
 
5
5
  /**
@@ -9,11 +9,17 @@ import 'better-auth';
9
9
  * reaches into zod's internals), so the surface the auth screens call is
10
10
  * declared by hand in AuthClientSurface and intersected with the rest of the
11
11
  * client. Keep it in sync with the plugins enabled below.
12
+ *
13
+ * signIn carries both halves: the client always mounts magicLinkClient, since
14
+ * which methods exist client-side costs nothing — whether the route answers is
15
+ * decided server-side by passing `magicLink` to createPlatformAuth.
12
16
  */
13
- type PlatformAuthClient = AuthClientSurface & Omit<ReturnType<typeof createAuthClient>, keyof AuthClientSurface>;
17
+ type PlatformAuthClient = Omit<AuthClientSurface, "signIn"> & {
18
+ signIn: AuthClientSurface["signIn"] & MagicLinkClientSurface["signIn"];
19
+ } & Omit<ReturnType<typeof createAuthClient>, keyof AuthClientSurface | keyof MagicLinkClientSurface>;
14
20
  /**
15
21
  * Creates a Better Auth client for React usage.
16
- * Provides useSession() and the email-OTP / admin plugin methods.
22
+ * Provides useSession() and the email-OTP / magic-link / admin plugin methods.
17
23
  */
18
24
  declare function createPlatformAuthClient(config?: PlatformAuthClientConfig): PlatformAuthClient;
19
25
 
package/dist/client.js CHANGED
@@ -1,10 +1,19 @@
1
1
  // src/client.ts
2
2
  import { createAuthClient } from "better-auth/react";
3
- import { emailOTPClient, adminClient } from "better-auth/client/plugins";
3
+ import {
4
+ emailOTPClient,
5
+ adminClient,
6
+ magicLinkClient
7
+ } from "better-auth/client/plugins";
4
8
  function createPlatformAuthClient(config) {
5
9
  return createAuthClient({
6
10
  baseURL: config?.baseURL ?? (typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"),
7
- plugins: [emailOTPClient(), adminClient(), ...config?.plugins ?? []]
11
+ plugins: [
12
+ emailOTPClient(),
13
+ magicLinkClient(),
14
+ adminClient(),
15
+ ...config?.plugins ?? []
16
+ ]
8
17
  });
9
18
  }
10
19
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createAuthClient } from \"better-auth/react\"\nimport { emailOTPClient, adminClient } from \"better-auth/client/plugins\"\nimport type { AuthClientSurface, PlatformAuthClientConfig } from \"./types\"\n\n/**\n * A Better Auth React client carrying the platform plugins.\n *\n * The concrete inferred type cannot be named in a published .d.ts (TS2742 — it\n * reaches into zod's internals), so the surface the auth screens call is\n * declared by hand in AuthClientSurface and intersected with the rest of the\n * client. Keep it in sync with the plugins enabled below.\n */\nexport type PlatformAuthClient = AuthClientSurface &\n Omit<ReturnType<typeof createAuthClient>, keyof AuthClientSurface>\n\n/**\n * Creates a Better Auth client for React usage.\n * Provides useSession() and the email-OTP / admin plugin methods.\n */\nexport function createPlatformAuthClient(\n config?: PlatformAuthClientConfig,\n): PlatformAuthClient {\n return createAuthClient({\n baseURL:\n config?.baseURL ??\n (typeof window !== \"undefined\"\n ? window.location.origin\n : \"http://localhost:3000\"),\n plugins: [emailOTPClient(), adminClient(), ...(config?.plugins ?? [])],\n }) as unknown as PlatformAuthClient\n}\n"],"mappings":";AAAA,SAAS,wBAAwB;AACjC,SAAS,gBAAgB,mBAAmB;AAkBrC,SAAS,yBACd,QACoB;AACpB,SAAO,iBAAiB;AAAA,IACtB,SACE,QAAQ,YACP,OAAO,WAAW,cACf,OAAO,SAAS,SAChB;AAAA,IACN,SAAS,CAAC,eAAe,GAAG,YAAY,GAAG,GAAI,QAAQ,WAAW,CAAC,CAAE;AAAA,EACvE,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createAuthClient } from \"better-auth/react\"\nimport {\n emailOTPClient,\n adminClient,\n magicLinkClient,\n} from \"better-auth/client/plugins\"\nimport type {\n AuthClientSurface,\n MagicLinkClientSurface,\n PlatformAuthClientConfig,\n} from \"./types\"\n\n/**\n * A Better Auth React client carrying the platform plugins.\n *\n * The concrete inferred type cannot be named in a published .d.ts (TS2742 — it\n * reaches into zod's internals), so the surface the auth screens call is\n * declared by hand in AuthClientSurface and intersected with the rest of the\n * client. Keep it in sync with the plugins enabled below.\n *\n * signIn carries both halves: the client always mounts magicLinkClient, since\n * which methods exist client-side costs nothing — whether the route answers is\n * decided server-side by passing `magicLink` to createPlatformAuth.\n */\nexport type PlatformAuthClient = Omit<AuthClientSurface, \"signIn\"> & {\n signIn: AuthClientSurface[\"signIn\"] & MagicLinkClientSurface[\"signIn\"]\n} & Omit<\n ReturnType<typeof createAuthClient>,\n keyof AuthClientSurface | keyof MagicLinkClientSurface\n >\n\n/**\n * Creates a Better Auth client for React usage.\n * Provides useSession() and the email-OTP / magic-link / admin plugin methods.\n */\nexport function createPlatformAuthClient(\n config?: PlatformAuthClientConfig,\n): PlatformAuthClient {\n return createAuthClient({\n baseURL:\n config?.baseURL ??\n (typeof window !== \"undefined\"\n ? window.location.origin\n : \"http://localhost:3000\"),\n plugins: [\n emailOTPClient(),\n magicLinkClient(),\n adminClient(),\n ...(config?.plugins ?? []),\n ],\n }) as unknown as PlatformAuthClient\n}\n"],"mappings":";AAAA,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA8BA,SAAS,yBACd,QACoB;AACpB,SAAO,iBAAiB;AAAA,IACtB,SACE,QAAQ,YACP,OAAO,WAAW,cACf,OAAO,SAAS,SAChB;AAAA,IACN,SAAS;AAAA,MACP,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,GAAI,QAAQ,WAAW,CAAC;AAAA,IAC1B;AAAA,EACF,CAAC;AACH;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { L as LoginFormProps, R as RegisterFormProps, V as VerifyEmailFormProps, F as ForgotPasswordFormProps, a as ResetPasswordFormProps, A as AuthLayoutProps, I as InvitationNoticeProps, b as AuthClientResult, c as LinkComponent } from './types-B7Ranu_a.js';
2
- export { d as AuthClientSurface, e as AuthInviteProps, f as AuthNavProps, g as AuthThemeProps, h as InvitationFailure, i as LoginFormLabels, P as PlatformAuthClientConfig, j as PlatformAuthConfig, k as PlatformAuthMailer, l as PlatformAuthMailerArgs, m as PlatformAuthMailerType, n as PlatformSession, o as PlatformSessionData, p as PlatformUser, q as RegisterFormLabels } from './types-B7Ranu_a.js';
1
+ import { L as LoginFormProps, R as RegisterFormProps, V as VerifyEmailFormProps, F as ForgotPasswordFormProps, M as MagicLinkFormProps, a as ResetPasswordFormProps, A as AuthLayoutProps, I as InvitationNoticeProps, b as AuthClientResult, c as LinkComponent } from './types-D6eIAL25.js';
2
+ export { d as AuthClientSurface, e as AuthInviteProps, f as AuthNavProps, g as AuthThemeProps, h as InvitationFailure, i as LoginFormLabels, j as MagicLinkClientSurface, k as MagicLinkConfig, l as MagicLinkFormLabels, P as PlatformAuthClientConfig, m as PlatformAuthConfig, n as PlatformAuthMailer, o as PlatformAuthMailerArgs, p as PlatformAuthMailerType, q as PlatformSession, r as PlatformSessionData, s as PlatformUser, t as RegisterFormLabels } from './types-D6eIAL25.js';
3
3
  import * as better_auth_react from 'better-auth/react';
4
4
  import * as better_auth from 'better-auth';
5
5
  import { PlatformAuthClient } from './client.js';
6
6
  import * as react from 'react';
7
7
  import { InputHTMLAttributes, ReactNode } from 'react';
8
- export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-DuoHHBNA.js';
8
+ export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-p9JYNUiB.js';
9
9
 
10
10
  /**
11
11
  * Returns a useSession hook bound to the given auth client.
@@ -144,6 +144,14 @@ declare namespace ForgotPasswordForm {
144
144
  };
145
145
  }
146
146
 
147
+ declare function MagicLinkForm({ onSuccess, loginUrl, callbackUrl, newUserCallbackUrl, errorCallbackUrl, labels, submitClassName, fieldClassName, error: externalError, linkComponent, invite, authClient, }: MagicLinkFormProps): react.JSX.Element;
148
+ declare namespace MagicLinkForm {
149
+ var defaults: {
150
+ title: string;
151
+ subtitle: string;
152
+ };
153
+ }
154
+
147
155
  declare function ResetPasswordForm({ email, onSuccess, loginUrl, labels, submitClassName, fieldClassName, linkComponent, authClient, }: ResetPasswordFormProps): react.JSX.Element;
148
156
  declare namespace ResetPasswordForm {
149
157
  var defaults: {
@@ -249,6 +257,50 @@ declare function initialOAuthError(labels: OAuthErrorLabels, param?: string): st
249
257
  */
250
258
  declare function clearOAuthError(param?: string): void;
251
259
 
260
+ /**
261
+ * Where a link that did not work should send the browser back to.
262
+ *
263
+ * Falls back to the current page rather than to Better Auth's own default,
264
+ * which is the success callback: that is a signed-in destination, so an auth
265
+ * guard bounces the visitor and drops the `?error=` on the way, and an expired
266
+ * link ends up looking like nothing happened. `fallback` covers the server
267
+ * render, where there is no current page to name.
268
+ */
269
+ declare function magicLinkErrorCallback(explicit: string | undefined, fallback: string, currentPath: string | undefined): string;
270
+ type MagicLinkErrorLabels = {
271
+ invalidToken: string;
272
+ signUpDisabled: string;
273
+ failed: string;
274
+ };
275
+ /**
276
+ * Turns the failure a followed magic link redirected back with into something
277
+ * the person can act on.
278
+ *
279
+ * Every way the flow can fail lands here rather than on the send: Better Auth
280
+ * consumes the token at /magic-link/verify, and any refusal there is thrown as
281
+ * a redirect to the errorCallbackURL. `INVALID_TOKEN` covers expiry and reuse
282
+ * alike — the token is consumed atomically on first use, so a link followed
283
+ * twice is indistinguishable from one that timed out, and the copy names both.
284
+ */
285
+ declare function magicLinkErrorMessage(code: string | null | undefined, labels?: Partial<MagicLinkErrorLabels>): string;
286
+ /**
287
+ * Whether a `?error=` came from a magic link rather than from OAuth.
288
+ *
289
+ * Both flows return to the same screens through the same parameter, so a page
290
+ * offering the two needs to know which vocabulary to read the code against —
291
+ * otherwise an expired link is reported as a failed social sign-in.
292
+ */
293
+ declare function isMagicLinkError(code: string | null | undefined): boolean;
294
+ /**
295
+ * Reads the failure a followed link redirected back with.
296
+ *
297
+ * Following a link leaves the app entirely, so no component state survives it;
298
+ * the address bar is the only carrier left. Mirrors initialOAuthError, down to
299
+ * reading `window.location` rather than a typed route search — the auth routes
300
+ * declare none on purpose.
301
+ */
302
+ declare function initialMagicLinkError(labels?: Partial<MagicLinkErrorLabels>, param?: string): string | undefined;
303
+
252
304
  interface AuthLinkProps {
253
305
  to: string;
254
306
  as?: LinkComponent;
@@ -264,4 +316,4 @@ interface AuthLinkProps {
264
316
  */
265
317
  declare function AuthLink({ to, as: Link, className, children }: AuthLinkProps): react.JSX.Element;
266
318
 
267
- export { AuthClientResult, AuthField, type AuthFieldProps, AuthLayout, AuthLayoutProps, AuthLink, AuthSubmit, ForgotPasswordForm, ForgotPasswordFormProps, InvitationNotice, InvitationNoticeProps, LinkComponent, LoginForm, LoginFormProps, type OAuthErrorLabels, RegisterForm, RegisterFormProps, ResetPasswordForm, ResetPasswordFormProps, SocialButtons, VerifyEmailForm, VerifyEmailFormProps, clearOAuthError, initialOAuthError, isEmailNotVerified, normalizeInviteToken, oauthErrorMessage, useLogout, useSession, withInviteToken };
319
+ export { AuthClientResult, AuthField, type AuthFieldProps, AuthLayout, AuthLayoutProps, AuthLink, AuthSubmit, ForgotPasswordForm, ForgotPasswordFormProps, InvitationNotice, InvitationNoticeProps, LinkComponent, LoginForm, LoginFormProps, type MagicLinkErrorLabels, MagicLinkForm, MagicLinkFormProps, type OAuthErrorLabels, RegisterForm, RegisterFormProps, ResetPasswordForm, ResetPasswordFormProps, SocialButtons, VerifyEmailForm, VerifyEmailFormProps, clearOAuthError, initialMagicLinkError, initialOAuthError, isEmailNotVerified, isMagicLinkError, magicLinkErrorCallback, magicLinkErrorMessage, normalizeInviteToken, oauthErrorMessage, useLogout, useSession, withInviteToken };
package/dist/index.js CHANGED
@@ -967,11 +967,180 @@ ForgotPasswordForm.defaults = {
967
967
  subtitle: DEFAULTS4.subtitle
968
968
  };
969
969
 
970
- // src/components/reset-password-form.tsx
970
+ // src/components/magic-link-form.tsx
971
971
  import { useState as useState6 } from "react";
972
+
973
+ // src/magic-link-error.ts
974
+ function magicLinkErrorCallback(explicit, fallback, currentPath) {
975
+ return explicit ?? currentPath ?? fallback;
976
+ }
977
+ var MAGIC_LINK_ERROR_DEFAULTS = {
978
+ invalidToken: "Ce lien n'est plus valide. Il expire apr\xE8s 5 minutes et ne fonctionne qu'une fois \u2014 demandes-en un nouveau.",
979
+ signUpDisabled: "Aucun compte n'existe pour cette adresse. Cr\xE9e-en un d'abord.",
980
+ failed: "La connexion par lien a \xE9chou\xE9. R\xE9essaie."
981
+ };
982
+ function magicLinkErrorMessage(code, labels = {}) {
983
+ const t = { ...MAGIC_LINK_ERROR_DEFAULTS, ...labels };
984
+ switch (code) {
985
+ case "INVALID_TOKEN":
986
+ return t.invalidToken;
987
+ case "new_user_signup_disabled":
988
+ return t.signUpDisabled;
989
+ default:
990
+ return t.failed;
991
+ }
992
+ }
993
+ var MAGIC_LINK_ERROR_CODES = /* @__PURE__ */ new Set([
994
+ "INVALID_TOKEN",
995
+ "new_user_signup_disabled",
996
+ "failed_to_create_user",
997
+ "failed_to_create_session"
998
+ ]);
999
+ function isMagicLinkError(code) {
1000
+ return !!code && MAGIC_LINK_ERROR_CODES.has(code);
1001
+ }
1002
+ function initialMagicLinkError(labels = {}, param = "error") {
1003
+ if (typeof window === "undefined") return void 0;
1004
+ const code = new URLSearchParams(window.location.search).get(param);
1005
+ if (!isMagicLinkError(code)) return void 0;
1006
+ return magicLinkErrorMessage(code, labels);
1007
+ }
1008
+
1009
+ // src/components/magic-link-form.tsx
972
1010
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
973
- var MIN_PASSWORD_LENGTH2 = 8;
974
1011
  var DEFAULTS5 = {
1012
+ title: "Connexion par lien",
1013
+ subtitle: "Entre ton adresse e-mail et nous t'enverrons un lien pour te connecter, sans mot de passe.",
1014
+ emailPlaceholder: "Adresse e-mail",
1015
+ submit: "Envoyer le lien",
1016
+ submitPending: "Envoi\u2026",
1017
+ sent: "Lien envoy\xE9. Ouvre ta bo\xEEte de r\xE9ception pour te connecter \u2014 il expire dans 5 minutes.",
1018
+ resend: "Renvoyer le lien",
1019
+ usePassword: "Tu pr\xE9f\xE8res ton mot de passe ?",
1020
+ login: "Se connecter",
1021
+ emailRequired: "Renseigne ton adresse e-mail",
1022
+ sendFailed: "L'envoi du lien a \xE9chou\xE9"
1023
+ };
1024
+ function MagicLinkForm({
1025
+ onSuccess,
1026
+ loginUrl = "/login",
1027
+ callbackUrl = "/",
1028
+ newUserCallbackUrl,
1029
+ errorCallbackUrl,
1030
+ labels,
1031
+ submitClassName,
1032
+ fieldClassName,
1033
+ error: externalError,
1034
+ linkComponent,
1035
+ invite,
1036
+ authClient
1037
+ }) {
1038
+ const t = { ...DEFAULTS5, ...labels };
1039
+ const [email, setEmail] = useState6("");
1040
+ const [ownError, setOwnError] = useState6();
1041
+ const [isPending, setIsPending] = useState6(false);
1042
+ const [isSent, setIsSent] = useState6(false);
1043
+ const error = ownError ?? externalError;
1044
+ const handleSubmit = async (e) => {
1045
+ e.preventDefault();
1046
+ if (!email.trim()) {
1047
+ setOwnError(t.emailRequired);
1048
+ return;
1049
+ }
1050
+ setOwnError(void 0);
1051
+ setIsPending(true);
1052
+ try {
1053
+ const res = await authClient.signIn.magicLink({
1054
+ email: email.trim(),
1055
+ // The invitation rides the callback: following the link leaves the
1056
+ // browser on the mail client, so the auth handler redeems the token on
1057
+ // the way back, as it does for OAuth.
1058
+ callbackURL: withInviteToken(callbackUrl, invite),
1059
+ ...newUserCallbackUrl ? { newUserCallbackURL: withInviteToken(newUserCallbackUrl, invite) } : {},
1060
+ // Resolved here rather than at render: the default is the current page,
1061
+ // and this runs in the browser, where there is one.
1062
+ errorCallbackURL: withInviteToken(
1063
+ magicLinkErrorCallback(
1064
+ errorCallbackUrl,
1065
+ loginUrl,
1066
+ typeof window !== "undefined" ? window.location.pathname : void 0
1067
+ ),
1068
+ invite
1069
+ )
1070
+ });
1071
+ if (res?.error) {
1072
+ setOwnError(res.error.message ?? t.sendFailed);
1073
+ return;
1074
+ }
1075
+ setIsSent(true);
1076
+ onSuccess?.(email.trim());
1077
+ } catch (err) {
1078
+ setOwnError(err instanceof Error ? err.message : t.sendFailed);
1079
+ } finally {
1080
+ setIsPending(false);
1081
+ }
1082
+ };
1083
+ return /* @__PURE__ */ jsxs9("div", { className: "space-y-7", children: [
1084
+ /* @__PURE__ */ jsx11(AuthAlert, { children: error }),
1085
+ /* @__PURE__ */ jsx11(AuthAlert, { tone: "success", children: !error && isSent ? t.sent : void 0 }),
1086
+ /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, className: "space-y-[1.125rem]", noValidate: true, children: [
1087
+ /* @__PURE__ */ jsx11(
1088
+ AuthField,
1089
+ {
1090
+ label: t.emailPlaceholder,
1091
+ type: "email",
1092
+ inputMode: "email",
1093
+ value: email,
1094
+ onChange: (e) => {
1095
+ setEmail(e.target.value);
1096
+ setIsSent(false);
1097
+ },
1098
+ required: true,
1099
+ disabled: isPending,
1100
+ autoComplete: "email",
1101
+ autoCapitalize: "none",
1102
+ spellCheck: false,
1103
+ invalid: !!error,
1104
+ fieldClassName
1105
+ }
1106
+ ),
1107
+ /* @__PURE__ */ jsx11(
1108
+ AuthSubmit,
1109
+ {
1110
+ spacedAbove: true,
1111
+ pending: isPending,
1112
+ disabled: !email.trim(),
1113
+ pendingLabel: t.submitPending,
1114
+ className: submitClassName,
1115
+ children: isSent ? t.resend : t.submit
1116
+ }
1117
+ )
1118
+ ] }),
1119
+ /* @__PURE__ */ jsxs9("p", { className: "text-center text-sm text-muted-foreground", children: [
1120
+ t.usePassword,
1121
+ " ",
1122
+ /* @__PURE__ */ jsx11(
1123
+ AuthLink,
1124
+ {
1125
+ to: withInviteToken(loginUrl, invite),
1126
+ as: linkComponent,
1127
+ className: AUTH_LINK_CLASS,
1128
+ children: t.login
1129
+ }
1130
+ )
1131
+ ] })
1132
+ ] });
1133
+ }
1134
+ MagicLinkForm.defaults = {
1135
+ title: DEFAULTS5.title,
1136
+ subtitle: DEFAULTS5.subtitle
1137
+ };
1138
+
1139
+ // src/components/reset-password-form.tsx
1140
+ import { useState as useState7 } from "react";
1141
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1142
+ var MIN_PASSWORD_LENGTH2 = 8;
1143
+ var DEFAULTS6 = {
975
1144
  title: "Nouveau mot de passe",
976
1145
  subtitle: "Entre le code \xE0 6 chiffres re\xE7u par e-mail et ton nouveau mot de passe.",
977
1146
  codePlaceholder: "Code de v\xE9rification",
@@ -1001,14 +1170,14 @@ function ResetPasswordForm({
1001
1170
  linkComponent,
1002
1171
  authClient
1003
1172
  }) {
1004
- const t = { ...DEFAULTS5, ...labels };
1005
- const [otp, setOtp] = useState6("");
1006
- const [password, setPassword] = useState6("");
1007
- const [confirmPassword, setConfirmPassword] = useState6("");
1008
- const [error, setError] = useState6();
1009
- const [isResetting, setIsResetting] = useState6(false);
1010
- const [isResending, setIsResending] = useState6(false);
1011
- const [resendMessage, setResendMessage] = useState6();
1173
+ const t = { ...DEFAULTS6, ...labels };
1174
+ const [otp, setOtp] = useState7("");
1175
+ const [password, setPassword] = useState7("");
1176
+ const [confirmPassword, setConfirmPassword] = useState7("");
1177
+ const [error, setError] = useState7();
1178
+ const [isResetting, setIsResetting] = useState7(false);
1179
+ const [isResending, setIsResending] = useState7(false);
1180
+ const [resendMessage, setResendMessage] = useState7();
1012
1181
  const tooShort = password.length > 0 && password.length < MIN_PASSWORD_LENGTH2;
1013
1182
  const mismatch = confirmPassword.length > 0 && confirmPassword !== password;
1014
1183
  const handleSubmit = async (e) => {
@@ -1062,11 +1231,11 @@ function ResetPasswordForm({
1062
1231
  setIsResending(false);
1063
1232
  }
1064
1233
  };
1065
- return /* @__PURE__ */ jsxs9("div", { className: "space-y-7", children: [
1066
- /* @__PURE__ */ jsx11(AuthAlert, { children: error }),
1067
- /* @__PURE__ */ jsx11(AuthAlert, { tone: "success", children: resendMessage }),
1068
- /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, className: "space-y-[1.125rem]", noValidate: true, children: [
1069
- /* @__PURE__ */ jsx11(
1234
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-7", children: [
1235
+ /* @__PURE__ */ jsx12(AuthAlert, { children: error }),
1236
+ /* @__PURE__ */ jsx12(AuthAlert, { tone: "success", children: resendMessage }),
1237
+ /* @__PURE__ */ jsxs10("form", { onSubmit: handleSubmit, className: "space-y-[1.125rem]", noValidate: true, children: [
1238
+ /* @__PURE__ */ jsx12(
1070
1239
  AuthOtpField,
1071
1240
  {
1072
1241
  id: "reset-password-otp",
@@ -1077,7 +1246,7 @@ function ResetPasswordForm({
1077
1246
  fieldClassName
1078
1247
  }
1079
1248
  ),
1080
- /* @__PURE__ */ jsx11(
1249
+ /* @__PURE__ */ jsx12(
1081
1250
  AuthField,
1082
1251
  {
1083
1252
  label: t.passwordPlaceholder,
@@ -1091,7 +1260,7 @@ function ResetPasswordForm({
1091
1260
  invalid: tooShort
1092
1261
  }
1093
1262
  ),
1094
- /* @__PURE__ */ jsx11(
1263
+ /* @__PURE__ */ jsx12(
1095
1264
  AuthField,
1096
1265
  {
1097
1266
  label: t.confirmPlaceholder,
@@ -1104,7 +1273,7 @@ function ResetPasswordForm({
1104
1273
  invalid: mismatch
1105
1274
  }
1106
1275
  ),
1107
- /* @__PURE__ */ jsx11(
1276
+ /* @__PURE__ */ jsx12(
1108
1277
  AuthSubmit,
1109
1278
  {
1110
1279
  spacedAbove: true,
@@ -1116,8 +1285,8 @@ function ResetPasswordForm({
1116
1285
  }
1117
1286
  )
1118
1287
  ] }),
1119
- /* @__PURE__ */ jsxs9("div", { className: "space-y-4 text-center text-sm text-muted-foreground", children: [
1120
- /* @__PURE__ */ jsx11(
1288
+ /* @__PURE__ */ jsxs10("div", { className: "space-y-4 text-center text-sm text-muted-foreground", children: [
1289
+ /* @__PURE__ */ jsx12(
1121
1290
  "button",
1122
1291
  {
1123
1292
  type: "button",
@@ -1127,10 +1296,10 @@ function ResetPasswordForm({
1127
1296
  children: isResending ? t.resendPending : t.resend
1128
1297
  }
1129
1298
  ),
1130
- /* @__PURE__ */ jsxs9("p", { children: [
1299
+ /* @__PURE__ */ jsxs10("p", { children: [
1131
1300
  t.rememberPassword,
1132
1301
  " ",
1133
- /* @__PURE__ */ jsx11(
1302
+ /* @__PURE__ */ jsx12(
1134
1303
  AuthLink,
1135
1304
  {
1136
1305
  to: loginUrl,
@@ -1144,25 +1313,25 @@ function ResetPasswordForm({
1144
1313
  ] });
1145
1314
  }
1146
1315
  ResetPasswordForm.defaults = {
1147
- title: DEFAULTS5.title,
1148
- subtitle: DEFAULTS5.subtitle
1316
+ title: DEFAULTS6.title,
1317
+ subtitle: DEFAULTS6.subtitle
1149
1318
  };
1150
1319
 
1151
1320
  // src/components/auth-heading.tsx
1152
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1321
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1153
1322
  function AuthHeading({
1154
1323
  title,
1155
1324
  subtitle,
1156
1325
  titleClassName = "text-[2rem] font-semibold leading-[1.1] tracking-[-0.03em] sm:text-[2.25rem]"
1157
1326
  }) {
1158
- return /* @__PURE__ */ jsxs10("header", { className: "space-y-2.5 text-center", children: [
1159
- /* @__PURE__ */ jsx12("h1", { className: titleClassName, children: title }),
1160
- subtitle && /* @__PURE__ */ jsx12("p", { className: "text-balance text-[0.9375rem] leading-relaxed text-muted-foreground", children: subtitle })
1327
+ return /* @__PURE__ */ jsxs11("header", { className: "space-y-2.5 text-center", children: [
1328
+ /* @__PURE__ */ jsx13("h1", { className: titleClassName, children: title }),
1329
+ subtitle && /* @__PURE__ */ jsx13("p", { className: "text-balance text-[0.9375rem] leading-relaxed text-muted-foreground", children: subtitle })
1161
1330
  ] });
1162
1331
  }
1163
1332
 
1164
1333
  // src/components/auth-layout.tsx
1165
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1334
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1166
1335
  function AuthLayout({
1167
1336
  logo,
1168
1337
  panel,
@@ -1172,7 +1341,7 @@ function AuthLayout({
1172
1341
  children,
1173
1342
  footer
1174
1343
  }) {
1175
- return /* @__PURE__ */ jsxs11(
1344
+ return /* @__PURE__ */ jsxs12(
1176
1345
  "div",
1177
1346
  {
1178
1347
  className: [
@@ -1186,7 +1355,7 @@ function AuthLayout({
1186
1355
  // screen, so the colour only has to sit behind the heading. Covering
1187
1356
  // the whole height would leave the panel's own copy showing under the
1188
1357
  // card, which reads as a second, half-hidden screen.
1189
- /* @__PURE__ */ jsx13(
1358
+ /* @__PURE__ */ jsx14(
1190
1359
  "div",
1191
1360
  {
1192
1361
  "aria-hidden": "true",
@@ -1194,12 +1363,12 @@ function AuthLayout({
1194
1363
  children: panel
1195
1364
  }
1196
1365
  ),
1197
- /* @__PURE__ */ jsxs11("div", { className: `mx-auto w-full ${panel ? "max-w-4xl" : "max-w-[440px]"}`, children: [
1366
+ /* @__PURE__ */ jsxs12("div", { className: `mx-auto w-full ${panel ? "max-w-4xl" : "max-w-[440px]"}`, children: [
1198
1367
  (logo || title) && // The mark sits tight above the title so the two read as one block
1199
1368
  // rather than as a stray label; the gap down to the card is the
1200
1369
  // largest on the screen — that step is what separates "who this is"
1201
1370
  // from "what you do here".
1202
- /* @__PURE__ */ jsxs11(
1371
+ /* @__PURE__ */ jsxs12(
1203
1372
  "div",
1204
1373
  {
1205
1374
  className: [
@@ -1210,8 +1379,8 @@ function AuthLayout({
1210
1379
  panel ? "text-white [&_p]:text-white/75 md:text-foreground md:[&_p]:text-muted-foreground" : ""
1211
1380
  ].filter(Boolean).join(" "),
1212
1381
  children: [
1213
- logo && /* @__PURE__ */ jsx13("div", { className: "flex justify-center", children: logo }),
1214
- title && /* @__PURE__ */ jsx13(
1382
+ logo && /* @__PURE__ */ jsx14("div", { className: "flex justify-center", children: logo }),
1383
+ title && /* @__PURE__ */ jsx14(
1215
1384
  AuthHeading,
1216
1385
  {
1217
1386
  title,
@@ -1222,7 +1391,7 @@ function AuthLayout({
1222
1391
  ]
1223
1392
  }
1224
1393
  ),
1225
- /* @__PURE__ */ jsxs11(
1394
+ /* @__PURE__ */ jsxs12(
1226
1395
  "div",
1227
1396
  {
1228
1397
  className: [
@@ -1236,7 +1405,7 @@ function AuthLayout({
1236
1405
  children: [
1237
1406
  panel && // Decorative: it must never carry information the form does not,
1238
1407
  // so it is hidden from assistive tech rather than described.
1239
- /* @__PURE__ */ jsx13(
1408
+ /* @__PURE__ */ jsx14(
1240
1409
  "div",
1241
1410
  {
1242
1411
  "aria-hidden": "true",
@@ -1244,11 +1413,11 @@ function AuthLayout({
1244
1413
  children: panel
1245
1414
  }
1246
1415
  ),
1247
- /* @__PURE__ */ jsx13("div", { className: panel ? "px-5 pb-12 pt-7 sm:p-7" : "sm:p-7", children })
1416
+ /* @__PURE__ */ jsx14("div", { className: panel ? "px-5 pb-12 pt-7 sm:p-7" : "sm:p-7", children })
1248
1417
  ]
1249
1418
  }
1250
1419
  ),
1251
- footer && /* @__PURE__ */ jsx13("div", { className: "mt-8 text-center text-xs leading-relaxed text-muted-foreground", children: footer })
1420
+ footer && /* @__PURE__ */ jsx14("div", { className: "mt-8 text-center text-xs leading-relaxed text-muted-foreground", children: footer })
1252
1421
  ] })
1253
1422
  ]
1254
1423
  }
@@ -1256,7 +1425,7 @@ function AuthLayout({
1256
1425
  }
1257
1426
 
1258
1427
  // src/components/invitation-notice.tsx
1259
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1428
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1260
1429
  function defaultSupportEmail() {
1261
1430
  if (typeof window === "undefined") return void 0;
1262
1431
  const host = window.location.hostname;
@@ -1276,15 +1445,15 @@ function InvitationNotice({
1276
1445
  action
1277
1446
  }) {
1278
1447
  const contact = supportEmail ?? defaultSupportEmail();
1279
- return /* @__PURE__ */ jsxs12("div", { className: "space-y-8", children: [
1280
- /* @__PURE__ */ jsxs12("div", { children: [
1281
- /* @__PURE__ */ jsx14("h1", { className: "text-2xl font-bold tracking-tight", children: title }),
1282
- /* @__PURE__ */ jsx14("p", { className: "mt-1 text-sm text-muted-foreground", children: REASON_MESSAGE[reason] ?? REASON_MESSAGE.unknown })
1448
+ return /* @__PURE__ */ jsxs13("div", { className: "space-y-8", children: [
1449
+ /* @__PURE__ */ jsxs13("div", { children: [
1450
+ /* @__PURE__ */ jsx15("h1", { className: "text-2xl font-bold tracking-tight", children: title }),
1451
+ /* @__PURE__ */ jsx15("p", { className: "mt-1 text-sm text-muted-foreground", children: REASON_MESSAGE[reason] ?? REASON_MESSAGE.unknown })
1283
1452
  ] }),
1284
- contact ? /* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
1453
+ contact ? /* @__PURE__ */ jsxs13("p", { className: "text-sm text-muted-foreground", children: [
1285
1454
  "\xC9crivez-nous \xE0",
1286
1455
  " ",
1287
- /* @__PURE__ */ jsx14(
1456
+ /* @__PURE__ */ jsx15(
1288
1457
  "a",
1289
1458
  {
1290
1459
  href: `mailto:${contact}`,
@@ -1331,14 +1500,19 @@ export {
1331
1500
  ForgotPasswordForm,
1332
1501
  InvitationNotice,
1333
1502
  LoginForm,
1503
+ MagicLinkForm,
1334
1504
  RegisterForm,
1335
1505
  ResetPasswordForm,
1336
1506
  SocialButtons,
1337
1507
  VerifyEmailForm,
1338
1508
  clearOAuthError,
1509
+ initialMagicLinkError,
1339
1510
  initialOAuthError,
1340
1511
  isEmailNotVerified,
1341
1512
  isInvitationFailure,
1513
+ isMagicLinkError,
1514
+ magicLinkErrorCallback,
1515
+ magicLinkErrorMessage,
1342
1516
  normalizeInviteToken,
1343
1517
  oauthErrorMessage,
1344
1518
  useLogout,