@astralarium/repo-auth 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,3 +9,52 @@ App wiring:
9
9
  - `/api/auth/$` responds via `serveAuthRequest`.
10
10
  - Provider credentials: `AUTH_<ID>_ID`, `AUTH_<ID>_SECRET`.
11
11
  Redirect URI: `/api/auth/callback/<id>`.
12
+
13
+ Dev sign-in:
14
+
15
+ ```ts
16
+ export const webAuthApp = {
17
+ ...app,
18
+ devSignIn: import.meta.env.DEV
19
+ ? devProviderSignIn({
20
+ provider: "google",
21
+ accountId: z.string().regex(/^\d+$/),
22
+ newUser: (id) => ({ name: `Dev ${id}`, email: `${id}@dev.invalid` }),
23
+ })
24
+ : undefined,
25
+ };
26
+
27
+ // dev sign-in page
28
+ await signInWithDevAccount(authClient, accountId);
29
+ ```
30
+
31
+ TanStack Start:
32
+
33
+ ```ts
34
+ export const fetchViewer = createServerFn({ method: "GET" }).handler(
35
+ viewerSession(withAuth),
36
+ );
37
+ const db = dbMiddleware(
38
+ createServerOnlyFn(() => databaseConnectionString(env)),
39
+ );
40
+ export const authed = authedMiddleware(sessionMiddleware(fetchViewer));
41
+
42
+ export const saveDraft = createServerFn({ method: "POST" })
43
+ .middleware([authed, db])
44
+ .handler(({ context: { session, sql } }) =>
45
+ saveDraftOf(sql, session.user.id),
46
+ );
47
+
48
+ export const Route = createFileRoute("/_app")({
49
+ beforeLoad: viewerRouteContext(fetchViewer),
50
+ });
51
+
52
+ // router `hydrate`
53
+ watchViewerRouter(router, queryClient, "/_app");
54
+
55
+ // client
56
+ saveDraft().catch((error) => {
57
+ if (isUnauthenticated(error)) openSignIn();
58
+ else throw error;
59
+ });
60
+ ```
package/dist/client.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /** Browser half of shell-auth: the session store, auth calls and OAuth popups. */
2
1
  import type { BetterAuthOptions } from "better-auth";
3
2
  import type { SessionLike } from "./types.ts";
4
3
  /** Opens `url`, a page starting a provider's OAuth redirect, in a popup
@@ -29,8 +28,8 @@ interface SessionStore<T> {
29
28
  get: () => T;
30
29
  listen: (listener: () => void) => () => void;
31
30
  }
32
- /** What `watchRouteViewer` reads of a router: TanStack Router's load event
33
- * and its matched routes. */
31
+ /** What `watchViewerRouter` reads of a router: TanStack Router's load
32
+ * event, its matched routes, and invalidation. */
34
33
  export interface ViewerRouter {
35
34
  subscribe(event: "onLoad", fn: () => void): () => void;
36
35
  state: {
@@ -39,14 +38,33 @@ export interface ViewerRouter {
39
38
  context: unknown;
40
39
  }[];
41
40
  };
41
+ invalidate(): Promise<unknown>;
42
42
  }
