@lalternative/auth 0.5.0 → 0.7.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/dist/{chunk-73BHM4PZ.js → chunk-6URDBMQY.js} +29 -1
- package/dist/chunk-6URDBMQY.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +19 -13
- package/dist/index.js +417 -245
- package/dist/index.js.map +1 -1
- package/dist/{invitation-BpowqA66.d.ts → invitation-BUqM_BF8.d.ts} +49 -10
- package/dist/server.d.ts +3 -3
- package/dist/server.js +9 -3
- package/dist/server.js.map +1 -1
- package/dist/{types-BEldQPou.d.ts → types-SieiPwdd.d.ts} +87 -11
- package/package.json +1 -1
- package/dist/chunk-73BHM4PZ.js.map +0 -1
|
@@ -37,6 +37,8 @@ async function claimInvitation({
|
|
|
37
37
|
function inviteTokenFrom(request, param = "invite") {
|
|
38
38
|
const direct = new URL(request.url).searchParams.get(param);
|
|
39
39
|
if (direct && direct.trim() !== "") return direct;
|
|
40
|
+
const held = inviteTokenCookie(request);
|
|
41
|
+
if (held) return held;
|
|
40
42
|
const referer = request.headers.get("referer");
|
|
41
43
|
if (!referer) return null;
|
|
42
44
|
try {
|
|
@@ -46,6 +48,29 @@ function inviteTokenFrom(request, param = "invite") {
|
|
|
46
48
|
return null;
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
var INVITE_TOKEN_COOKIE = "invite_token";
|
|
52
|
+
function pinInviteToken(request, param = "invite") {
|
|
53
|
+
if (inviteTokenCookie(request)) return null;
|
|
54
|
+
const token = inviteTokenFrom(request, param);
|
|
55
|
+
return token ? holdInviteTokenCookie(token) : null;
|
|
56
|
+
}
|
|
57
|
+
function holdInviteTokenCookie(token, maxAgeSeconds = 1800) {
|
|
58
|
+
return `${INVITE_TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; Max-Age=${maxAgeSeconds}; HttpOnly; SameSite=Lax`;
|
|
59
|
+
}
|
|
60
|
+
function releaseInviteTokenCookie() {
|
|
61
|
+
return `${INVITE_TOKEN_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`;
|
|
62
|
+
}
|
|
63
|
+
function inviteTokenCookie(request) {
|
|
64
|
+
const header = request.headers.get("cookie");
|
|
65
|
+
if (!header) return null;
|
|
66
|
+
for (const part of header.split(";")) {
|
|
67
|
+
const [name, ...rest] = part.trim().split("=");
|
|
68
|
+
if (name !== INVITE_TOKEN_COOKIE) continue;
|
|
69
|
+
const value = decodeURIComponent(rest.join("=")).trim();
|
|
70
|
+
return value === "" ? null : value;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
49
74
|
var SIGNUP_COMPLETING = [
|
|
50
75
|
"/sign-up/email",
|
|
51
76
|
"/sign-in/email-otp",
|
|
@@ -65,8 +90,11 @@ function isInvitationFailure(outcome) {
|
|
|
65
90
|
export {
|
|
66
91
|
claimInvitation,
|
|
67
92
|
inviteTokenFrom,
|
|
93
|
+
pinInviteToken,
|
|
94
|
+
holdInviteTokenCookie,
|
|
95
|
+
releaseInviteTokenCookie,
|
|
68
96
|
completesSignup,
|
|
69
97
|
invitationOutcomeCookie,
|
|
70
98
|
isInvitationFailure
|
|
71
99
|
};
|
|
72
|
-
//# sourceMappingURL=chunk-
|
|
100
|
+
//# sourceMappingURL=chunk-6URDBMQY.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. Three sources, in order of trust:\n *\n * - the URL, for a callback reached through a redirect whose query string\n * the app controls;\n * - the cookie held since the link was opened, which is the only one that\n * survives an OAuth round trip or an OTP screen that never carried the\n * token (see holdInviteTokenCookie);\n * - the Referer, for the password flow, whose XHR is issued BY the page\n * holding it.\n *\n * The Referer stays last and stays supported: it covers a caller that never\n * held the cookie. It is attacker-controlled input on a best-effort path, so a\n * malformed one is ignored rather than thrown on.\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 held = inviteTokenCookie(request)\n if (held) return held\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/** Name of the cookie holding the token between the link and the claim. */\nconst INVITE_TOKEN_COOKIE = \"invite_token\"\n\n/**\n * Pins the token onto the browser the first time a request carries it, so the\n * rest of the sign-up can find it.\n *\n * Called on every auth request, not only the ones completing a sign-up: the\n * token is legible on the FIRST call of a flow (the page holding it issues that\n * XHR, so the Referer still has it) and gone by the last (verified from a\n * screen that never held it, or returned from Google). Waiting for the moment\n * the account exists is waiting one request too long.\n *\n * Returns null when there is nothing to pin — no token in the request, or one\n * already held — so a caller can skip the Set-Cookie entirely.\n */\nexport function pinInviteToken(\n request: Request,\n param = \"invite\",\n): string | null {\n if (inviteTokenCookie(request)) return null\n const token = inviteTokenFrom(request, param)\n return token ? holdInviteTokenCookie(token) : null\n}\n\n/**\n * Holds the token from the moment the link is opened until the account exists.\n *\n * The URL and the Referer each cover only part of the ground: the OTP flow\n * verifies from a screen that never carried the token, and an OAuth sign-up\n * comes back from Google with no Referer of ours at all. Both lose it, and the\n * invitee lands on the default tier with the offer still pending.\n *\n * SameSite=Lax rather than Strict: the return from Google is a cross-site\n * top-level navigation, which Strict would refuse — the one case this exists\n * for. HttpOnly because the page has no reason to read it, and short-lived\n * because signing up takes minutes: a single-use invitation has no business\n * sitting in a browser for longer.\n */\nexport function holdInviteTokenCookie(\n token: string,\n maxAgeSeconds = 1800,\n): string {\n return `${INVITE_TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; Max-Age=${maxAgeSeconds}; HttpOnly; SameSite=Lax`\n}\n\n/**\n * Clears the held token. Sent once the claim has been attempted: the token is\n * single-use, so keeping it would only replay a call that can no longer\n * succeed.\n */\nexport function releaseInviteTokenCookie(): string {\n return `${INVITE_TOKEN_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`\n}\n\nfunction inviteTokenCookie(request: Request): string | null {\n const header = request.headers.get(\"cookie\")\n if (!header) return null\n for (const part of header.split(\";\")) {\n const [name, ...rest] = part.trim().split(\"=\")\n if (name !== INVITE_TOKEN_COOKIE) continue\n const value = decodeURIComponent(rest.join(\"=\")).trim()\n return value === \"\" ? null : value\n }\n return null\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;AAqBO,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,OAAO,kBAAkB,OAAO;AACtC,MAAI,KAAM,QAAO;AAEjB,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;AAGA,IAAM,sBAAsB;AAerB,SAAS,eACd,SACA,QAAQ,UACO;AACf,MAAI,kBAAkB,OAAO,EAAG,QAAO;AACvC,QAAM,QAAQ,gBAAgB,SAAS,KAAK;AAC5C,SAAO,QAAQ,sBAAsB,KAAK,IAAI;AAChD;AAgBO,SAAS,sBACd,OACA,gBAAgB,MACR;AACR,SAAO,GAAG,mBAAmB,IAAI,mBAAmB,KAAK,CAAC,qBAAqB,aAAa;AAC9F;AAOO,SAAS,2BAAmC;AACjD,SAAO,GAAG,mBAAmB;AAC/B;AAEA,SAAS,kBAAkB,SAAiC;AAC1D,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,CAAC,MAAM,GAAG,IAAI,IAAI,KAAK,KAAK,EAAE,MAAM,GAAG;AAC7C,QAAI,SAAS,oBAAqB;AAClC,UAAM,QAAQ,mBAAmB,KAAK,KAAK,GAAG,CAAC,EAAE,KAAK;AACtD,WAAO,UAAU,KAAK,OAAO;AAAA,EAC/B;AACA,SAAO;AACT;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
CHANGED
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 } from './types-
|
|
2
|
-
export { b as InvitationFailure, c as LoginFormLabels, P as PlatformAuthClientConfig, d as PlatformAuthConfig, e as PlatformAuthMailer, f as PlatformAuthMailerArgs, g as PlatformAuthMailerType, h as PlatformSession, i as PlatformSessionData, j as PlatformUser, k as RegisterFormLabels } from './types-
|
|
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-SieiPwdd.js';
|
|
2
|
+
export { b as InvitationFailure, c as LoginFormLabels, P as PlatformAuthClientConfig, d as PlatformAuthConfig, e as PlatformAuthMailer, f as PlatformAuthMailerArgs, g as PlatformAuthMailerType, h as PlatformSession, i as PlatformSessionData, j as PlatformUser, k as RegisterFormLabels } from './types-SieiPwdd.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-
|
|
8
|
+
export { C as ClaimOutcome, i as isInvitationFailure } from './invitation-BUqM_BF8.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Returns a useSession hook bound to the given auth client.
|
|
@@ -90,9 +90,9 @@ interface AuthSubmitProps {
|
|
|
90
90
|
*/
|
|
91
91
|
declare function AuthSubmit({ pending, disabled, pendingLabel, children, }: AuthSubmitProps): react.JSX.Element;
|
|
92
92
|
|
|
93
|
-
declare function LoginForm({ onSuccess, registerUrl, forgotPasswordUrl, socialCallbackUrl, socialProviders, coreTokenUrl, labels, authClient, }: LoginFormProps): react.JSX.Element;
|
|
93
|
+
declare function LoginForm({ onSuccess, registerUrl, forgotPasswordUrl, socialCallbackUrl, socialProviders, coreTokenUrl, labels, titleClassName, error: externalError, authClient, }: LoginFormProps): react.JSX.Element;
|
|
94
94
|
|
|
95
|
-
declare function RegisterForm({ onSuccess, loginUrl, legal, socialCallbackUrl, socialProviders, labels, authClient, }: RegisterFormProps): react.JSX.Element;
|
|
95
|
+
declare function RegisterForm({ onSuccess, loginUrl, legal, socialCallbackUrl, socialProviders, labels, titleClassName, error: externalError, authClient, }: RegisterFormProps): react.JSX.Element;
|
|
96
96
|
|
|
97
97
|
interface SocialButtonsProps {
|
|
98
98
|
providers: Array<"google" | "github">;
|
|
@@ -105,27 +105,33 @@ interface SocialButtonsProps {
|
|
|
105
105
|
}
|
|
106
106
|
declare function SocialButtons({ providers, onSelect, disabled, separator, labels, }: SocialButtonsProps): react.JSX.Element | null;
|
|
107
107
|
|
|
108
|
-
declare function VerifyEmailForm({ email, onSuccess, authClient, }: VerifyEmailFormProps): react.JSX.Element;
|
|
108
|
+
declare function VerifyEmailForm({ email, onSuccess, loginUrl, labels, titleClassName, authClient, }: VerifyEmailFormProps): react.JSX.Element;
|
|
109
109
|
|
|
110
|
-
declare function ForgotPasswordForm({ onSuccess, loginUrl, authClient, }: ForgotPasswordFormProps): react.JSX.Element;
|
|
110
|
+
declare function ForgotPasswordForm({ onSuccess, loginUrl, labels, titleClassName, authClient, }: ForgotPasswordFormProps): react.JSX.Element;
|
|
111
111
|
|
|
112
|
-
declare function ResetPasswordForm({ email, onSuccess, loginUrl, authClient, }: ResetPasswordFormProps): react.JSX.Element;
|
|
112
|
+
declare function ResetPasswordForm({ email, onSuccess, loginUrl, labels, titleClassName, authClient, }: ResetPasswordFormProps): react.JSX.Element;
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* The frame every auth screen sits in.
|
|
115
|
+
* The frame every auth screen sits in. It owns the ground, the card and the
|
|
116
|
+
* app's own marks (logo, illustration, legal footer); the form it wraps owns
|
|
117
|
+
* its title and its fields.
|
|
116
118
|
*
|
|
117
119
|
* Mobile and desktop are two layouts, not one scaled down. On a phone the form
|
|
118
120
|
* IS the page: no card, no border, edge-to-edge padding, top-aligned so the
|
|
119
121
|
* fields stay above the keyboard instead of being pushed under it by vertical
|
|
120
122
|
* centering. From sm up it becomes a bounded card on a tinted ground — a
|
|
121
|
-
* full-width form on a 1440px display is unreadable.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
123
|
+
* full-width form on a 1440px display is unreadable.
|
|
124
|
+
*
|
|
125
|
+
* With a `panel` the card splits in two from md up, the form on the left and
|
|
126
|
+
* the illustration on the right. The panel is dropped below that width rather
|
|
127
|
+
* than stacked: a decorative half-screen above a form costs a full swipe
|
|
128
|
+
* before the first field. Without a `panel` the card stays the single 420px
|
|
129
|
+
* column it has always been.
|
|
124
130
|
*
|
|
125
131
|
* min-h-dvh rather than min-h-screen: on mobile browsers 100vh includes the
|
|
126
132
|
* retracting URL bar, so a screen-height container overflows by its height.
|
|
127
133
|
*/
|
|
128
|
-
declare function AuthLayout({ logo,
|
|
134
|
+
declare function AuthLayout({ logo, panel, children, footer, }: AuthLayoutProps): react.JSX.Element;
|
|
129
135
|
|
|
130
136
|
/**
|
|
131
137
|
* What an invitee sees when their link does not work.
|