@lalternative/auth 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,62 @@ 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
+ `initialMagicLinkError` ignores OAuth's codes, and `initialOAuthError` is
90
+ unchanged, so a screen offering both flows reads the one `?error=` against each
91
+ vocabulary without either claiming the other's failures. `INVALID_TOKEN` covers
92
+ expiry and reuse alike: the token is consumed atomically on first use, so a link
93
+ followed twice is indistinguishable from one that timed out.
94
+
39
95
  ### Invitations
40
96
 
41
97
  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-q-GAhurn.js';
2
+ import { d as AuthClientSurface, j as MagicLinkClientSurface, P as PlatformAuthClientConfig } from './types-BbTfkT5F.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-q-GAhurn.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-q-GAhurn.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-BbTfkT5F.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-BbTfkT5F.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-Y9l1o_pm.js';
8
+ export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-DcALM_sl.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, 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,40 @@ declare function initialOAuthError(labels: OAuthErrorLabels, param?: string): st
249
257
  */
250
258
  declare function clearOAuthError(param?: string): void;
251
259
 
260
+ type MagicLinkErrorLabels = {
261
+ invalidToken: string;
262
+ signUpDisabled: string;
263
+ failed: string;
264
+ };
265
+ /**
266
+ * Turns the failure a followed magic link redirected back with into something
267
+ * the person can act on.
268
+ *
269
+ * Every way the flow can fail lands here rather than on the send: Better Auth
270
+ * consumes the token at /magic-link/verify, and any refusal there is thrown as
271
+ * a redirect to the errorCallbackURL. `INVALID_TOKEN` covers expiry and reuse
272
+ * alike — the token is consumed atomically on first use, so a link followed
273
+ * twice is indistinguishable from one that timed out, and the copy names both.
274
+ */
275
+ declare function magicLinkErrorMessage(code: string | null | undefined, labels?: Partial<MagicLinkErrorLabels>): string;
276
+ /**
277
+ * Whether a `?error=` came from a magic link rather than from OAuth.
278
+ *
279
+ * Both flows return to the same screens through the same parameter, so a page
280
+ * offering the two needs to know which vocabulary to read the code against —
281
+ * otherwise an expired link is reported as a failed social sign-in.
282
+ */
283
+ declare function isMagicLinkError(code: string | null | undefined): boolean;
284
+ /**
285
+ * Reads the failure a followed link redirected back with.
286
+ *
287
+ * Following a link leaves the app entirely, so no component state survives it;
288
+ * the address bar is the only carrier left. Mirrors initialOAuthError, down to
289
+ * reading `window.location` rather than a typed route search — the auth routes
290
+ * declare none on purpose.
291
+ */
292
+ declare function initialMagicLinkError(labels?: Partial<MagicLinkErrorLabels>, param?: string): string | undefined;
293
+
252
294
  interface AuthLinkProps {
253
295
  to: string;
254
296
  as?: LinkComponent;
@@ -264,4 +306,4 @@ interface AuthLinkProps {
264
306
  */
265
307
  declare function AuthLink({ to, as: Link, className, children }: AuthLinkProps): react.JSX.Element;
266
308
 
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 };
309
+ 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, magicLinkErrorMessage, normalizeInviteToken, oauthErrorMessage, useLogout, useSession, withInviteToken };
package/dist/index.js CHANGED
@@ -967,11 +967,131 @@ 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
972
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
973
- var MIN_PASSWORD_LENGTH2 = 8;
974
973
  var DEFAULTS5 = {
974
+ title: "Connexion par lien",
975
+ subtitle: "Entre ton adresse e-mail et nous t'enverrons un lien pour te connecter, sans mot de passe.",
976
+ emailPlaceholder: "Adresse e-mail",
977
+ submit: "Envoyer le lien",
978
+ submitPending: "Envoi\u2026",
979
+ sent: "Lien envoy\xE9. Ouvre ta bo\xEEte de r\xE9ception pour te connecter \u2014 il expire dans 5 minutes.",
980
+ resend: "Renvoyer le lien",
981
+ usePassword: "Tu pr\xE9f\xE8res ton mot de passe ?",
982
+ login: "Se connecter",
983
+ emailRequired: "Renseigne ton adresse e-mail",
984
+ sendFailed: "L'envoi du lien a \xE9chou\xE9"
985
+ };
986
+ function MagicLinkForm({
987
+ onSuccess,
988
+ loginUrl = "/login",
989
+ callbackUrl = "/",
990
+ newUserCallbackUrl,
991
+ labels,
992
+ submitClassName,
993
+ fieldClassName,
994
+ error: externalError,
995
+ linkComponent,
996
+ invite,
997
+ authClient
998
+ }) {
999
+ const t = { ...DEFAULTS5, ...labels };
1000
+ const [email, setEmail] = useState6("");
1001
+ const [ownError, setOwnError] = useState6();
1002
+ const [isPending, setIsPending] = useState6(false);
1003
+ const [isSent, setIsSent] = useState6(false);
1004
+ const error = ownError ?? externalError;
1005
+ const handleSubmit = async (e) => {
1006
+ e.preventDefault();
1007
+ if (!email.trim()) {
1008
+ setOwnError(t.emailRequired);
1009
+ return;
1010
+ }
1011
+ setOwnError(void 0);
1012
+ setIsPending(true);
1013
+ try {
1014
+ const res = await authClient.signIn.magicLink({
1015
+ email: email.trim(),
1016
+ // The invitation rides the callback: following the link leaves the
1017
+ // browser on the mail client, so the auth handler redeems the token on
1018
+ // the way back, as it does for OAuth.
1019
+ callbackURL: withInviteToken(callbackUrl, invite),
1020
+ ...newUserCallbackUrl ? { newUserCallbackURL: withInviteToken(newUserCallbackUrl, invite) } : {}
1021
+ });
1022
+ if (res?.error) {
1023
+ setOwnError(res.error.message ?? t.sendFailed);
1024
+ return;
1025
+ }
1026
+ setIsSent(true);
1027
+ onSuccess?.(email.trim());
1028
+ } catch (err) {
1029
+ setOwnError(err instanceof Error ? err.message : t.sendFailed);
1030
+ } finally {
1031
+ setIsPending(false);
1032
+ }
1033
+ };
1034
+ return /* @__PURE__ */ jsxs9("div", { className: "space-y-7", children: [
1035
+ /* @__PURE__ */ jsx11(AuthAlert, { children: error }),
1036
+ /* @__PURE__ */ jsx11(AuthAlert, { tone: "success", children: !error && isSent ? t.sent : void 0 }),
1037
+ /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, className: "space-y-[1.125rem]", noValidate: true, children: [
1038
+ /* @__PURE__ */ jsx11(
1039
+ AuthField,
1040
+ {
1041
+ label: t.emailPlaceholder,
1042
+ type: "email",
1043
+ inputMode: "email",
1044
+ value: email,
1045
+ onChange: (e) => {
1046
+ setEmail(e.target.value);
1047
+ setIsSent(false);
1048
+ },
1049
+ required: true,
1050
+ disabled: isPending,
1051
+ autoComplete: "email",
1052
+ autoCapitalize: "none",
1053
+ spellCheck: false,
1054
+ invalid: !!error,
1055
+ fieldClassName
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsx11(
1059
+ AuthSubmit,
1060
+ {
1061
+ spacedAbove: true,
1062
+ pending: isPending,
1063
+ disabled: !email.trim(),
1064
+ pendingLabel: t.submitPending,
1065
+ className: submitClassName,
1066
+ children: isSent ? t.resend : t.submit
1067
+ }
1068
+ )
1069
+ ] }),
1070
+ /* @__PURE__ */ jsxs9("p", { className: "text-center text-sm text-muted-foreground", children: [
1071
+ t.usePassword,
1072
+ " ",
1073
+ /* @__PURE__ */ jsx11(
1074
+ AuthLink,
1075
+ {
1076
+ to: withInviteToken(loginUrl, invite),
1077
+ as: linkComponent,
1078
+ className: AUTH_LINK_CLASS,
1079
+ children: t.login
1080
+ }
1081
+ )
1082
+ ] })
1083
+ ] });
1084
+ }
1085
+ MagicLinkForm.defaults = {
1086
+ title: DEFAULTS5.title,
1087
+ subtitle: DEFAULTS5.subtitle
1088
+ };
1089
+
1090
+ // src/components/reset-password-form.tsx
1091
+ import { useState as useState7 } from "react";
1092
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1093
+ var MIN_PASSWORD_LENGTH2 = 8;
1094
+ var DEFAULTS6 = {
975
1095
  title: "Nouveau mot de passe",
976
1096
  subtitle: "Entre le code \xE0 6 chiffres re\xE7u par e-mail et ton nouveau mot de passe.",
977
1097
  codePlaceholder: "Code de v\xE9rification",
@@ -1001,14 +1121,14 @@ function ResetPasswordForm({
1001
1121
  linkComponent,
1002
1122
  authClient
1003
1123
  }) {
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();
1124
+ const t = { ...DEFAULTS6, ...labels };
1125
+ const [otp, setOtp] = useState7("");
1126
+ const [password, setPassword] = useState7("");
1127
+ const [confirmPassword, setConfirmPassword] = useState7("");
1128
+ const [error, setError] = useState7();
1129
+ const [isResetting, setIsResetting] = useState7(false);
1130
+ const [isResending, setIsResending] = useState7(false);
1131
+ const [resendMessage, setResendMessage] = useState7();
1012
1132
  const tooShort = password.length > 0 && password.length < MIN_PASSWORD_LENGTH2;
1013
1133
  const mismatch = confirmPassword.length > 0 && confirmPassword !== password;
1014
1134
  const handleSubmit = async (e) => {
@@ -1062,11 +1182,11 @@ function ResetPasswordForm({
1062
1182
  setIsResending(false);
1063
1183
  }
1064
1184
  };
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(
1185
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-7", children: [
1186
+ /* @__PURE__ */ jsx12(AuthAlert, { children: error }),
1187
+ /* @__PURE__ */ jsx12(AuthAlert, { tone: "success", children: resendMessage }),
1188
+ /* @__PURE__ */ jsxs10("form", { onSubmit: handleSubmit, className: "space-y-[1.125rem]", noValidate: true, children: [
1189
+ /* @__PURE__ */ jsx12(
1070
1190
  AuthOtpField,
1071
1191
  {
1072
1192
  id: "reset-password-otp",
@@ -1077,7 +1197,7 @@ function ResetPasswordForm({
1077
1197
  fieldClassName
1078
1198
  }
1079
1199
  ),
1080
- /* @__PURE__ */ jsx11(
1200
+ /* @__PURE__ */ jsx12(
1081
1201
  AuthField,
1082
1202
  {
1083
1203
  label: t.passwordPlaceholder,
@@ -1091,7 +1211,7 @@ function ResetPasswordForm({
1091
1211
  invalid: tooShort
1092
1212
  }
1093
1213
  ),
1094
- /* @__PURE__ */ jsx11(
1214
+ /* @__PURE__ */ jsx12(
1095
1215
  AuthField,
1096
1216
  {
1097
1217
  label: t.confirmPlaceholder,
@@ -1104,7 +1224,7 @@ function ResetPasswordForm({
1104
1224
  invalid: mismatch
1105
1225
  }
1106
1226
  ),
1107
- /* @__PURE__ */ jsx11(
1227
+ /* @__PURE__ */ jsx12(
1108
1228
  AuthSubmit,
1109
1229
  {
1110
1230
  spacedAbove: true,
@@ -1116,8 +1236,8 @@ function ResetPasswordForm({
1116
1236
  }
1117
1237
  )
1118
1238
  ] }),
1119
- /* @__PURE__ */ jsxs9("div", { className: "space-y-4 text-center text-sm text-muted-foreground", children: [
1120
- /* @__PURE__ */ jsx11(
1239
+ /* @__PURE__ */ jsxs10("div", { className: "space-y-4 text-center text-sm text-muted-foreground", children: [
1240
+ /* @__PURE__ */ jsx12(
1121
1241
  "button",
1122
1242
  {
1123
1243
  type: "button",
@@ -1127,10 +1247,10 @@ function ResetPasswordForm({
1127
1247
  children: isResending ? t.resendPending : t.resend
1128
1248
  }
1129
1249
  ),
1130
- /* @__PURE__ */ jsxs9("p", { children: [
1250
+ /* @__PURE__ */ jsxs10("p", { children: [
1131
1251
  t.rememberPassword,
1132
1252
  " ",
1133
- /* @__PURE__ */ jsx11(
1253
+ /* @__PURE__ */ jsx12(
1134
1254
  AuthLink,
1135
1255
  {
1136
1256
  to: loginUrl,
@@ -1144,25 +1264,25 @@ function ResetPasswordForm({
1144
1264
  ] });
1145
1265
  }
1146
1266
  ResetPasswordForm.defaults = {
1147
- title: DEFAULTS5.title,
1148
- subtitle: DEFAULTS5.subtitle
1267
+ title: DEFAULTS6.title,
1268
+ subtitle: DEFAULTS6.subtitle
1149
1269
  };
1150
1270
 
1151
1271
  // src/components/auth-heading.tsx
1152
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1272
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1153
1273
  function AuthHeading({
1154
1274
  title,
1155
1275
  subtitle,
1156
1276
  titleClassName = "text-[2rem] font-semibold leading-[1.1] tracking-[-0.03em] sm:text-[2.25rem]"
1157
1277
  }) {
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 })
1278
+ return /* @__PURE__ */ jsxs11("header", { className: "space-y-2.5 text-center", children: [
1279
+ /* @__PURE__ */ jsx13("h1", { className: titleClassName, children: title }),
1280
+ subtitle && /* @__PURE__ */ jsx13("p", { className: "text-balance text-[0.9375rem] leading-relaxed text-muted-foreground", children: subtitle })
1161
1281
  ] });
1162
1282
  }
1163
1283
 
1164
1284
  // src/components/auth-layout.tsx
1165
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1285
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1166
1286
  function AuthLayout({
1167
1287
  logo,
1168
1288
  panel,
@@ -1172,7 +1292,7 @@ function AuthLayout({
1172
1292
  children,
1173
1293
  footer
1174
1294
  }) {
1175
- return /* @__PURE__ */ jsxs11(
1295
+ return /* @__PURE__ */ jsxs12(
1176
1296
  "div",
1177
1297
  {
1178
1298
  className: [
@@ -1186,7 +1306,7 @@ function AuthLayout({
1186
1306
  // screen, so the colour only has to sit behind the heading. Covering
1187
1307
  // the whole height would leave the panel's own copy showing under the
1188
1308
  // card, which reads as a second, half-hidden screen.
1189
- /* @__PURE__ */ jsx13(
1309
+ /* @__PURE__ */ jsx14(
1190
1310
  "div",
1191
1311
  {
1192
1312
  "aria-hidden": "true",
@@ -1194,12 +1314,12 @@ function AuthLayout({
1194
1314
  children: panel
1195
1315
  }
1196
1316
  ),
1197
- /* @__PURE__ */ jsxs11("div", { className: `mx-auto w-full ${panel ? "max-w-4xl" : "max-w-[440px]"}`, children: [
1317
+ /* @__PURE__ */ jsxs12("div", { className: `mx-auto w-full ${panel ? "max-w-4xl" : "max-w-[440px]"}`, children: [
1198
1318
  (logo || title) && // The mark sits tight above the title so the two read as one block
1199
1319
  // rather than as a stray label; the gap down to the card is the
1200
1320
  // largest on the screen — that step is what separates "who this is"
1201
1321
  // from "what you do here".
1202
- /* @__PURE__ */ jsxs11(
1322
+ /* @__PURE__ */ jsxs12(
1203
1323
  "div",
1204
1324
  {
1205
1325
  className: [
@@ -1210,8 +1330,8 @@ function AuthLayout({
1210
1330
  panel ? "text-white [&_p]:text-white/75 md:text-foreground md:[&_p]:text-muted-foreground" : ""
1211
1331
  ].filter(Boolean).join(" "),
1212
1332
  children: [
1213
- logo && /* @__PURE__ */ jsx13("div", { className: "flex justify-center", children: logo }),
1214
- title && /* @__PURE__ */ jsx13(
1333
+ logo && /* @__PURE__ */ jsx14("div", { className: "flex justify-center", children: logo }),
1334
+ title && /* @__PURE__ */ jsx14(
1215
1335
  AuthHeading,
1216
1336
  {
1217
1337
  title,
@@ -1222,7 +1342,7 @@ function AuthLayout({
1222
1342
  ]
1223
1343
  }
1224
1344
  ),
1225
- /* @__PURE__ */ jsxs11(
1345
+ /* @__PURE__ */ jsxs12(
1226
1346
  "div",
1227
1347
  {
1228
1348
  className: [
@@ -1236,7 +1356,7 @@ function AuthLayout({
1236
1356
  children: [
1237
1357
  panel && // Decorative: it must never carry information the form does not,
1238
1358
  // so it is hidden from assistive tech rather than described.
1239
- /* @__PURE__ */ jsx13(
1359
+ /* @__PURE__ */ jsx14(
1240
1360
  "div",
1241
1361
  {
1242
1362
  "aria-hidden": "true",
@@ -1244,11 +1364,11 @@ function AuthLayout({
1244
1364
  children: panel
1245
1365
  }
1246
1366
  ),
1247
- /* @__PURE__ */ jsx13("div", { className: panel ? "px-5 pb-12 pt-7 sm:p-7" : "sm:p-7", children })
1367
+ /* @__PURE__ */ jsx14("div", { className: panel ? "px-5 pb-12 pt-7 sm:p-7" : "sm:p-7", children })
1248
1368
  ]
1249
1369
  }
1250
1370
  ),
1251
- footer && /* @__PURE__ */ jsx13("div", { className: "mt-8 text-center text-xs leading-relaxed text-muted-foreground", children: footer })
1371
+ footer && /* @__PURE__ */ jsx14("div", { className: "mt-8 text-center text-xs leading-relaxed text-muted-foreground", children: footer })
1252
1372
  ] })
1253
1373
  ]
1254
1374
  }
@@ -1256,7 +1376,7 @@ function AuthLayout({
1256
1376
  }
1257
1377
 
1258
1378
  // src/components/invitation-notice.tsx
1259
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1379
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1260
1380
  function defaultSupportEmail() {
1261
1381
  if (typeof window === "undefined") return void 0;
1262
1382
  const host = window.location.hostname;
@@ -1276,15 +1396,15 @@ function InvitationNotice({
1276
1396
  action
1277
1397
  }) {
1278
1398
  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 })
1399
+ return /* @__PURE__ */ jsxs13("div", { className: "space-y-8", children: [
1400
+ /* @__PURE__ */ jsxs13("div", { children: [
1401
+ /* @__PURE__ */ jsx15("h1", { className: "text-2xl font-bold tracking-tight", children: title }),
1402
+ /* @__PURE__ */ jsx15("p", { className: "mt-1 text-sm text-muted-foreground", children: REASON_MESSAGE[reason] ?? REASON_MESSAGE.unknown })
1283
1403
  ] }),
1284
- contact ? /* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
1404
+ contact ? /* @__PURE__ */ jsxs13("p", { className: "text-sm text-muted-foreground", children: [
1285
1405
  "\xC9crivez-nous \xE0",
1286
1406
  " ",
1287
- /* @__PURE__ */ jsx14(
1407
+ /* @__PURE__ */ jsx15(
1288
1408
  "a",
1289
1409
  {
1290
1410
  href: `mailto:${contact}`,
@@ -1323,6 +1443,39 @@ function clearOAuthError(param = "error") {
1323
1443
  url.searchParams.delete(param);
1324
1444
  window.history.replaceState({}, "", url.toString());
1325
1445
  }
1446
+
1447
+ // src/magic-link-error.ts
1448
+ var MAGIC_LINK_ERROR_DEFAULTS = {
1449
+ invalidToken: "Ce lien n'est plus valide. Il expire apr\xE8s 5 minutes et ne fonctionne qu'une fois \u2014 demandes-en un nouveau.",
1450
+ signUpDisabled: "Aucun compte n'existe pour cette adresse. Cr\xE9e-en un d'abord.",
1451
+ failed: "La connexion par lien a \xE9chou\xE9. R\xE9essaie."
1452
+ };
1453
+ function magicLinkErrorMessage(code, labels = {}) {
1454
+ const t = { ...MAGIC_LINK_ERROR_DEFAULTS, ...labels };
1455
+ switch (code) {
1456
+ case "INVALID_TOKEN":
1457
+ return t.invalidToken;
1458
+ case "new_user_signup_disabled":
1459
+ return t.signUpDisabled;
1460
+ default:
1461
+ return t.failed;
1462
+ }
1463
+ }
1464
+ var MAGIC_LINK_ERROR_CODES = /* @__PURE__ */ new Set([
1465
+ "INVALID_TOKEN",
1466
+ "new_user_signup_disabled",
1467
+ "failed_to_create_user",
1468
+ "failed_to_create_session"
1469
+ ]);
1470
+ function isMagicLinkError(code) {
1471
+ return !!code && MAGIC_LINK_ERROR_CODES.has(code);
1472
+ }
1473
+ function initialMagicLinkError(labels = {}, param = "error") {
1474
+ if (typeof window === "undefined") return void 0;
1475
+ const code = new URLSearchParams(window.location.search).get(param);
1476
+ if (!isMagicLinkError(code)) return void 0;
1477
+ return magicLinkErrorMessage(code, labels);
1478
+ }
1326
1479
  export {
1327
1480
  AuthField,
1328
1481
  AuthLayout,
@@ -1331,14 +1484,18 @@ export {
1331
1484
  ForgotPasswordForm,
1332
1485
  InvitationNotice,
1333
1486
  LoginForm,
1487
+ MagicLinkForm,
1334
1488
  RegisterForm,
1335
1489
  ResetPasswordForm,
1336
1490
  SocialButtons,
1337
1491
  VerifyEmailForm,
1338
1492
  clearOAuthError,
1493
+ initialMagicLinkError,
1339
1494
  initialOAuthError,
1340
1495
  isEmailNotVerified,
1341
1496
  isInvitationFailure,
1497
+ isMagicLinkError,
1498
+ magicLinkErrorMessage,
1342
1499
  normalizeInviteToken,
1343
1500
  oauthErrorMessage,
1344
1501
  useLogout,