43
- /** The route whose pages read the session, and its session. */
44
- export interface ViewerRoute<Session> {
45
- /** Id of the layout route above every page reading the session. */
46
- routeId: string;
47
- /** The session in the route's context: `null` signed out, `undefined`
48
- * on a page carrying none. */
49
- select: (context: unknown) => Session | null | undefined;
43
+ /** What `watchViewerRouter` reads of a TanStack Query query. */
44
+ interface ViewerQuery {
45
+ state: {
46
+ status: string;
47
+ fetchStatus: string;
48
+ };
49
+ reset(): void;
50
+ }
51
+ /** What `watchViewerRouter` reads of a TanStack Query client: its cached
52
+ * queries and refetch of the active ones. */
53
+ export interface ViewerQueryClient {
54
+ getQueryCache(): {
55
+ getAll(): readonly ViewerQuery[];
56
+ };
57
+ refetchQueries(filters: {
58
+ type: "active";
59
+ predicate: (query: ViewerQuery) => boolean;
60
+ }): Promise<unknown>;
61
+ }
62
+ /** Context of the layout route carrying the viewer's session
63
+ * (`viewerRouteContext`). */
64
+ export interface ViewerContext<Session> {
65
+ /** `null` signed out; `undefined` on a page reached by client
66
+ * navigation. */
67
+ session: Session | null | undefined;
50
68
  }
51
69
  /** Shared live session store and auth operations for `Auth`. */
52
70
  export declare function createShellAuthClient<Auth extends ShellAuthLike>(): {
@@ -2142,16 +2160,14 @@ export declare function createShellAuthClient<Auth extends ShellAuthLike>(): {
2142
2160
  }>;
2143
2161
  refreshSession: () => Promise<void>;
2144
2162
  watchSession: (session: Auth["$Infer"]["Session"] | null | undefined, onChange: () => void) => () => void;
2145
- watchRouteViewer: (router: ViewerRouter, { routeId, select }: ViewerRoute<Auth["$Infer"]["Session"]>, onChange: () => void) => () => void;
2146
- signInWithProvider: (provider: string, callbackURL: string) => Promise<string | undefined>;
2147
- signUpOrInWithEmail: (account: {
2148
- email: string;
2149
- password: string;
2150
- name: string;
2163
+ watchViewerRouter: (router: ViewerRouter, queryClient: ViewerQueryClient, routeId: string) => () => void;
2164
+ post: (path: string, body: Record<string, string>, { form }?: {
2165
+ form?: boolean | undefined;
2151
2166
  }) => Promise<{
2152
2167
  message?: string | undefined;
2153
2168
  status: number;
2154
2169
  statusText: string;
2155
2170
  } | undefined>;
2171
+ signInWithProvider: (provider: string, callbackURL: string) => Promise<string | undefined>;
2156
2172
  };
2157
2173
  export {};
package/dist/client.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** Browser half of shell-auth: the session store, auth calls and OAuth popups. */
2
+ import { log } from "@astralarium/repo-telemetry";
1
3
  import { getGlobalBroadcastChannel } from "better-auth/client";
2
4
  import { InferServerPlugin } from "better-auth/client/plugins";
3
5
  import { createAuthClient } from "better-auth/react";
@@ -124,32 +126,55 @@ export function createShellAuthClient() {
124
126
  });
125
127
  }
126
128
  /**
127
- * Watches the signed-in user from the first loaded page under `routeId`;
128
- * other pages read no session. A server-rendered page's session seeds
129
- * the watch; one reached by client navigation carries none, and the
130
- * store's first read sets the user. Calls `onChange` on each user change
131
- * after. Returns the unsubscribe.
129
+ * Watches the signed-in user from the first loaded page under `routeId`,
130
+ * the layout route carrying `viewerRouteContext`; other pages read no
131
+ * session. A server-rendered page's session seeds the watch; one reached
132
+ * by client navigation carries none, and the store's first read sets the
133
+ * user. Each user change after clears `queryClient`'s queries and
134
+ * invalidates `router`, both holding the previous user's data; the
135
+ * route loaders refetch the queries they read, then the active queries
136
+ * still unread refetch: one fetch per query. Returns the unsubscribe.
132
137
  */
133
- function watchRouteViewer(router, {
134
- routeId,
135
- select
136
- }, onChange) {
138
+ function watchViewerRouter(router, queryClient, routeId) {
137
139
  let stop = router.subscribe("onLoad", () => {
138
140
  const match = router.state.matches.find(route => route.routeId === routeId);
139
141
  if (!match) return;
140
142
  stop();
141
- stop = watchSession(select(match.context), onChange);
143
+ const {
144
+ session
145
+ } = match.context;
146
+ stop = watchSession(session, () => {
147
+ for (const query of queryClient.getQueryCache().getAll()) query.reset();
148
+ const failed = exception => log.warn("auth.viewer_reload_failed", "Reloading the page for a new viewer failed", {
149
+ exception
150
+ });
151
+ void router.invalidate().catch(failed).then(() => queryClient.refetchQueries({
152
+ type: "active",
153
+ predicate: ({
154
+ state
155
+ }) => state.status === "pending" && state.fetchStatus === "idle"
156
+ })).catch(failed);
157
+ });
142
158
  });
143
159
  return () => stop();
144
160
  }
145
- /** POSTs `body` to the auth endpoint at `path`; resolves to its error.
146
- * Endpoint methods (`signIn.social`) type per app, unresolved here. */
147
- async function post(path, body) {
161
+ /** POSTs `body` to the auth endpoint at `path`, a form with `form`;
162
+ * resolves to its error. Endpoint methods (`signIn.social`) type per
163
+ * app, unresolved here; plugin endpoints (`devProviderSignIn`) call
164
+ * through it. */
165
+ async function post(path, body, {
166
+ form = false
167
+ } = {}) {
148
168
  const {
149
169
  error
150
170
  } = await authClient.$fetch(path, {
151
171
  method: "POST",
152
- body
172
+ body,
173
+ ...(form && {
174
+ headers: {
175
+ "content-type": "application/x-www-form-urlencoded"
176
+ }
177
+ })
153
178
  });
154
179
  return error ?? undefined;
155
180
  }
@@ -166,29 +191,13 @@ export function createShellAuthClient() {
166
191
  return cause instanceof Error ? cause.message : String(cause);
167
192
  }
168
193
  }
169
- /** Signs up an email account, or into it when it exists, then reads
170
- * the new session in this tab: `$fetch` skips Better Auth's session
171
- * signal; `sessionBroadcast` tells the others. Resolves to the sign-in
172
- * error when both fail. */
173
- async function signUpOrInWithEmail(account) {
174
- const {
175
- email,
176
- password
177
- } = account;
178
- const error = (await post("/sign-up/email", account)) && (await post("/sign-in/email", {
179
- email,
180
- password
181
- }));
182
- if (!error) await sessionStore.get().refetch();
183
- return error;
184
- }
185
194
  return {
186
195
  authClient,
187
196
  sessionStore,
188
197
  refreshSession,
189
198
  watchSession,
190
- watchRouteViewer,
191
- signInWithProvider,
192
- signUpOrInWithEmail
199
+ watchViewerRouter,
200
+ post,
201
+ signInWithProvider
193
202
  };
194
203
  }
@@ -0,0 +1,21 @@
1
+ /** Dev builds' browser sign-in as provider accounts. */
2
+ import type { createShellAuthClient } from "../client.ts";
3
+ /** What dev sign-in calls of a shell auth client
4
+ * (`createShellAuthClient`). */
5
+ export interface DevSignInClient {
6
+ post: ReturnType<typeof createShellAuthClient>["post"];
7
+ sessionStore: {
8
+ get(): {
9
+ refetch(): Promise<unknown>;
10
+ };
11
+ };
12
+ }
13
+ /** Signs in through `client`'s dev sign-in (`devProviderSignIn`) as the
14
+ * provider account `accountId`, then reads the new session in this tab:
15
+ * `post` skips Better Auth's session signal; `sessionBroadcast` tells the
16
+ * others. Resolves to the error when sign-in fails. */
17
+ export declare function signInWithDevAccount({ post, sessionStore }: DevSignInClient, accountId: string): Promise<{
18
+ message?: string | undefined;
19
+ status: number;
20
+ statusText: string;
21
+ } | undefined>;
@@ -0,0 +1,17 @@
1
+ import { DEV_SIGN_IN_ENDPOINT } from "./endpoint.js";
2
+ /** Signs in through `client`'s dev sign-in (`devProviderSignIn`) as the
3
+ * provider account `accountId`, then reads the new session in this tab:
4
+ * `post` skips Better Auth's session signal; `sessionBroadcast` tells the
5
+ * others. Resolves to the error when sign-in fails. */
6
+ export async function signInWithDevAccount({
7
+ post,
8
+ sessionStore
9
+ }, accountId) {
10
+ const error = await post(DEV_SIGN_IN_ENDPOINT, {
11
+ accountId
12
+ }, {
13
+ form: true
14
+ });
15
+ if (!error) await sessionStore.get().refetch();
16
+ return error;
17
+ }
@@ -0,0 +1,2 @@
1
+ /** Dev sign-in's endpoint under `AUTH_BASE_PATH`. */
2
+ export declare const DEV_SIGN_IN_ENDPOINT = "/dev/sign-in";
@@ -0,0 +1,2 @@
1
+ /** Dev sign-in's endpoint under `AUTH_BASE_PATH`. */
2
+ export const DEV_SIGN_IN_ENDPOINT = "/dev/sign-in";
@@ -0,0 +1,4 @@
1
+ /** Dev builds' sign-in as provider accounts, and provider accounts
2
+ * resolved to users for seeds. */
3
+ export { type ContextAuth, ensureProviderUser, type ProviderUser, } from "./provider-user.ts";
4
+ export { type DevProviderSignIn, devProviderSignIn, type DevProviderSignInOptions, isDevSignIn, } from "./sign-in.ts";
@@ -0,0 +1,4 @@
1
+ /** Dev builds' sign-in as provider accounts, and provider accounts
2
+ * resolved to users for seeds. */
3
+ export { ensureProviderUser } from "./provider-user.js";
4
+ export { devProviderSignIn, isDevSignIn } from "./sign-in.js";
@@ -0,0 +1,30 @@
1
+ /** Provider accounts resolved to users through better-auth's adapter. */
2
+ import type { AuthContext } from "better-auth";
3
+ /** A provider account and the user it creates when new. */
4
+ export interface ProviderUser {
5
+ /** Provider id (`google`, `github`). */
6
+ providerId: string;
7
+ /** The provider's account id. */
8
+ accountId: string;
9
+ /** Name of a new user. */
10
+ name: string;
11
+ /** Email of a new user; a user holding it gains the account. */
12
+ email: string;
13
+ }
14
+ /** What a dev sign-in function reads of an auth instance: its context's
15
+ * `Keys`; `ensureProviderUser` reads `internalAdapter`. */
16
+ export interface ContextAuth<Keys extends keyof AuthContext = "internalAdapter"> {
17
+ $context: Promise<Pick<AuthContext, Keys>>;
18
+ }
19
+ /**
20
+ * The id of the user owning `providerId` account `accountId`, else of the
21
+ * user holding `email`, else of a new user (`name`, verified `email`); the
22
+ * latter two gain a token-less account. `email` must be trusted (the
23
+ * account's own claim): its holder gains the account. Rows go through
24
+ * better-auth's adapter, so the app's `databaseHooks` run. Concurrent
25
+ * calls for one new account resolve to one user. Queries run inside
26
+ * `withShellAuth`. Seeds call it outside any request: better-auth's
27
+ * `handleOAuthUserInfo`, which dev sign-in uses, takes an endpoint context
28
+ * and creates a session.
29
+ */
30
+ export declare function ensureProviderUser(auth: ContextAuth, { providerId, accountId, name, email }: ProviderUser): Promise<string>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The id of the user owning `providerId` account `accountId`, else of the
3
+ * user holding `email`, else of a new user (`name`, verified `email`); the
4
+ * latter two gain a token-less account. `email` must be trusted (the
5
+ * account's own claim): its holder gains the account. Rows go through
6
+ * better-auth's adapter, so the app's `databaseHooks` run. Concurrent
7
+ * calls for one new account resolve to one user. Queries run inside
8
+ * `withShellAuth`. Seeds call it outside any request: better-auth's
9
+ * `handleOAuthUserInfo`, which dev sign-in uses, takes an endpoint context
10
+ * and creates a session.
11
+ */
12
+ export async function ensureProviderUser(auth, {
13
+ providerId,
14
+ accountId,
15
+ name,
16
+ email
17
+ }) {
18
+ const {
19
+ internalAdapter
20
+ } = await auth.$context;
21
+ const owner = () => internalAdapter.findAccountOwnerByKey({
22
+ providerId,
23
+ accountId
24
+ }).then(found => found?.kind === "owned" ? found.user.id : undefined);
25
+ const existing = await owner();
26
+ if (existing) return existing;
27
+ try {
28
+ const holder = await internalAdapter.findUserByEmail(email);
29
+ if (holder) {
30
+ await internalAdapter.linkAccount({
31
+ userId: holder.user.id,
32
+ providerId,
33
+ accountId
34
+ });
35
+ return holder.user.id;
36
+ }
37
+ const {
38
+ user
39
+ } = await internalAdapter.createOAuthUser({
40
+ name,
41
+ email,
42
+ emailVerified: true
43
+ }, {
44
+ providerId,
45
+ accountId
46
+ });
47
+ return user.id;
48
+ } catch (error) {
49
+ // A concurrent call created the account first: the unique account or
50
+ // email index rejected this one. A failed re-read leaves `error`.
51
+ const winner = await owner().catch(() => undefined);
52
+ if (winner) return winner;
53
+ throw error;
54
+ }
55
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import type { ContextAuth } from "./provider-user.ts";
3
+ /** Dev sign-in's provider and its accounts: `devProviderSignIn`'s options. */
4
+ export interface DevProviderSignInOptions {
5
+ /** Provider id the signed-in accounts belong to. */
6
+ provider: string;
7
+ /** Valid account ids; others answer 400. Default: any non-empty id. */
8
+ accountId?: z.ZodType<string>;
9
+ /** Name and email of a new account's user. */
10
+ newUser: (accountId: string) => {
11
+ name: string;
12
+ email: string;
13
+ };
14
+ }
15
+ /**
16
+ * better-auth plugin for dev builds, `ShellAuthApp.devSignIn`: `POST
17
+ * /dev/sign-in` with form field `accountId` signs in as that `provider`
18
+ * account through better-auth's OAuth sign-in (`handleOAuthUserInfo`): its
19
+ * owner, else the user holding `newUser`'s email under the app's account
20
+ * linking, else a new user (`newUser`, verified email); new accounts hold
21
+ * no tokens, stored tokens stay. Requests from a better-auth trusted origin
22
+ * only (`isTrustedOrigin`: the base URL, allowed hosts, `trustedOrigins`);
23
+ * others and a missing `Origin` answer 403; a refused sign-in answers 401.
24
+ * better-auth sets the session cookies, plus a marker cookie holding the
25
+ * session id signed like the session token (`isDevSignIn`), expiring at
26
+ * the session's sign-in expiry. Answers the session's `token` and `user`
27
+ * id.
28
+ */
29
+ export declare function devProviderSignIn({ provider, accountId, newUser, }: DevProviderSignInOptions): {
30
+ id: "dev-provider-sign-in";
31
+ endpoints: {
32
+ devProviderSignIn: import("better-auth").StrictEndpoint<"/dev/sign-in", {
33
+ method: "POST";
34
+ body: z.ZodObject<{
35
+ accountId: z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>;
36
+ }, z.core.$strip>;
37
+ requireRequest: true;
38
+ metadata: {
39
+ allowedMediaTypes: string[];
40
+ };
41
+ }, {
42
+ token: string;
43
+ user: {
44
+ id: string;
45
+ };
46
+ }>;
47
+ };
48
+ };
49
+ /** The dev sign-in plugin `devProviderSignIn` builds. */
50
+ export type DevProviderSignIn = ReturnType<typeof devProviderSignIn>;
51
+ /**
52
+ * Whether `headers` carry `auth`'s marker cookie holding `session`'s id
53
+ * signed with `auth`'s secret: the auth server signed that id, as dev
54
+ * sign-in does for each session it creates. The marker's name follows
55
+ * `auth`'s cookie naming (prefix, `__Secure-` on HTTPS). False past the
56
+ * marker's sign-in expiry, while an extended session lives on.
57
+ */
58
+ export declare function isDevSignIn(auth: ContextAuth<"createAuthCookie" | "secret">, headers: Headers, session: {
59
+ session: {
60
+ id: string;
61
+ };
62
+ } | null): Promise<boolean>;
@@ -0,0 +1,116 @@
1
+ import { APIError, createAuthEndpoint } from "better-auth/api";
2
+ import { parseCookies, setSessionCookie } from "better-auth/cookies";
3
+ import { constantTimeEqual, makeSignature } from "better-auth/crypto";
4
+ import { handleOAuthUserInfo } from "better-auth/oauth2";
5
+ import { z } from "zod";
6
+ import { DEV_SIGN_IN_ENDPOINT } from "./endpoint.js";
7
+ /** better-auth cookie holding the id of the session dev sign-in created,
8
+ * signed with the auth secret. Its lifetime is fixed at sign-in to the
9
+ * session's `expiresIn`; a session extended on refresh (`updateAge`)
10
+ * outlives it. */
11
+ const MARKER = "dev_sign_in";
12
+ /**
13
+ * better-auth plugin for dev builds, `ShellAuthApp.devSignIn`: `POST
14
+ * /dev/sign-in` with form field `accountId` signs in as that `provider`
15
+ * account through better-auth's OAuth sign-in (`handleOAuthUserInfo`): its
16
+ * owner, else the user holding `newUser`'s email under the app's account
17
+ * linking, else a new user (`newUser`, verified email); new accounts hold
18
+ * no tokens, stored tokens stay. Requests from a better-auth trusted origin
19
+ * only (`isTrustedOrigin`: the base URL, allowed hosts, `trustedOrigins`);
20
+ * others and a missing `Origin` answer 403; a refused sign-in answers 401.
21
+ * better-auth sets the session cookies, plus a marker cookie holding the
22
+ * session id signed like the session token (`isDevSignIn`), expiring at
23
+ * the session's sign-in expiry. Answers the session's `token` and `user`
24
+ * id.
25
+ */
26
+ export function devProviderSignIn({
27
+ provider,
28
+ accountId = z.string().min(1),
29
+ newUser
30
+ }) {
31
+ return {
32
+ id: "dev-provider-sign-in",
33
+ endpoints: {
34
+ devProviderSignIn: createAuthEndpoint(DEV_SIGN_IN_ENDPOINT, {
35
+ method: "POST",
36
+ body: z.object({
37
+ accountId
38
+ }),
39
+ requireRequest: true,
40
+ metadata: {
41
+ allowedMediaTypes: ["application/x-www-form-urlencoded"]
42
+ }
43
+ }, async ctx => {
44
+ const {
45
+ request,
46
+ context
47
+ } = ctx;
48
+ const origin = request.headers.get("origin");
49
+ if (!origin || !context.isTrustedOrigin(origin)) throw APIError.fromStatus("FORBIDDEN", {
50
+ message: "Untrusted origin sign-in forbidden."
51
+ });
52
+ const id = ctx.body.accountId;
53
+ const signIn = () => handleOAuthUserInfo(ctx, {
54
+ userInfo: {
55
+ id,
56
+ ...newUser(id),
57
+ emailVerified: true
58
+ },
59
+ account: {
60
+ providerId: provider,
61
+ accountId: id
62
+ }
63
+ });
64
+ const first = await signIn();
65
+ // A concurrent sign-in as the same new account fails this one's
66
+ // create; the retry signs in as the winner's user.
67
+ const {
68
+ data,
69
+ error
70
+ } = first.data ? first : await signIn();
71
+ if (!data) throw APIError.fromStatus("UNAUTHORIZED", {
72
+ message: error
73
+ });
74
+ const {
75
+ session,
76
+ user
77
+ } = data;
78
+ await setSessionCookie(ctx, {
79
+ session,
80
+ user
81
+ });
82
+ const marker = context.createAuthCookie(MARKER);
83
+ await ctx.setSignedCookie(marker.name, session.id, context.secret, {
84
+ ...marker.attributes,
85
+ maxAge: context.sessionConfig.expiresIn
86
+ });
87
+ return ctx.json({
88
+ token: session.token,
89
+ user: {
90
+ id: user.id
91
+ }
92
+ });
93
+ })
94
+ }
95
+ };
96
+ }
97
+ /**
98
+ * Whether `headers` carry `auth`'s marker cookie holding `session`'s id
99
+ * signed with `auth`'s secret: the auth server signed that id, as dev
100
+ * sign-in does for each session it creates. The marker's name follows
101
+ * `auth`'s cookie naming (prefix, `__Secure-` on HTTPS). False past the
102
+ * marker's sign-in expiry, while an extended session lives on.
103
+ */
104
+ export async function isDevSignIn(auth, headers, session) {
105
+ if (!session) return false;
106
+ const {
107
+ createAuthCookie,
108
+ secret
109
+ } = await auth.$context;
110
+ const value = parseCookies(headers.get("cookie") ?? "").get(createAuthCookie(MARKER).name);
111
+ if (value === undefined) return false;
112
+ const {
113
+ id
114
+ } = session.session;
115
+ return constantTimeEqual(value, `${id}.${await makeSignature(id, secret)}`);
116
+ }
@@ -1,6 +1,7 @@
1
1
  import { type BetterAuthOptions, type BetterAuthPlugin, type Session, type User } from "better-auth";
2
2
  import { type Dialect } from "kysely";
3
3
  import { z } from "zod";
4
+ import type { DevProviderSignIn } from "./dev-sign-in/sign-in.ts";
4
5
  import type { AuthProviderCredentials, AuthProviders } from "./providers/provider.ts";
5
6
  import type { SessionLike } from "./types.ts";
6
7
  /** `auth.users` columns beside better-auth's core fields. */
@@ -22,8 +23,16 @@ export interface ShellAuthApp<Fields extends AdditionalFields, Projected extends
22
23
  providers?: AuthProviders;
23
24
  /** Signed-in users link more provider accounts (`/link-social`). */
24
25
  linkSocial?: boolean;
25
- /** Email+password sign-in and sign-up endpoints, for dev builds. */
26
- devEmailSignIn?: boolean;
26
+ /**
27
+ * Dev sign-in as any account of a provider (`devProviderSignIn`): `POST
28
+ * /api/auth/dev/sign-in` grants a session for any account id to any
29
+ * caller reaching the server; its trusted-origin `Origin` check is the
30
+ * one gate. Build it under the dev build flag alone
31
+ * (`import.meta.env.DEV`), which drops it from production bundles;
32
+ * `testProductionAuth` proves the production build serves no dev
33
+ * sign-in.
34
+ */
35
+ devSignIn?: DevProviderSignIn;
27
36
  /** Page OAuth failures redirect to, with `error` and `error_description`
28
37
  * query params. */
29
38
  errorURL: string;
@@ -300,15 +309,9 @@ export interface ShellSession<Fields extends AdditionalFields> {
300
309
  user: User<SchemaOptions<Fields>["user"]>;
301
310
  }
302
311
  /** better-auth for `config` over the running scope's database: the `auth.*`
303
- * schema, the session projection, the providers' plugins, then
304
- * `config.plugins`. */
312
+ * schema, the session projection, the providers' plugins, dev sign-in,
313
+ * then `config.plugins`. */
305
314
  declare function buildAuth<Fields extends AdditionalFields, Projected extends SessionLike>(config: ShellAuthConfig<Fields, Projected>): import("better-auth").Auth<{
306
- onAPIError: {
307
- errorURL: string;
308
- };
309
- emailAndPassword?: {
310
- enabled: true;
311
- } | undefined;
312
315
  baseURL: NonNullable<import("better-auth").BaseURLConfig | undefined>;
313
316
  secret: string;
314
317
  database: {
@@ -361,7 +364,29 @@ declare function buildAuth<Fields extends AdditionalFields, Projected extends Se
361
364
  $Infer: {
362
365
  Session: Projected;
363
366
  };
364
- }, ...BetterAuthPlugin[]];
367
+ }, ...(BetterAuthPlugin | {
368
+ id: "dev-provider-sign-in";
369
+ endpoints: {
370
+ devProviderSignIn: import("better-auth").StrictEndpoint<"/dev/sign-in", {
371
+ method: "POST";
372
+ body: z.ZodObject<{
373
+ accountId: z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>;
374
+ }, z.core.$strip>;
375
+ requireRequest: true;
376
+ metadata: {
377
+ allowedMediaTypes: string[];
378
+ };
379
+ }, {
380
+ token: string;
381
+ user: {
382
+ id: string;
383
+ };
384
+ }>;
385
+ };
386
+ })[]];
387
+ onAPIError: {
388
+ errorURL: string;
389
+ };
365
390
  appName: string;
366
391
  advanced: {
367
392
  disableOriginCheck: false;
@@ -238,14 +238,14 @@ const scopedPostgres = {
238
238
  createIntrospector: db => new PostgresIntrospector(db)
239
239
  };
240
240
  /** better-auth for `config` over the running scope's database: the `auth.*`
241
- * schema, the session projection, the providers' plugins, then
242
- * `config.plugins`. */
241
+ * schema, the session projection, the providers' plugins, dev sign-in,
242
+ * then `config.plugins`. */
243
243
  function buildAuth(config) {
244
244
  const {
245
245
  baseURL,
246
246
  secret,
247
247
  plugins,
248
- devEmailSignIn,
248
+ devSignIn,
249
249
  errorURL
250
250
  } = config;
251
251
  const schema = schemaOptions(config);
@@ -260,12 +260,7 @@ function buildAuth(config) {
260
260
  transaction: true
261
261
  },
262
262
  socialProviders: providers.socialProviders,
263
- plugins: [sessionProjection(config.projectSession), ...providers.plugins, ...(plugins ?? [])],
264
- ...(devEmailSignIn && {
265
- emailAndPassword: {
266
- enabled: true
267
- }
268
- }),
263
+ plugins: [sessionProjection(config.projectSession), ...providers.plugins, ...(devSignIn ? [devSignIn] : []), ...(plugins ?? [])],
269
264
  onAPIError: {
270
265
  errorURL
271
266
  }