@lalternative/auth 0.3.0 → 0.4.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 +23 -3
- package/dist/chunk-73BHM4PZ.js +72 -0
- package/dist/chunk-73BHM4PZ.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/invitation-DIZAb2BJ.d.ts +82 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +14 -1
- package/dist/server.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -45,17 +45,37 @@ that held the token.
|
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
// server — auth callback (e.g. routes/api/auth/$.ts)
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
claimInvitation,
|
|
50
|
+
completesSignup,
|
|
51
|
+
invitationOutcomeCookie,
|
|
52
|
+
inviteTokenFrom,
|
|
53
|
+
} from "@lalternative/auth/server"
|
|
54
|
+
|
|
55
|
+
const response = await auth.handler(request)
|
|
56
|
+
if (!response.ok || !completesSignup(new URL(request.url).pathname)) return response
|
|
57
|
+
|
|
58
|
+
// Read the session back from the Set-Cookie the handler just issued, so this
|
|
59
|
+
// works for every flow without parsing each response body shape.
|
|
60
|
+
const setCookie = response.headers.get("set-cookie")
|
|
61
|
+
const session = setCookie
|
|
62
|
+
? await auth.api
|
|
63
|
+
.getSession({ headers: new Headers({ cookie: setCookie }) })
|
|
64
|
+
.catch(() => null)
|
|
65
|
+
: null
|
|
49
66
|
|
|
50
67
|
const token = inviteTokenFrom(request)
|
|
51
|
-
if (token && user) {
|
|
68
|
+
if (token && session?.user) {
|
|
52
69
|
// Best-effort: a sign-in must never fail because a claim did not go through.
|
|
53
70
|
const outcome = await claimInvitation({
|
|
54
71
|
endpoint: `${process.env.LUNGOR_API_URL}/invitations/claim`,
|
|
55
72
|
apiKey: process.env.LUNGOR_APP_API_KEY,
|
|
56
73
|
token,
|
|
57
|
-
externalUserId: user.id,
|
|
74
|
+
externalUserId: session.user.id,
|
|
58
75
|
})
|
|
76
|
+
if (outcome !== "granted") {
|
|
77
|
+
response.headers.append("set-cookie", invitationOutcomeCookie(outcome))
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
```
|
|
61
81
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/invitation.ts
|
|
2
|
+
var OUTCOME_BY_STATUS = {
|
|
3
|
+
404: "unknown",
|
|
4
|
+
409: "claimed",
|
|
5
|
+
410: "expired"
|
|
6
|
+
};
|
|
7
|
+
async function claimInvitation({
|
|
8
|
+
endpoint,
|
|
9
|
+
token,
|
|
10
|
+
externalUserId,
|
|
11
|
+
apiKey,
|
|
12
|
+
extra,
|
|
13
|
+
headers,
|
|
14
|
+
timeoutMs = 5e3
|
|
15
|
+
}) {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(endpoint, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
22
|
+
...headers
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
token,
|
|
26
|
+
external_user_id: externalUserId,
|
|
27
|
+
...extra
|
|
28
|
+
}),
|
|
29
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
30
|
+
});
|
|
31
|
+
if (res.ok) return "granted";
|
|
32
|
+
return OUTCOME_BY_STATUS[res.status] ?? "failed";
|
|
33
|
+
} catch {
|
|
34
|
+
return "failed";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function inviteTokenFrom(request, param = "invite") {
|
|
38
|
+
const direct = new URL(request.url).searchParams.get(param);
|
|
39
|
+
if (direct && direct.trim() !== "") return direct;
|
|
40
|
+
const referer = request.headers.get("referer");
|
|
41
|
+
if (!referer) return null;
|
|
42
|
+
try {
|
|
43
|
+
const token = new URL(referer).searchParams.get(param);
|
|
44
|
+
return token && token.trim() !== "" ? token : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var SIGNUP_COMPLETING = [
|
|
50
|
+
"/sign-up/email",
|
|
51
|
+
"/sign-in/email-otp",
|
|
52
|
+
"/email-otp/verify-email",
|
|
53
|
+
"/callback/"
|
|
54
|
+
];
|
|
55
|
+
function completesSignup(pathname) {
|
|
56
|
+
return SIGNUP_COMPLETING.some((p) => pathname.includes(p));
|
|
57
|
+
}
|
|
58
|
+
function invitationOutcomeCookie(outcome, name = "invite_claim") {
|
|
59
|
+
return `${name}=${outcome}; Path=/; Max-Age=120; SameSite=Lax`;
|
|
60
|
+
}
|
|
61
|
+
function isInvitationFailure(outcome) {
|
|
62
|
+
return outcome === "expired" || outcome === "claimed" || outcome === "unknown";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
claimInvitation,
|
|
67
|
+
inviteTokenFrom,
|
|
68
|
+
completesSignup,
|
|
69
|
+
invitationOutcomeCookie,
|
|
70
|
+
isInvitationFailure
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=chunk-73BHM4PZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/invitation.ts"],"sourcesContent":["import type { InvitationFailure } from \"./types\"\n\n/**\n * What became of a claim, in terms the invitee can be told.\n *\n * 'expired' and 'claimed' are kept apart from 'unknown' because only they say\n * the offer was real, which is what tells someone that asking for a new link is\n * worth it rather than doubting the address they were invited at. The three\n * failures match InvitationFailure, so an outcome feeds InvitationNotice\n * directly.\n */\nexport type ClaimOutcome = \"granted\" | InvitationFailure | \"failed\"\n\nconst OUTCOME_BY_STATUS: Record<number, ClaimOutcome> = {\n 404: \"unknown\",\n 409: \"claimed\",\n 410: \"expired\",\n}\n\nexport interface ClaimInvitationOptions {\n /** Absolute URL of the endpoint that redeems a token. */\n endpoint: string\n token: string\n /** The account the app just created, which the grant is attached to. */\n externalUserId: string\n /** Sent as the Authorization bearer — typically the app's API key. */\n apiKey?: string\n /** Merged into the request body, for backends wanting more than the token. */\n extra?: Record<string, unknown>\n /** Headers merged last, so a caller can pass a cookie-based credential. */\n headers?: Record<string, string>\n /** Bounds the call so a slow API never stalls the sign-in response. */\n timeoutMs?: number\n}\n\n/**\n * Redeems an invitation token for a user who has just signed in, turning the\n * offer into a grant on their account.\n *\n * Why this belongs on the SERVER, on the auth callback rather than in the page:\n * the invitation link lands on /register?invite=<token>, but the sign-up that\n * follows can complete through any of three flows (password + OTP, OAuth\n * redirect, email verification), and only two of them return to the page that\n * held the token. Claiming where the session is established covers every flow\n * with one code path.\n *\n * Best-effort by design: a sign-in must never fail because an invitation could\n * not be redeemed. A failed claim leaves the invitation unclaimed and the user\n * on their default tier — recoverable by following the link again, since a\n * refused claim consumes nothing.\n */\nexport async function claimInvitation({\n endpoint,\n token,\n externalUserId,\n apiKey,\n extra,\n headers,\n timeoutMs = 5000,\n}: ClaimInvitationOptions): Promise<ClaimOutcome> {\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),\n ...headers,\n },\n body: JSON.stringify({\n token,\n external_user_id: externalUserId,\n ...extra,\n }),\n signal: AbortSignal.timeout(timeoutMs),\n })\n\n if (res.ok) return \"granted\"\n return OUTCOME_BY_STATUS[res.status] ?? \"failed\"\n } catch {\n return \"failed\"\n }\n}\n\n/**\n * Extracts the invitation token from an auth request.\n *\n * The token lives on the page the invitee landed on (/register?invite=…), never\n * on the auth endpoints themselves, so it has to be recovered from the request\n * that completes the sign-up. Two sources, because no single one covers every\n * flow: the URL, for the OAuth callback reached through a redirect whose query\n * string the app controls; and the Referer, for the password and OTP flows,\n * which are XHR calls issued BY that page and therefore carry it.\n *\n * Reading it per-request keeps the claim stateless: nothing associates a\n * browser with a pending invitation, and a request with no token claims\n * nothing. A malformed Referer is ignored rather than thrown on — it is\n * attacker-controlled input on a best-effort path.\n */\nexport function inviteTokenFrom(\n request: Request,\n param = \"invite\",\n): string | null {\n const direct = new URL(request.url).searchParams.get(param)\n if (direct && direct.trim() !== \"\") return direct\n\n const referer = request.headers.get(\"referer\")\n if (!referer) return null\n try {\n const token = new URL(referer).searchParams.get(param)\n return token && token.trim() !== \"\" ? token : null\n } catch {\n return null\n }\n}\n\n/**\n * BetterAuth paths that complete a sign-up. Email/password returns the user\n * straight from sign-up/email; the OTP and OAuth flows only produce a usable\n * account once the verification/callback succeeds, so those are the moments\n * worth reacting to.\n */\nconst SIGNUP_COMPLETING = [\n \"/sign-up/email\",\n \"/sign-in/email-otp\",\n \"/email-otp/verify-email\",\n \"/callback/\",\n]\n\n/**\n * Whether this request is the one that just created a usable account — the\n * moment to provision, claim an invitation, or greet someone. Matching on the\n * path rather than on a response body keeps it flow-agnostic: the three\n * sign-up flows return three different shapes.\n */\nexport function completesSignup(pathname: string): boolean {\n return SIGNUP_COMPLETING.some((p) => pathname.includes(p))\n}\n\n/**\n * Carries a failed claim to the next page. The claim happens inside an auth\n * response nobody renders, so its result would otherwise reach only the server\n * log — leaving an invitee on the default tier with no idea their link had\n * lapsed. Short-lived and readable by the page, which reports it and clears it.\n */\nexport function invitationOutcomeCookie(\n outcome: ClaimOutcome,\n name = \"invite_claim\",\n): string {\n return `${name}=${outcome}; Path=/; Max-Age=120; SameSite=Lax`\n}\n\n/**\n * Whether an outcome is one the invitee should be shown a reason for.\n * 'failed' is excluded: it means the call did not complete, so the offer may\n * still be good and telling someone their invitation is invalid would be wrong.\n */\nexport function isInvitationFailure(\n outcome: ClaimOutcome,\n): outcome is InvitationFailure {\n return outcome === \"expired\" || outcome === \"claimed\" || outcome === \"unknown\"\n}\n"],"mappings":";AAaA,IAAM,oBAAkD;AAAA,EACtD,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAkCA,eAAsB,gBAAgB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAAkD;AAChD,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,UAAU;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAI,SAAS,EAAE,eAAe,UAAU,MAAM,GAAG,IAAI,CAAC;AAAA,QACtD,GAAG;AAAA,MACL;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA,kBAAkB;AAAA,QAClB,GAAG;AAAA,MACL,CAAC;AAAA,MACD,QAAQ,YAAY,QAAQ,SAAS;AAAA,IACvC,CAAC;AAED,QAAI,IAAI,GAAI,QAAO;AACnB,WAAO,kBAAkB,IAAI,MAAM,KAAK;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBO,SAAS,gBACd,SACA,QAAQ,UACO;AACf,QAAM,SAAS,IAAI,IAAI,QAAQ,GAAG,EAAE,aAAa,IAAI,KAAK;AAC1D,MAAI,UAAU,OAAO,KAAK,MAAM,GAAI,QAAO;AAE3C,QAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;AAC7C,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI;AACF,UAAM,QAAQ,IAAI,IAAI,OAAO,EAAE,aAAa,IAAI,KAAK;AACrD,WAAO,SAAS,MAAM,KAAK,MAAM,KAAK,QAAQ;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,SAAS,gBAAgB,UAA2B;AACzD,SAAO,kBAAkB,KAAK,CAAC,MAAM,SAAS,SAAS,CAAC,CAAC;AAC3D;AAQO,SAAS,wBACd,SACA,OAAO,gBACC;AACR,SAAO,GAAG,IAAI,IAAI,OAAO;AAC3B;AAOO,SAAS,oBACd,SAC8B;AAC9B,SAAO,YAAY,aAAa,YAAY,aAAa,YAAY;AACvE;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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
|
+
export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-DIZAb2BJ.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Returns a useSession hook bound to the given auth client.
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isInvitationFailure
|
|
3
|
+
} from "./chunk-73BHM4PZ.js";
|
|
4
|
+
|
|
1
5
|
// src/hooks/use-session.ts
|
|
2
6
|
function useSession(authClient) {
|
|
3
7
|
return authClient.useSession();
|
|
@@ -434,6 +438,7 @@ export {
|
|
|
434
438
|
InvitationNotice,
|
|
435
439
|
ResetPasswordForm,
|
|
436
440
|
VerifyEmailForm,
|
|
441
|
+
isInvitationFailure,
|
|
437
442
|
useLogout,
|
|
438
443
|
useSession
|
|
439
444
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-session.ts","../src/components/verify-email-form.tsx","../src/components/forgot-password-form.tsx","../src/components/reset-password-form.tsx","../src/components/auth-layout.tsx","../src/components/invitation-notice.tsx"],"sourcesContent":["import type { PlatformAuthClient } from \"../client\"\n\n/**\n * Returns a useSession hook bound to the given auth client.\n * Usage: const { data: session, isPending } = useSession(authClient)\n */\nexport function useSession(authClient: PlatformAuthClient) {\n return authClient.useSession()\n}\n\n/**\n * Returns a logout function bound to the given auth client.\n * Usage: const logout = useLogout(authClient)\n */\nexport function useLogout(authClient: PlatformAuthClient) {\n const signOut = async () => {\n await authClient.signOut()\n }\n return signOut\n}\n","import { useState, type FormEvent } from \"react\"\nimport type { VerifyEmailFormProps } from \"../types\"\n\nexport function VerifyEmailForm({\n email,\n onSuccess,\n authClient,\n}: VerifyEmailFormProps) {\n const [otp, setOtp] = useState(\"\")\n const [isVerifying, setIsVerifying] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const [resendMessage, setResendMessage] = useState<string | undefined>()\n const [error, setError] = useState<string | undefined>()\n\n const handleVerify = async (e: FormEvent) => {\n e.preventDefault()\n if (!otp.trim() || otp.length < 6) {\n setError(\"Please enter the 6-digit code\")\n return\n }\n setError(undefined)\n setIsVerifying(true)\n try {\n const res = await authClient.emailOtp.verifyEmail({ email, otp })\n console.log(\"[verify-email] response:\", JSON.stringify(res?.data), \"error:\", JSON.stringify(res?.error))\n if (res?.error) {\n setError(res.error.message ?? \"Invalid code. Please try again.\")\n return\n }\n onSuccess?.()\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Invalid code. Please try again.\")\n } finally {\n setIsVerifying(false)\n }\n }\n\n const handleResend = async () => {\n if (!email) {\n setError(\"Email address is not available. Please register again.\")\n return\n }\n setError(undefined)\n setResendMessage(undefined)\n setIsResending(true)\n try {\n await authClient.emailOtp.sendVerificationOtp({\n email,\n type: \"email-verification\",\n })\n setResendMessage(\"A new code has been sent to your inbox.\")\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to resend code.\")\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">\n Verify your email\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {email ? (\n <>\n Enter the 6-digit code sent to{\" \"}\n <span className=\"font-medium text-foreground\">{email}</span>\n </>\n ) : (\n \"Enter the 6-digit code sent to your email\"\n )}\n </p>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {error}\n </div>\n )}\n\n {resendMessage && (\n <div className=\"rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-xs text-green-700\">\n {resendMessage}\n </div>\n )}\n\n <form onSubmit={handleVerify} className=\"space-y-4\" noValidate>\n <input\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n maxLength={6}\n value={otp}\n onChange={(e) => setOtp(e.target.value.replace(/\\D/g, \"\"))}\n placeholder=\"000000\"\n required\n disabled={isVerifying}\n autoComplete=\"one-time-code\"\n className=\"flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-center text-lg tracking-[0.4em] font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <button\n type=\"submit\"\n disabled={isVerifying || otp.length < 6}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\"\n >\n {isVerifying ? \"Verifying...\" : \"Verify email\"}\n </button>\n\n <button\n type=\"button\"\n onClick={handleResend}\n disabled={isResending}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\"\n >\n {isResending ? \"Sending...\" : \"Resend code\"}\n </button>\n </form>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n Already verified?{\" \"}\n <a\n href=\"/login\"\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n Sign in\n </a>\n </p>\n </div>\n )\n}\n","import { useState, type FormEvent } from \"react\"\nimport type { ForgotPasswordFormProps } from \"../types\"\n\nexport function ForgotPasswordForm({\n onSuccess,\n loginUrl = \"/login\",\n authClient,\n}: ForgotPasswordFormProps) {\n const [email, setEmail] = useState(\"\")\n const [error, setError] = useState<string | undefined>()\n const [isPending, setIsPending] = useState(false)\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault()\n if (!email.trim()) {\n setError(\"Please enter your email address\")\n return\n }\n setError(undefined)\n setIsPending(true)\n try {\n const res = await authClient.emailOtp.sendVerificationOtp({\n email,\n type: \"forget-password\",\n })\n if (res?.error) {\n setError(res.error.message ?? \"Failed to send reset code\")\n return\n }\n onSuccess?.(email)\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to send reset code\")\n } finally {\n setIsPending(false)\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">\n Forgot your password?\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n Enter your email address and we'll send you a code to reset your\n password.\n </p>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {error}\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-4\" noValidate>\n <input\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"Email address\"\n required\n disabled={isPending}\n autoComplete=\"email\"\n className=\"flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <button\n type=\"submit\"\n disabled={isPending || !email.trim()}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\"\n >\n {isPending ? \"Sending...\" : \"Send reset code\"}\n </button>\n </form>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n Remember your password?{\" \"}\n <a\n href={loginUrl}\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n Sign in\n </a>\n </p>\n </div>\n )\n}\n","import { useState, type FormEvent } from \"react\"\nimport type { ResetPasswordFormProps } from \"../types\"\n\nexport function ResetPasswordForm({\n email,\n onSuccess,\n loginUrl = \"/login\",\n authClient,\n}: ResetPasswordFormProps) {\n const [otp, setOtp] = useState(\"\")\n const [password, setPassword] = useState(\"\")\n const [confirmPassword, setConfirmPassword] = useState(\"\")\n const [error, setError] = useState<string | undefined>()\n const [isResetting, setIsResetting] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const [resendMessage, setResendMessage] = useState<string | undefined>()\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault()\n if (!otp.trim() || otp.length < 6) {\n setError(\"Please enter the 6-digit code\")\n return\n }\n if (password.length < 8) {\n setError(\"Password must be at least 8 characters\")\n return\n }\n if (password !== confirmPassword) {\n setError(\"Passwords do not match\")\n return\n }\n setError(undefined)\n setIsResetting(true)\n try {\n const res = await fetch(\"/api/auth/email-otp/reset-password\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email, otp, password }),\n })\n if (!res.ok) {\n const body = await res.json().catch(() => null)\n setError(body?.message ?? \"Failed to reset password\")\n return\n }\n onSuccess?.()\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to reset password\",\n )\n } finally {\n setIsResetting(false)\n }\n }\n\n const handleResend = async () => {\n setError(undefined)\n setResendMessage(undefined)\n setIsResending(true)\n try {\n await authClient.emailOtp.sendVerificationOtp({\n email,\n type: \"forget-password\",\n })\n setResendMessage(\"A new code has been sent to your inbox.\")\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to resend code.\")\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">\n Reset your password\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n Enter the 6-digit code sent to{\" \"}\n <span className=\"font-medium text-foreground\">{email}</span> and your\n new password.\n </p>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {error}\n </div>\n )}\n\n {resendMessage && (\n <div className=\"rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-xs text-green-700\">\n {resendMessage}\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-4\" noValidate>\n <input\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n maxLength={6}\n value={otp}\n onChange={(e) => setOtp(e.target.value.replace(/\\D/g, \"\"))}\n placeholder=\"000000\"\n required\n disabled={isResetting}\n autoComplete=\"one-time-code\"\n className=\"flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-center text-lg tracking-[0.4em] font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <input\n type=\"password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n placeholder=\"New password\"\n required\n disabled={isResetting}\n autoComplete=\"new-password\"\n className=\"flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <input\n type=\"password\"\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n placeholder=\"Confirm new password\"\n required\n disabled={isResetting}\n autoComplete=\"new-password\"\n className=\"flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <button\n type=\"submit\"\n disabled={isResetting || otp.length < 6 || !password}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\"\n >\n {isResetting ? \"Resetting...\" : \"Reset password\"}\n </button>\n\n <button\n type=\"button\"\n onClick={handleResend}\n disabled={isResending}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\"\n >\n {isResending ? \"Sending...\" : \"Resend code\"}\n </button>\n </form>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n Remember your password?{\" \"}\n <a\n href={loginUrl}\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n Sign in\n </a>\n </p>\n </div>\n )\n}\n","import type { AuthLayoutProps } from \"../types\"\n\nexport function AuthLayout({\n logo,\n title,\n subtitle,\n children,\n footer,\n}: AuthLayoutProps) {\n return (\n <div className=\"flex min-h-screen items-center justify-center bg-background\">\n <div className=\"w-full max-w-md\">\n <div className=\"mb-10 text-center\">\n {logo && <div className=\"mb-6 flex justify-center\">{logo}</div>}\n <h1 className=\"text-[32px] font-light tracking-tight\">{title}</h1>\n {subtitle && (\n <p className=\"mt-2 text-sm text-muted-foreground\">{subtitle}</p>\n )}\n </div>\n\n <div className=\"rounded-xl border bg-card p-8 shadow-sm\">\n {children}\n </div>\n\n {footer && (\n <div className=\"mt-6 text-center text-xs text-muted-foreground\">\n {footer}\n </div>\n )}\n </div>\n </div>\n )\n}\n","import type { InvitationNoticeProps } from \"../types\"\n\n/**\n * Every app is reachable at contact@ its own apex domain, so the address is\n * derived rather than configured. Sub-domains are stripped because the app is\n * routinely served from app./admin. while the mailbox lives on the apex;\n * multi-part public suffixes (.co.uk) would need a real suffix list and no app\n * using this is on one.\n */\nfunction defaultSupportEmail(): string | undefined {\n if (typeof window === \"undefined\") return undefined\n const host = window.location.hostname\n if (!host || host === \"localhost\" || /^[\\d.]+$/.test(host)) return undefined\n const apex = host.split(\".\").slice(-2).join(\".\")\n return `contact@${apex}`\n}\n\nconst REASON_MESSAGE: Record<string, string> = {\n expired: \"Cette invitation a expiré.\",\n claimed: \"Cette invitation a déjà été utilisée.\",\n unknown: \"Ce lien d'invitation n'est pas valide.\",\n}\n\n/**\n * What an invitee sees when their link does not work.\n *\n * It names the reason instead of merging the cases into one message: \"expired\"\n * tells someone their link was real and that asking for another one is worth\n * it, which \"invalid\" does not. Short TTLs make that distinction routine.\n *\n * The only way forward offered is a mailto, deliberately. A self-service\n * \"request a new invitation\" form would mean a public endpoint accepting dead\n * tokens, a queue to moderate, and a way to probe which tokens once existed —\n * for a flow where the operator already knows the person by name.\n */\nexport function InvitationNotice({\n reason = \"unknown\",\n supportEmail,\n title = \"Invitation indisponible\",\n action,\n}: InvitationNoticeProps) {\n const contact = supportEmail ?? defaultSupportEmail()\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {REASON_MESSAGE[reason] ?? REASON_MESSAGE.unknown}\n </p>\n </div>\n\n {contact ? (\n <p className=\"text-sm text-muted-foreground\">\n Écrivez-nous à{\" \"}\n <a\n href={`mailto:${contact}`}\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n {contact}\n </a>{\" \"}\n pour en recevoir une nouvelle.\n </p>\n ) : null}\n\n {action}\n </div>\n )\n}\n"],"mappings":"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","names":["useState","jsx","jsxs","useState","jsx","jsxs","jsx","jsxs","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-session.ts","../src/components/verify-email-form.tsx","../src/components/forgot-password-form.tsx","../src/components/reset-password-form.tsx","../src/components/auth-layout.tsx","../src/components/invitation-notice.tsx"],"sourcesContent":["import type { PlatformAuthClient } from \"../client\"\n\n/**\n * Returns a useSession hook bound to the given auth client.\n * Usage: const { data: session, isPending } = useSession(authClient)\n */\nexport function useSession(authClient: PlatformAuthClient) {\n return authClient.useSession()\n}\n\n/**\n * Returns a logout function bound to the given auth client.\n * Usage: const logout = useLogout(authClient)\n */\nexport function useLogout(authClient: PlatformAuthClient) {\n const signOut = async () => {\n await authClient.signOut()\n }\n return signOut\n}\n","import { useState, type FormEvent } from \"react\"\nimport type { VerifyEmailFormProps } from \"../types\"\n\nexport function VerifyEmailForm({\n email,\n onSuccess,\n authClient,\n}: VerifyEmailFormProps) {\n const [otp, setOtp] = useState(\"\")\n const [isVerifying, setIsVerifying] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const [resendMessage, setResendMessage] = useState<string | undefined>()\n const [error, setError] = useState<string | undefined>()\n\n const handleVerify = async (e: FormEvent) => {\n e.preventDefault()\n if (!otp.trim() || otp.length < 6) {\n setError(\"Please enter the 6-digit code\")\n return\n }\n setError(undefined)\n setIsVerifying(true)\n try {\n const res = await authClient.emailOtp.verifyEmail({ email, otp })\n console.log(\"[verify-email] response:\", JSON.stringify(res?.data), \"error:\", JSON.stringify(res?.error))\n if (res?.error) {\n setError(res.error.message ?? \"Invalid code. Please try again.\")\n return\n }\n onSuccess?.()\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Invalid code. Please try again.\")\n } finally {\n setIsVerifying(false)\n }\n }\n\n const handleResend = async () => {\n if (!email) {\n setError(\"Email address is not available. Please register again.\")\n return\n }\n setError(undefined)\n setResendMessage(undefined)\n setIsResending(true)\n try {\n await authClient.emailOtp.sendVerificationOtp({\n email,\n type: \"email-verification\",\n })\n setResendMessage(\"A new code has been sent to your inbox.\")\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to resend code.\")\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">\n Verify your email\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {email ? (\n <>\n Enter the 6-digit code sent to{\" \"}\n <span className=\"font-medium text-foreground\">{email}</span>\n </>\n ) : (\n \"Enter the 6-digit code sent to your email\"\n )}\n </p>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {error}\n </div>\n )}\n\n {resendMessage && (\n <div className=\"rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-xs text-green-700\">\n {resendMessage}\n </div>\n )}\n\n <form onSubmit={handleVerify} className=\"space-y-4\" noValidate>\n <input\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n maxLength={6}\n value={otp}\n onChange={(e) => setOtp(e.target.value.replace(/\\D/g, \"\"))}\n placeholder=\"000000\"\n required\n disabled={isVerifying}\n autoComplete=\"one-time-code\"\n className=\"flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-center text-lg tracking-[0.4em] font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <button\n type=\"submit\"\n disabled={isVerifying || otp.length < 6}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\"\n >\n {isVerifying ? \"Verifying...\" : \"Verify email\"}\n </button>\n\n <button\n type=\"button\"\n onClick={handleResend}\n disabled={isResending}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\"\n >\n {isResending ? \"Sending...\" : \"Resend code\"}\n </button>\n </form>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n Already verified?{\" \"}\n <a\n href=\"/login\"\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n Sign in\n </a>\n </p>\n </div>\n )\n}\n","import { useState, type FormEvent } from \"react\"\nimport type { ForgotPasswordFormProps } from \"../types\"\n\nexport function ForgotPasswordForm({\n onSuccess,\n loginUrl = \"/login\",\n authClient,\n}: ForgotPasswordFormProps) {\n const [email, setEmail] = useState(\"\")\n const [error, setError] = useState<string | undefined>()\n const [isPending, setIsPending] = useState(false)\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault()\n if (!email.trim()) {\n setError(\"Please enter your email address\")\n return\n }\n setError(undefined)\n setIsPending(true)\n try {\n const res = await authClient.emailOtp.sendVerificationOtp({\n email,\n type: \"forget-password\",\n })\n if (res?.error) {\n setError(res.error.message ?? \"Failed to send reset code\")\n return\n }\n onSuccess?.(email)\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to send reset code\")\n } finally {\n setIsPending(false)\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">\n Forgot your password?\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n Enter your email address and we'll send you a code to reset your\n password.\n </p>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {error}\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-4\" noValidate>\n <input\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"Email address\"\n required\n disabled={isPending}\n autoComplete=\"email\"\n className=\"flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <button\n type=\"submit\"\n disabled={isPending || !email.trim()}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\"\n >\n {isPending ? \"Sending...\" : \"Send reset code\"}\n </button>\n </form>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n Remember your password?{\" \"}\n <a\n href={loginUrl}\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n Sign in\n </a>\n </p>\n </div>\n )\n}\n","import { useState, type FormEvent } from \"react\"\nimport type { ResetPasswordFormProps } from \"../types\"\n\nexport function ResetPasswordForm({\n email,\n onSuccess,\n loginUrl = \"/login\",\n authClient,\n}: ResetPasswordFormProps) {\n const [otp, setOtp] = useState(\"\")\n const [password, setPassword] = useState(\"\")\n const [confirmPassword, setConfirmPassword] = useState(\"\")\n const [error, setError] = useState<string | undefined>()\n const [isResetting, setIsResetting] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const [resendMessage, setResendMessage] = useState<string | undefined>()\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault()\n if (!otp.trim() || otp.length < 6) {\n setError(\"Please enter the 6-digit code\")\n return\n }\n if (password.length < 8) {\n setError(\"Password must be at least 8 characters\")\n return\n }\n if (password !== confirmPassword) {\n setError(\"Passwords do not match\")\n return\n }\n setError(undefined)\n setIsResetting(true)\n try {\n const res = await fetch(\"/api/auth/email-otp/reset-password\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email, otp, password }),\n })\n if (!res.ok) {\n const body = await res.json().catch(() => null)\n setError(body?.message ?? \"Failed to reset password\")\n return\n }\n onSuccess?.()\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to reset password\",\n )\n } finally {\n setIsResetting(false)\n }\n }\n\n const handleResend = async () => {\n setError(undefined)\n setResendMessage(undefined)\n setIsResending(true)\n try {\n await authClient.emailOtp.sendVerificationOtp({\n email,\n type: \"forget-password\",\n })\n setResendMessage(\"A new code has been sent to your inbox.\")\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to resend code.\")\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">\n Reset your password\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n Enter the 6-digit code sent to{\" \"}\n <span className=\"font-medium text-foreground\">{email}</span> and your\n new password.\n </p>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {error}\n </div>\n )}\n\n {resendMessage && (\n <div className=\"rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-xs text-green-700\">\n {resendMessage}\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-4\" noValidate>\n <input\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n maxLength={6}\n value={otp}\n onChange={(e) => setOtp(e.target.value.replace(/\\D/g, \"\"))}\n placeholder=\"000000\"\n required\n disabled={isResetting}\n autoComplete=\"one-time-code\"\n className=\"flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-center text-lg tracking-[0.4em] font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <input\n type=\"password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n placeholder=\"New password\"\n required\n disabled={isResetting}\n autoComplete=\"new-password\"\n className=\"flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <input\n type=\"password\"\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n placeholder=\"Confirm new password\"\n required\n disabled={isResetting}\n autoComplete=\"new-password\"\n className=\"flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n\n <button\n type=\"submit\"\n disabled={isResetting || otp.length < 6 || !password}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\"\n >\n {isResetting ? \"Resetting...\" : \"Reset password\"}\n </button>\n\n <button\n type=\"button\"\n onClick={handleResend}\n disabled={isResending}\n className=\"inline-flex h-11 w-full items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\"\n >\n {isResending ? \"Sending...\" : \"Resend code\"}\n </button>\n </form>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n Remember your password?{\" \"}\n <a\n href={loginUrl}\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n Sign in\n </a>\n </p>\n </div>\n )\n}\n","import type { AuthLayoutProps } from \"../types\"\n\nexport function AuthLayout({\n logo,\n title,\n subtitle,\n children,\n footer,\n}: AuthLayoutProps) {\n return (\n <div className=\"flex min-h-screen items-center justify-center bg-background\">\n <div className=\"w-full max-w-md\">\n <div className=\"mb-10 text-center\">\n {logo && <div className=\"mb-6 flex justify-center\">{logo}</div>}\n <h1 className=\"text-[32px] font-light tracking-tight\">{title}</h1>\n {subtitle && (\n <p className=\"mt-2 text-sm text-muted-foreground\">{subtitle}</p>\n )}\n </div>\n\n <div className=\"rounded-xl border bg-card p-8 shadow-sm\">\n {children}\n </div>\n\n {footer && (\n <div className=\"mt-6 text-center text-xs text-muted-foreground\">\n {footer}\n </div>\n )}\n </div>\n </div>\n )\n}\n","import type { InvitationNoticeProps } from \"../types\"\n\n/**\n * Every app is reachable at contact@ its own apex domain, so the address is\n * derived rather than configured. Sub-domains are stripped because the app is\n * routinely served from app./admin. while the mailbox lives on the apex;\n * multi-part public suffixes (.co.uk) would need a real suffix list and no app\n * using this is on one.\n */\nfunction defaultSupportEmail(): string | undefined {\n if (typeof window === \"undefined\") return undefined\n const host = window.location.hostname\n if (!host || host === \"localhost\" || /^[\\d.]+$/.test(host)) return undefined\n const apex = host.split(\".\").slice(-2).join(\".\")\n return `contact@${apex}`\n}\n\nconst REASON_MESSAGE: Record<string, string> = {\n expired: \"Cette invitation a expiré.\",\n claimed: \"Cette invitation a déjà été utilisée.\",\n unknown: \"Ce lien d'invitation n'est pas valide.\",\n}\n\n/**\n * What an invitee sees when their link does not work.\n *\n * It names the reason instead of merging the cases into one message: \"expired\"\n * tells someone their link was real and that asking for another one is worth\n * it, which \"invalid\" does not. Short TTLs make that distinction routine.\n *\n * The only way forward offered is a mailto, deliberately. A self-service\n * \"request a new invitation\" form would mean a public endpoint accepting dead\n * tokens, a queue to moderate, and a way to probe which tokens once existed —\n * for a flow where the operator already knows the person by name.\n */\nexport function InvitationNotice({\n reason = \"unknown\",\n supportEmail,\n title = \"Invitation indisponible\",\n action,\n}: InvitationNoticeProps) {\n const contact = supportEmail ?? defaultSupportEmail()\n\n return (\n <div className=\"space-y-8\">\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {REASON_MESSAGE[reason] ?? REASON_MESSAGE.unknown}\n </p>\n </div>\n\n {contact ? (\n <p className=\"text-sm text-muted-foreground\">\n Écrivez-nous à{\" \"}\n <a\n href={`mailto:${contact}`}\n className=\"font-medium text-foreground underline underline-offset-4 hover:text-foreground/80\"\n >\n {contact}\n </a>{\" \"}\n pour en recevoir une nouvelle.\n </p>\n ) : null}\n\n {action}\n </div>\n )\n}\n"],"mappings":"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","names":["useState","jsx","jsxs","useState","jsx","jsxs","jsx","jsxs","jsx","jsxs"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { a as InvitationFailure } from './types-DKEKNQkL.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What became of a claim, in terms the invitee can be told.
|
|
5
|
+
*
|
|
6
|
+
* 'expired' and 'claimed' are kept apart from 'unknown' because only they say
|
|
7
|
+
* the offer was real, which is what tells someone that asking for a new link is
|
|
8
|
+
* worth it rather than doubting the address they were invited at. The three
|
|
9
|
+
* failures match InvitationFailure, so an outcome feeds InvitationNotice
|
|
10
|
+
* directly.
|
|
11
|
+
*/
|
|
12
|
+
type ClaimOutcome = "granted" | InvitationFailure | "failed";
|
|
13
|
+
interface ClaimInvitationOptions {
|
|
14
|
+
/** Absolute URL of the endpoint that redeems a token. */
|
|
15
|
+
endpoint: string;
|
|
16
|
+
token: string;
|
|
17
|
+
/** The account the app just created, which the grant is attached to. */
|
|
18
|
+
externalUserId: string;
|
|
19
|
+
/** Sent as the Authorization bearer — typically the app's API key. */
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/** Merged into the request body, for backends wanting more than the token. */
|
|
22
|
+
extra?: Record<string, unknown>;
|
|
23
|
+
/** Headers merged last, so a caller can pass a cookie-based credential. */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Bounds the call so a slow API never stalls the sign-in response. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Redeems an invitation token for a user who has just signed in, turning the
|
|
30
|
+
* offer into a grant on their account.
|
|
31
|
+
*
|
|
32
|
+
* Why this belongs on the SERVER, on the auth callback rather than in the page:
|
|
33
|
+
* the invitation link lands on /register?invite=<token>, but the sign-up that
|
|
34
|
+
* follows can complete through any of three flows (password + OTP, OAuth
|
|
35
|
+
* redirect, email verification), and only two of them return to the page that
|
|
36
|
+
* held the token. Claiming where the session is established covers every flow
|
|
37
|
+
* with one code path.
|
|
38
|
+
*
|
|
39
|
+
* Best-effort by design: a sign-in must never fail because an invitation could
|
|
40
|
+
* not be redeemed. A failed claim leaves the invitation unclaimed and the user
|
|
41
|
+
* on their default tier — recoverable by following the link again, since a
|
|
42
|
+
* refused claim consumes nothing.
|
|
43
|
+
*/
|
|
44
|
+
declare function claimInvitation({ endpoint, token, externalUserId, apiKey, extra, headers, timeoutMs, }: ClaimInvitationOptions): Promise<ClaimOutcome>;
|
|
45
|
+
/**
|
|
46
|
+
* Extracts the invitation token from an auth request.
|
|
47
|
+
*
|
|
48
|
+
* The token lives on the page the invitee landed on (/register?invite=…), never
|
|
49
|
+
* on the auth endpoints themselves, so it has to be recovered from the request
|
|
50
|
+
* that completes the sign-up. Two sources, because no single one covers every
|
|
51
|
+
* flow: the URL, for the OAuth callback reached through a redirect whose query
|
|
52
|
+
* string the app controls; and the Referer, for the password and OTP flows,
|
|
53
|
+
* which are XHR calls issued BY that page and therefore carry it.
|
|
54
|
+
*
|
|
55
|
+
* Reading it per-request keeps the claim stateless: nothing associates a
|
|
56
|
+
* browser with a pending invitation, and a request with no token claims
|
|
57
|
+
* nothing. A malformed Referer is ignored rather than thrown on — it is
|
|
58
|
+
* attacker-controlled input on a best-effort path.
|
|
59
|
+
*/
|
|
60
|
+
declare function inviteTokenFrom(request: Request, param?: string): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Whether this request is the one that just created a usable account — the
|
|
63
|
+
* moment to provision, claim an invitation, or greet someone. Matching on the
|
|
64
|
+
* path rather than on a response body keeps it flow-agnostic: the three
|
|
65
|
+
* sign-up flows return three different shapes.
|
|
66
|
+
*/
|
|
67
|
+
declare function completesSignup(pathname: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Carries a failed claim to the next page. The claim happens inside an auth
|
|
70
|
+
* response nobody renders, so its result would otherwise reach only the server
|
|
71
|
+
* log — leaving an invitee on the default tier with no idea their link had
|
|
72
|
+
* lapsed. Short-lived and readable by the page, which reports it and clears it.
|
|
73
|
+
*/
|
|
74
|
+
declare function invitationOutcomeCookie(outcome: ClaimOutcome, name?: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Whether an outcome is one the invitee should be shown a reason for.
|
|
77
|
+
* 'failed' is excluded: it means the call did not complete, so the offer may
|
|
78
|
+
* still be good and telling someone their invitation is invalid would be wrong.
|
|
79
|
+
*/
|
|
80
|
+
declare function isInvitationFailure(outcome: ClaimOutcome): outcome is InvitationFailure;
|
|
81
|
+
|
|
82
|
+
export { type ClaimOutcome as C, type ClaimInvitationOptions as a, completesSignup as b, claimInvitation as c, invitationOutcomeCookie as d, inviteTokenFrom as e, isInvitationFailure as i };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Auth, BetterAuthOptions } from 'better-auth';
|
|
2
2
|
import { b as PlatformAuthConfig } from './types-DKEKNQkL.js';
|
|
3
3
|
export { f as PlatformSession, g as PlatformSessionData, h as PlatformUser } from './types-DKEKNQkL.js';
|
|
4
|
+
export { a as ClaimInvitationOptions, C as ClaimOutcome, c as claimInvitation, b as completesSignup, d as invitationOutcomeCookie, e as inviteTokenFrom, i as isInvitationFailure } from './invitation-DIZAb2BJ.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Creates a Better Auth instance with platform defaults.
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claimInvitation,
|
|
3
|
+
completesSignup,
|
|
4
|
+
invitationOutcomeCookie,
|
|
5
|
+
inviteTokenFrom,
|
|
6
|
+
isInvitationFailure
|
|
7
|
+
} from "./chunk-73BHM4PZ.js";
|
|
8
|
+
|
|
1
9
|
// src/server.ts
|
|
2
10
|
import { betterAuth, APIError } from "better-auth";
|
|
3
11
|
import { emailOTP, admin } from "better-auth/plugins";
|
|
@@ -114,6 +122,11 @@ function createPlatformAuth(config) {
|
|
|
114
122
|
});
|
|
115
123
|
}
|
|
116
124
|
export {
|
|
117
|
-
|
|
125
|
+
claimInvitation,
|
|
126
|
+
completesSignup,
|
|
127
|
+
createPlatformAuth,
|
|
128
|
+
invitationOutcomeCookie,
|
|
129
|
+
inviteTokenFrom,
|
|
130
|
+
isInvitationFailure
|
|
118
131
|
};
|
|
119
132
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin } from \"better-auth/plugins\"\nimport type { PlatformAuthConfig, PlatformAuthMailerType } from \"./types\"\n\nconst DEFAULT_EMAIL_SUBJECTS: Record<string, string> = {\n \"email-verification\": \"Verify your account\",\n \"forget-password\": \"Reset your password\",\n \"sign-in\": \"Your sign-in code\",\n}\n\nfunction defaultRenderOtpEmail(otp: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your verification code</h2>\n <p style=\"color:#555;margin-bottom:24px\">Use the code below to continue. It expires in 5 minutes.</p>\n <div style=\"background:#f5f5f5;border-radius:8px;padding:24px;text-align:center;letter-spacing:8px;font-size:32px;font-weight:700\">\n ${otp}\n </div>\n <p style=\"color:#999;font-size:12px;margin-top:24px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\n/**\n * Creates a Better Auth instance with platform defaults.\n * Each app calls this with its own config (DB, secret, providers, plugins).\n */\nexport function createPlatformAuth(\n config: PlatformAuthConfig,\n): Auth<BetterAuthOptions> {\n const {\n database,\n baseURL,\n secret,\n appName,\n mailer,\n google,\n github,\n plugins = [],\n betaMode = false,\n isInvited,\n emailSubjects,\n renderOtpEmail,\n } = config\n\n const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects }\n const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail\n\n // The concrete instance type (with email-otp/admin plugins) is widened to\n // the base Auth type so the published .d.ts stays portable (inferring the\n // full plugin type triggers TS2742 — it can't be named without a zod ref).\n // The admin() plugin's user.role field is re-exposed via module augmentation\n // below, so consumers (e.g. transcript-web me.ts) still see session.user.role.\n return betterAuth({\n database,\n baseURL,\n secret,\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n },\n // Never auto-merge a social identity into an existing account by matching\n // email. Better Auth links by default (email-verified providers are trusted),\n // so signing in with Google/GitHub on an email already registered would fold\n // that identity into the existing account. We keep each sign-in method its\n // own account: a social login on a taken email is refused, not linked.\n account: {\n accountLinking: {\n enabled: false,\n },\n },\n hooks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n before: async (ctx: any) => {\n if (!betaMode) return\n if (ctx.path !== \"/sign-up/email\") return\n const body = ctx.body as { email?: string; inviteToken?: string } | undefined\n const email = body?.email\n const inviteToken = body?.inviteToken\n if (email && inviteToken && isInvited) {\n const ok = await isInvited(email, inviteToken)\n if (ok) return\n }\n throw new APIError(\"FORBIDDEN\", {\n message: \"Registration is invite-only during the private beta.\",\n })\n },\n },\n plugins: [\n emailOTP({\n async sendVerificationOTP({ email, otp, type }) {\n const subject = subjects[type]\n ? `${subjects[type]} - ${appName}`\n : `Your ${appName} code`\n const html = renderEmail(otp, type as PlatformAuthMailerType)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: type as PlatformAuthMailerType,\n otp,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging OTP to stdout for ${email} (${type}): ${otp}`,\n )\n },\n otpLength: 6,\n expiresIn: 300,\n overrideDefaultEmailVerification: true,\n }),\n admin(),\n ...plugins, // app-specific plugins (e.g. tanstackStartCookies)\n ],\n socialProviders: {\n ...(google\n ? {\n google: {\n clientId: google.clientId,\n clientSecret: google.clientSecret,\n },\n }\n : {}),\n ...(github\n ? {\n github: {\n clientId: github.clientId,\n clientSecret: github.clientSecret,\n },\n }\n : {}),\n },\n }) as unknown as Auth<BetterAuthOptions>\n}\n\nexport type PlatformAuth = ReturnType<typeof createPlatformAuth>\n\n// Re-export the session contract from /server so consumers that import the\n// auth factory can type api.getSession() without a second import path.\nexport type {\n PlatformUser,\n PlatformSession,\n PlatformSessionData,\n} from \"./types\"\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin } from \"better-auth/plugins\"\nimport type { PlatformAuthConfig, PlatformAuthMailerType } from \"./types\"\n\nconst DEFAULT_EMAIL_SUBJECTS: Record<string, string> = {\n \"email-verification\": \"Verify your account\",\n \"forget-password\": \"Reset your password\",\n \"sign-in\": \"Your sign-in code\",\n}\n\nfunction defaultRenderOtpEmail(otp: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your verification code</h2>\n <p style=\"color:#555;margin-bottom:24px\">Use the code below to continue. It expires in 5 minutes.</p>\n <div style=\"background:#f5f5f5;border-radius:8px;padding:24px;text-align:center;letter-spacing:8px;font-size:32px;font-weight:700\">\n ${otp}\n </div>\n <p style=\"color:#999;font-size:12px;margin-top:24px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\n/**\n * Creates a Better Auth instance with platform defaults.\n * Each app calls this with its own config (DB, secret, providers, plugins).\n */\nexport function createPlatformAuth(\n config: PlatformAuthConfig,\n): Auth<BetterAuthOptions> {\n const {\n database,\n baseURL,\n secret,\n appName,\n mailer,\n google,\n github,\n plugins = [],\n betaMode = false,\n isInvited,\n emailSubjects,\n renderOtpEmail,\n } = config\n\n const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects }\n const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail\n\n // The concrete instance type (with email-otp/admin plugins) is widened to\n // the base Auth type so the published .d.ts stays portable (inferring the\n // full plugin type triggers TS2742 — it can't be named without a zod ref).\n // The admin() plugin's user.role field is re-exposed via module augmentation\n // below, so consumers (e.g. transcript-web me.ts) still see session.user.role.\n return betterAuth({\n database,\n baseURL,\n secret,\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n },\n // Never auto-merge a social identity into an existing account by matching\n // email. Better Auth links by default (email-verified providers are trusted),\n // so signing in with Google/GitHub on an email already registered would fold\n // that identity into the existing account. We keep each sign-in method its\n // own account: a social login on a taken email is refused, not linked.\n account: {\n accountLinking: {\n enabled: false,\n },\n },\n hooks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n before: async (ctx: any) => {\n if (!betaMode) return\n if (ctx.path !== \"/sign-up/email\") return\n const body = ctx.body as { email?: string; inviteToken?: string } | undefined\n const email = body?.email\n const inviteToken = body?.inviteToken\n if (email && inviteToken && isInvited) {\n const ok = await isInvited(email, inviteToken)\n if (ok) return\n }\n throw new APIError(\"FORBIDDEN\", {\n message: \"Registration is invite-only during the private beta.\",\n })\n },\n },\n plugins: [\n emailOTP({\n async sendVerificationOTP({ email, otp, type }) {\n const subject = subjects[type]\n ? `${subjects[type]} - ${appName}`\n : `Your ${appName} code`\n const html = renderEmail(otp, type as PlatformAuthMailerType)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: type as PlatformAuthMailerType,\n otp,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging OTP to stdout for ${email} (${type}): ${otp}`,\n )\n },\n otpLength: 6,\n expiresIn: 300,\n overrideDefaultEmailVerification: true,\n }),\n admin(),\n ...plugins, // app-specific plugins (e.g. tanstackStartCookies)\n ],\n socialProviders: {\n ...(google\n ? {\n google: {\n clientId: google.clientId,\n clientSecret: google.clientSecret,\n },\n }\n : {}),\n ...(github\n ? {\n github: {\n clientId: github.clientId,\n clientSecret: github.clientSecret,\n },\n }\n : {}),\n },\n }) as unknown as Auth<BetterAuthOptions>\n}\n\nexport type PlatformAuth = ReturnType<typeof createPlatformAuth>\n\n// Re-export the session contract from /server so consumers that import the\n// auth factory can type api.getSession() without a second import path.\nexport type {\n PlatformUser,\n PlatformSession,\n PlatformSessionData,\n} from \"./types\"\n\n// Invitation claiming runs on the auth callback, where the session is\n// established — the one place every sign-up flow passes through.\nexport {\n claimInvitation,\n completesSignup,\n invitationOutcomeCookie,\n inviteTokenFrom,\n isInvitationFailure,\n} from \"./invitation\"\nexport type { ClaimOutcome, ClaimInvitationOptions } from \"./invitation\"\n"],"mappings":";;;;;;;;;AAAA,SAAS,YAAY,gBAAmD;AACxE,SAAS,UAAU,aAAa;AAGhC,IAAM,yBAAiD;AAAA,EACrD,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,WAAW;AACb;AAEA,SAAS,sBAAsB,KAAqB;AAClD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,oBAKW,GAAG;AAAA;AAAA;AAAA;AAAA;AAKvB;AAMO,SAAS,mBACd,QACyB;AACzB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,CAAC;AAAA,IACX,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,WAAW,EAAE,GAAG,wBAAwB,GAAG,cAAc;AAC/D,QAAM,cAAc,kBAAkB;AAOtC,SAAO,WAAW;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA,kBAAkB;AAAA,MAChB,SAAS;AAAA,MACT,0BAA0B;AAAA,IAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,SAAS;AAAA,MACP,gBAAgB;AAAA,QACd,SAAS;AAAA,MACX;AAAA,IACF;AAAA,IACA,OAAO;AAAA;AAAA,MAEL,QAAQ,OAAO,QAAa;AAC1B,YAAI,CAAC,SAAU;AACf,YAAI,IAAI,SAAS,iBAAkB;AACnC,cAAM,OAAO,IAAI;AACjB,cAAM,QAAQ,MAAM;AACpB,cAAM,cAAc,MAAM;AAC1B,YAAI,SAAS,eAAe,WAAW;AACrC,gBAAM,KAAK,MAAM,UAAU,OAAO,WAAW;AAC7C,cAAI,GAAI;AAAA,QACV;AACA,cAAM,IAAI,SAAS,aAAa;AAAA,UAC9B,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,SAAS;AAAA,QACP,MAAM,oBAAoB,EAAE,OAAO,KAAK,KAAK,GAAG;AAC9C,gBAAM,UAAU,SAAS,IAAI,IACzB,GAAG,SAAS,IAAI,CAAC,MAAM,OAAO,KAC9B,QAAQ,OAAO;AACnB,gBAAM,OAAO,YAAY,KAAK,IAA8B;AAE5D,cAAI,QAAQ;AACV,kBAAM,OAAO;AAAA,cACX,IAAI;AAAA,cACJ;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YACF,CAAC;AACD;AAAA,UACF;AAEA,kBAAQ;AAAA,YACN,iEAA4D,KAAK,KAAK,IAAI,MAAM,GAAG;AAAA,UACrF;AAAA,QACF;AAAA,QACA,WAAW;AAAA,QACX,WAAW;AAAA,QACX,kCAAkC;AAAA,MACpC,CAAC;AAAA,MACD,MAAM;AAAA,MACN,GAAG;AAAA;AAAA,IACL;AAAA,IACA,iBAAiB;AAAA,MACf,GAAI,SACA;AAAA,QACE,QAAQ;AAAA,UACN,UAAU,OAAO;AAAA,UACjB,cAAc,OAAO;AAAA,QACvB;AAAA,MACF,IACA,CAAC;AAAA,MACL,GAAI,SACA;AAAA,QACE,QAAQ;AAAA,UACN,UAAU,OAAO;AAAA,UACjB,cAAc,OAAO;AAAA,QACvB;AAAA,MACF,IACA,CAAC;AAAA,IACP;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lalternative/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Shared Better Auth wrapper for L'Alternative apps (server + React client + auth UI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup",
|
|
27
27
|
"dev": "tsup --watch",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "pnpm build"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"better-auth": ">=1.4.0",
|