@lalternative/auth 0.4.0 → 0.4.2
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 +3 -2
- package/dist/chunk-73BHM4PZ.js +72 -0
- package/dist/chunk-73BHM4PZ.js.map +1 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +13 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +778 -0
- package/dist/index.js.map +1 -0
- package/dist/invitation-CzfLna7q.d.ts +82 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +132 -0
- package/dist/server.js.map +1 -0
- package/dist/types-DxllDyOa.d.ts +196 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Shared [Better Auth](https://better-auth.com) wrapper for L'Alternative apps.
|
|
4
4
|
|
|
5
5
|
Provides platform auth defaults (email-OTP + admin plugins), a React client,
|
|
6
|
-
and the auth UI forms (verify-email, forgot/reset password,
|
|
6
|
+
and the auth UI forms (login, register, verify-email, forgot/reset password,
|
|
7
|
+
auth layout).
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -32,7 +33,7 @@ export const authClient = createPlatformAuthClient({ baseURL })
|
|
|
32
33
|
|
|
33
34
|
```tsx
|
|
34
35
|
// UI + hooks
|
|
35
|
-
import { VerifyEmailForm, ForgotPasswordForm, ResetPasswordForm, AuthLayout, useSession, useLogout } from "@lalternative/auth"
|
|
36
|
+
import { LoginForm, RegisterForm, SocialButtons, VerifyEmailForm, ForgotPasswordForm, ResetPasswordForm, AuthLayout, useSession, useLogout } from "@lalternative/auth"
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
### Invitations
|
|
@@ -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/client.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as better_auth_react from 'better-auth/react';
|
|
2
|
+
import { P as PlatformAuthClientConfig } from './types-DxllDyOa.js';
|
|
3
|
+
import 'better-auth';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Better Auth client for React usage.
|
|
7
|
+
* Provides useSession() hook and other React-integrated methods.
|
|
8
|
+
*/
|
|
9
|
+
declare function createPlatformAuthClient(config?: PlatformAuthClientConfig): better_auth_react.ReactAuthClient<{
|
|
10
|
+
baseURL: string;
|
|
11
|
+
plugins: any[];
|
|
12
|
+
}>;
|
|
13
|
+
type PlatformAuthClient = ReturnType<typeof createPlatformAuthClient>;
|
|
14
|
+
|
|
15
|
+
export { type PlatformAuthClient, createPlatformAuthClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { createAuthClient } from "better-auth/react";
|
|
3
|
+
import { emailOTPClient, adminClient } from "better-auth/client/plugins";
|
|
4
|
+
function createPlatformAuthClient(config) {
|
|
5
|
+
return createAuthClient({
|
|
6
|
+
baseURL: config?.baseURL ?? (typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"),
|
|
7
|
+
plugins: [emailOTPClient(), adminClient(), ...config?.plugins ?? []]
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
createPlatformAuthClient
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +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 { PlatformAuthClientConfig } from \"./types\"\n\n/**\n * Creates a Better Auth client for React usage.\n * Provides useSession() hook and other React-integrated methods.\n */\nexport function createPlatformAuthClient(config?: PlatformAuthClientConfig) {\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 })\n}\n\nexport type PlatformAuthClient = ReturnType<typeof createPlatformAuthClient>\n"],"mappings":";AAAA,SAAS,wBAAwB;AACjC,SAAS,gBAAgB,mBAAmB;AAOrC,SAAS,yBAAyB,QAAmC;AAC1E,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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { L as LoginFormProps, R as RegisterFormProps, V as VerifyEmailFormProps, F as ForgotPasswordFormProps, a as ResetPasswordFormProps, A as AuthLayoutProps, I as InvitationNoticeProps } from './types-DxllDyOa.js';
|
|
2
|
+
export { b as InvitationFailure, P as PlatformAuthClientConfig, c as PlatformAuthConfig, d as PlatformAuthMailer, e as PlatformAuthMailerArgs, f as PlatformAuthMailerType, g as PlatformSession, h as PlatformSessionData, i as PlatformUser } from './types-DxllDyOa.js';
|
|
3
|
+
import * as better_auth_react from 'better-auth/react';
|
|
4
|
+
import * as better_auth from 'better-auth';
|
|
5
|
+
import { PlatformAuthClient } from './client.js';
|
|
6
|
+
import * as react from 'react';
|
|
7
|
+
export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-CzfLna7q.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns a useSession hook bound to the given auth client.
|
|
11
|
+
* Usage: const { data: session, isPending } = useSession(authClient)
|
|
12
|
+
*/
|
|
13
|
+
declare function useSession(authClient: PlatformAuthClient): {
|
|
14
|
+
data: {
|
|
15
|
+
user: better_auth.StripEmptyObjects<{
|
|
16
|
+
id: string;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
updatedAt: Date;
|
|
19
|
+
email: string;
|
|
20
|
+
emailVerified: boolean;
|
|
21
|
+
name: string;
|
|
22
|
+
image?: string | null | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
session: better_auth.StripEmptyObjects<{
|
|
25
|
+
id: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
userId: string;
|
|
29
|
+
expiresAt: Date;
|
|
30
|
+
token: string;
|
|
31
|
+
ipAddress?: string | null | undefined;
|
|
32
|
+
userAgent?: string | null | undefined;
|
|
33
|
+
}>;
|
|
34
|
+
} | null;
|
|
35
|
+
isPending: boolean;
|
|
36
|
+
isRefetching: boolean;
|
|
37
|
+
error: better_auth_react.BetterFetchError | null;
|
|
38
|
+
refetch: (queryParams?: {
|
|
39
|
+
query?: better_auth.SessionQueryParams;
|
|
40
|
+
} | undefined) => Promise<void>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Returns a logout function bound to the given auth client.
|
|
44
|
+
* Usage: const logout = useLogout(authClient)
|
|
45
|
+
*/
|
|
46
|
+
declare function useLogout(authClient: PlatformAuthClient): () => Promise<void>;
|
|
47
|
+
|
|
48
|
+
declare function LoginForm({ onSuccess, registerUrl, forgotPasswordUrl, socialCallbackUrl, socialProviders, coreTokenUrl, authClient, }: LoginFormProps): react.JSX.Element;
|
|
49
|
+
|
|
50
|
+
declare function RegisterForm({ onSuccess, loginUrl, legal, socialCallbackUrl, socialProviders, authClient, }: RegisterFormProps): react.JSX.Element;
|
|
51
|
+
|
|
52
|
+
interface SocialButtonsProps {
|
|
53
|
+
providers: Array<"google" | "github">;
|
|
54
|
+
onSelect: (provider: "google" | "github") => void | Promise<void>;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
}
|
|
57
|
+
declare function SocialButtons({ providers, onSelect, disabled, }: SocialButtonsProps): react.JSX.Element | null;
|
|
58
|
+
|
|
59
|
+
declare function VerifyEmailForm({ email, onSuccess, authClient, }: VerifyEmailFormProps): react.JSX.Element;
|
|
60
|
+
|
|
61
|
+
declare function ForgotPasswordForm({ onSuccess, loginUrl, authClient, }: ForgotPasswordFormProps): react.JSX.Element;
|
|
62
|
+
|
|
63
|
+
declare function ResetPasswordForm({ email, onSuccess, loginUrl, authClient, }: ResetPasswordFormProps): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
declare function AuthLayout({ logo, title, subtitle, children, footer, }: AuthLayoutProps): react.JSX.Element;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What an invitee sees when their link does not work.
|
|
69
|
+
*
|
|
70
|
+
* It names the reason instead of merging the cases into one message: "expired"
|
|
71
|
+
* tells someone their link was real and that asking for another one is worth
|
|
72
|
+
* it, which "invalid" does not. Short TTLs make that distinction routine.
|
|
73
|
+
*
|
|
74
|
+
* The only way forward offered is a mailto, deliberately. A self-service
|
|
75
|
+
* "request a new invitation" form would mean a public endpoint accepting dead
|
|
76
|
+
* tokens, a queue to moderate, and a way to probe which tokens once existed —
|
|
77
|
+
* for a flow where the operator already knows the person by name.
|
|
78
|
+
*/
|
|
79
|
+
declare function InvitationNotice({ reason, supportEmail, title, action, }: InvitationNoticeProps): react.JSX.Element;
|
|
80
|
+
|
|
81
|
+
export { AuthLayout, AuthLayoutProps, ForgotPasswordForm, ForgotPasswordFormProps, InvitationNotice, InvitationNoticeProps, LoginForm, LoginFormProps, RegisterForm, RegisterFormProps, ResetPasswordForm, ResetPasswordFormProps, SocialButtons, VerifyEmailForm, VerifyEmailFormProps, useLogout, useSession };
|