@vonall/auth 0.4.0 → 0.6.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +14 -7
  3. package/dist/index.d.ts +1 -1
  4. package/dist/messages.d.ts +460 -16
  5. package/dist/messages.js +4 -14
  6. package/dist/remote/app_user.remote.d.ts +2 -2
  7. package/dist/remote/app_user.remote.js +13 -13
  8. package/dist/remote/einladung.remote.js +15 -12
  9. package/dist/server/{registry.d.ts → context.d.ts} +13 -16
  10. package/dist/server/context.js +22 -0
  11. package/dist/server/create.d.ts +27 -22
  12. package/dist/server/create.js +50 -33
  13. package/dist/server/index.d.ts +7 -7
  14. package/dist/server/index.js +6 -6
  15. package/dist/server/loads.d.ts +14 -2
  16. package/dist/server/loads.js +19 -14
  17. package/dist/server/repositories/EinladungRepo.d.ts +4 -1
  18. package/dist/server/repositories/EinladungRepo.js +66 -73
  19. package/dist/server/repositories/UserRepo.d.ts +4 -1
  20. package/dist/server/repositories/UserRepo.js +49 -57
  21. package/dist/server/service/einladung.service.d.ts +6 -2
  22. package/dist/server/service/einladung.service.js +56 -51
  23. package/dist/server/service/mail.d.ts +16 -12
  24. package/dist/server/service/mail.js +32 -34
  25. package/dist/testing/index.d.ts +6 -8
  26. package/dist/testing/index.js +16 -19
  27. package/dist/ui/AdminView.svelte +2 -1
  28. package/dist/ui/InvitationsTab.svelte +2 -1
  29. package/dist/ui/LoginForm.svelte +1 -1
  30. package/dist/ui/ResetPasswordForm.svelte +2 -2
  31. package/dist/ui/context.d.ts +6 -7
  32. package/dist/ui/context.js +15 -9
  33. package/dist/util/signup.d.ts +2 -0
  34. package/dist/util/signup.js +2 -0
  35. package/package.json +3 -2
  36. package/dist/server/registry.js +0 -22
@@ -1,8 +1,8 @@
1
1
  /** The Admin option: every account. */
2
2
  export declare const getAllUsers: import("$app/server").RemoteQueryFunction<void, {
3
- id: string;
4
- name: string;
5
3
  email: string;
4
+ name: string;
5
+ id: string;
6
6
  image: string | null;
7
7
  emailVerified: boolean;
8
8
  createdAt: Date;
@@ -3,16 +3,16 @@ import { getRequestEvent } from "$app/server";
3
3
  import { z } from "zod";
4
4
  import { m } from "../messages.js";
5
5
  import { adminQuery, adminSchemaCommand, protectedCommand, protectedForm, protectedQuery, protectedSchemaCommand } from "../server/helpers.js";
6
- import { authInstance } from "../server/registry.js";
7
- import { UserRepo } from "../server/repositories/UserRepo.js";
6
+ import { authContext } from "../server/context.js";
7
+ import { userRepo } from "../server/repositories/UserRepo.js";
8
8
  import { emailSchema, nameSchema, passwordSchema } from "../types.js";
9
9
  import { isSessionNotFresh, reauthPath } from "../util/fresh.js";
10
10
  /** The Admin option: every account. */
11
11
  export const getAllUsers = adminQuery(async () => {
12
- return await UserRepo.getAllUsers();
12
+ return await userRepo(authContext()).getAllUsers();
13
13
  });
14
14
  export const updateUserProfile = protectedForm(z.object({ name: nameSchema, email: emailSchema }), async (data, _issue, user) => {
15
- const auth = authInstance();
15
+ const { auth } = authContext();
16
16
  const event = getRequestEvent();
17
17
  if (data.email !== user.email) {
18
18
  await auth.api.changeEmail({ body: { newEmail: data.email }, headers: event.request.headers });
@@ -24,7 +24,7 @@ const passwordPair = { newPassword: passwordSchema, confirmPassword: z.string()
24
24
  const passwordsMatch = { error: () => m().passwordsMismatch, path: ["confirmPassword"] };
25
25
  export const changePassword = protectedForm(z.object({ currentPassword: passwordSchema, ...passwordPair }).refine((d) => d.newPassword === d.confirmPassword, passwordsMatch), async (data) => {
26
26
  const event = getRequestEvent();
27
- await authInstance().api.changePassword({
27
+ await authContext().auth.api.changePassword({
28
28
  body: { currentPassword: data.currentPassword, newPassword: data.newPassword, revokeOtherSessions: true },
29
29
  headers: event.request.headers,
30
30
  });
@@ -32,13 +32,13 @@ export const changePassword = protectedForm(z.object({ currentPassword: password
32
32
  });
33
33
  export const setPassword = protectedForm(z.object(passwordPair).refine((d) => d.newPassword === d.confirmPassword, passwordsMatch), async (data) => {
34
34
  const event = getRequestEvent();
35
- await authInstance().api.setPassword({ body: { newPassword: data.newPassword }, headers: event.request.headers });
35
+ await authContext().auth.api.setPassword({ body: { newPassword: data.newPassword }, headers: event.request.headers });
36
36
  return { success: true };
37
37
  });
38
38
  export const getSessions = protectedQuery(async () => {
39
39
  const event = getRequestEvent();
40
40
  try {
41
- return await authInstance().api.listSessions({ headers: event.request.headers });
41
+ return await authContext().auth.api.listSessions({ headers: event.request.headers });
42
42
  }
43
43
  catch (e) {
44
44
  // Only a login from the last day may list the sessions; the sessions are in the settings
@@ -50,7 +50,7 @@ export const getSessions = protectedQuery(async () => {
50
50
  export const revokeSessionById = protectedSchemaCommand(z.object({ sessionId: z.string() }), async (data) => {
51
51
  const event = getRequestEvent();
52
52
  const currentSessionToken = event.locals.session?.token;
53
- await authInstance().api.revokeSession({ body: { token: data.sessionId }, headers: event.request.headers });
53
+ await authContext().auth.api.revokeSession({ body: { token: data.sessionId }, headers: event.request.headers });
54
54
  const isCurrentSession = data.sessionId === currentSessionToken;
55
55
  if (!isCurrentSession)
56
56
  await getSessions().refresh();
@@ -58,19 +58,19 @@ export const revokeSessionById = protectedSchemaCommand(z.object({ sessionId: z.
58
58
  });
59
59
  export const revokeOthers = protectedCommand(async () => {
60
60
  const event = getRequestEvent();
61
- await authInstance().api.revokeOtherSessions({ headers: event.request.headers });
61
+ await authContext().auth.api.revokeOtherSessions({ headers: event.request.headers });
62
62
  await getSessions().refresh();
63
63
  return { success: true };
64
64
  });
65
65
  export const getLinkedAccounts = protectedQuery(async (user) => {
66
- return await UserRepo.getUserAccounts(user.id);
66
+ return await userRepo(authContext()).getUserAccounts(user.id);
67
67
  });
68
68
  /** The Admin option. */
69
69
  export const setUserRole = adminSchemaCommand(z.object({ userId: z.string(), role: z.enum(["admin", "user"]) }), async ({ userId, role }, admin) => {
70
70
  if (userId === admin.id) {
71
71
  throw error(400, { message: m().ownRole, id: "own_role", timestamp: new Date().toISOString() });
72
72
  }
73
- await UserRepo.setRole(userId, role);
73
+ await userRepo(authContext()).setRole(userId, role);
74
74
  await getAllUsers().refresh();
75
75
  return { success: true };
76
76
  });
@@ -79,12 +79,12 @@ export const deleteUser = adminSchemaCommand(z.object({ userId: z.string() }), a
79
79
  if (userId === admin.id) {
80
80
  throw error(400, { message: m().ownAccount, id: "own_account", timestamp: new Date().toISOString() });
81
81
  }
82
- await UserRepo.kontoLoeschen(userId);
82
+ await userRepo(authContext()).kontoLoeschen(userId);
83
83
  await getAllUsers().refresh();
84
84
  return { success: true };
85
85
  });
86
86
  /** Deletes the user's own account with all its data. */
87
87
  export const deleteOwnAccount = protectedCommand(async (user) => {
88
- await UserRepo.kontoLoeschen(user.id);
88
+ await userRepo(authContext()).kontoLoeschen(user.id);
89
89
  return { success: true };
90
90
  });
@@ -2,10 +2,10 @@ import { error } from "@sveltejs/kit";
2
2
  import { z } from "zod";
3
3
  import { m } from "../messages.js";
4
4
  import { adminQuery, adminSchemaCommand, protectedQuery, protectedSchemaCommand } from "../server/helpers.js";
5
- import { registry } from "../server/registry.js";
6
- import { EinladungRepo } from "../server/repositories/EinladungRepo.js";
7
- import { EinladungService } from "../server/service/einladung.service.js";
8
- import { sendEinladungEmail } from "../server/service/mail.js";
5
+ import { authContext } from "../server/context.js";
6
+ import { einladungRepo } from "../server/repositories/EinladungRepo.js";
7
+ import { einladungService } from "../server/service/einladung.service.js";
8
+ import { authMails } from "../server/service/mail.js";
9
9
  import { emailSchema } from "../types.js";
10
10
  import { messageOf } from "../util/messageOf.js";
11
11
  import { isAdmin, mayInvite } from "../util/signup.js";
@@ -16,20 +16,22 @@ function unwrap(result) {
16
16
  }
17
17
  /** The user's open Einladungen, and whether the server can send them by email. */
18
18
  export const getOffeneEinladungen = protectedQuery(async (user) => {
19
- return { einladungen: await EinladungRepo.listOpenBy(user.id, new Date()), mail: registry().mailer.enabled };
19
+ const ctx = authContext();
20
+ return { einladungen: await einladungRepo(ctx).listOpenBy(user.id, new Date()), mail: ctx.mailer.enabled };
20
21
  });
21
22
  /** Creates an Einladung; the link is returned only here. With an email, it is also sent (if mail is set up). */
22
23
  export const createEinladung = protectedSchemaCommand(z.object({ email: emailSchema.nullable() }), async ({ email }, user) => {
23
- const { origin, signupMode, admin, locale } = registry();
24
+ const ctx = authContext();
25
+ const { origin, signupMode, admin } = ctx;
24
26
  if (!mayInvite(signupMode, isAdmin(user))) {
25
27
  throw error(403, { message: m().onlyAdminsInvite, id: "forbidden", timestamp: new Date().toISOString() });
26
28
  }
27
- const einladung = await EinladungService.erstellen(user.id, { email });
29
+ const einladung = await einladungService(ctx).erstellen(user.id, { email });
28
30
  const link = `${origin}/einladung/${einladung.token}`;
29
31
  let mailFehler = null;
30
32
  if (email) {
31
33
  try {
32
- await sendEinladungEmail(user.name, email, { link, expiresAt: einladung.expiresAt }, locale);
34
+ await authMails(ctx).einladung(user.name, email, { link, expiresAt: einladung.expiresAt });
33
35
  }
34
36
  catch (e) {
35
37
  mailFehler = messageOf(e, m().mailFailed);
@@ -41,17 +43,18 @@ export const createEinladung = protectedSchemaCommand(z.object({ email: emailSch
41
43
  return { link, expiresAt: einladung.expiresAt, mailFehler };
42
44
  });
43
45
  export const withdrawEinladung = protectedSchemaCommand(z.object({ einladungId: z.string() }), async ({ einladungId }, user) => {
44
- unwrap(await EinladungService.zurueckziehen(user.id, einladungId));
46
+ const ctx = authContext();
47
+ unwrap(await einladungService(ctx).zurueckziehen(user.id, einladungId));
45
48
  await getOffeneEinladungen().refresh();
46
- if (registry().admin && isAdmin(user))
49
+ if (ctx.admin && isAdmin(user))
47
50
  await getAlleEinladungen().refresh();
48
51
  });
49
52
  /** The Admin option: every open Einladung, and withdrawing any of them. */
50
53
  export const getAlleEinladungen = adminQuery(async () => {
51
- return await EinladungRepo.listAllOpen(new Date());
54
+ return await einladungRepo(authContext()).listAllOpen(new Date());
52
55
  });
53
56
  export const adminWithdrawEinladung = adminSchemaCommand(z.object({ einladungId: z.string() }), async ({ einladungId }) => {
54
- unwrap(await EinladungService.zurueckziehen(null, einladungId));
57
+ unwrap(await einladungService(authContext()).zurueckziehen(null, einladungId));
55
58
  await getAlleEinladungen().refresh();
56
59
  await getOffeneEinladungen().refresh();
57
60
  });
@@ -1,11 +1,6 @@
1
- /**
2
- * What `createAuth()` set up, for the package's repositories, services and remote functions: the Product's database,
3
- * its tables, the options and the better-auth instance. One app per process, so a module-level value; the Product's
4
- * wiring (`src/lib/auth.ts`) runs before any request.
5
- */
6
1
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
7
2
  import type { Mailer } from "@vonall/mail";
8
- import type { FullSchema } from "../schema/index.js";
3
+ import type { AuthSchema } from "../schema/index.js";
9
4
  import type { Auth } from "./create.js";
10
5
  import type { SignupMode } from "../types.js";
11
6
  export type Db = NodePgDatabase<any>;
@@ -16,27 +11,29 @@ export type AuthLog = {
16
11
  warn(message: string, context?: Record<string, unknown>): void;
17
12
  error(message: string, context?: Record<string, unknown>, error?: unknown): void;
18
13
  };
19
- export type Registry = {
14
+ export type AuthContext<S extends AuthSchema = AuthSchema> = {
20
15
  db: Db;
21
- schema: FullSchema;
16
+ schema: S;
22
17
  appName: string;
23
18
  origin: string;
24
- /** The app's locale for dates in mails, e.g. "de-CH". */
19
+ /** The app's locale, e.g. "de-CH": dates in mails and on the pages. */
25
20
  locale: string;
26
21
  signupMode: SignupMode;
27
22
  mailer: Mailer;
28
- admin: boolean;
29
23
  log: AuthLog;
30
- /** The Product's instance, from `registerAuth()`; null between `authOptions()` and that call. */
31
- auth: Auth | null;
24
+ /** The Admin option: `schema` has the role columns (and the instance the admin plugin, `register()` checks). */
25
+ admin: boolean;
26
+ /** Whether `schema` has the OAuth provider's tables (`@vonall/mcp`). */
27
+ mcp: boolean;
32
28
  /** The social logins that have keys. */
33
29
  providers: {
34
30
  github: boolean;
35
31
  google: boolean;
36
32
  };
33
+ /** The Product's instance; reading it before `register()` throws. */
34
+ readonly auth: Auth;
37
35
  };
38
- export declare function setRegistry(registry: Registry): void;
39
- export declare function registry(): Registry;
40
- /** The Product's better-auth instance. */
41
- export declare function authInstance(): Auth;
36
+ /** The context of the current request: `event.locals.auth`, which the session handle set. */
37
+ export declare function authContext(): AuthContext;
38
+ export declare function contextOf(locals: object): AuthContext;
42
39
  export declare const consoleLog: AuthLog;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What `createAuth()` set up, for everything else in this package and in `@vonall/mcp`: the Product's database and
3
+ * tables, its options and, after `register()`, its better-auth instance. It is passed explicitly; inside a request it
4
+ * is also in `event.locals.auth` (the session handle puts it there), where the remote functions and the loads find it.
5
+ */
6
+ import { getRequestEvent } from "$app/server";
7
+ /** The context of the current request: `event.locals.auth`, which the session handle set. */
8
+ export function authContext() {
9
+ return contextOf(getRequestEvent().locals);
10
+ }
11
+ export function contextOf(locals) {
12
+ const ctx = locals.auth;
13
+ if (!ctx)
14
+ throw new Error("@vonall/auth: no auth context on this request; the session handle of createAuth().register() is not in hooks.server.ts");
15
+ return ctx;
16
+ }
17
+ export const consoleLog = {
18
+ debug: () => { },
19
+ info: (message, context) => console.info(message, context ?? ""),
20
+ warn: (message, context) => console.warn(message, context ?? ""),
21
+ error: (message, context, error) => console.error(message, context ?? "", error ?? ""),
22
+ };
@@ -1,7 +1,8 @@
1
1
  /**
2
- * better-auth for a Product, from explicit options. The Product's `src/lib/auth.ts` builds the instance itself, so its
3
- * types are inferred in the Product (its plugins included): `betterAuth({ ...authOptions({…}), plugins: […] })`, then
4
- * `registerAuth(auth)` for the session handle and the API handler; everything else in this package reads the result.
2
+ * better-auth for a Product: `const wiring = createAuth({…})`, then the Product builds the instance itself, so its
3
+ * types are inferred there (its plugins included): `betterAuth({ ...wiring.options, plugins: […] })`, and
4
+ * `wiring.register(auth)` gives the context, the session handle and the API handler; everything else in this package
5
+ * takes the context.
5
6
  */
6
7
  import type { RequestHandler } from "@sveltejs/kit";
7
8
  import type { Handle } from "@sveltejs/kit/hooks";
@@ -12,29 +13,27 @@ import { sveltekitCookies } from "better-auth/svelte-kit";
12
13
  import type { Mailer } from "@vonall/mail";
13
14
  import { roles, ac } from "../permissions.js";
14
15
  import type { AuthSchema } from "../schema/index.js";
15
- import { type AuthLog, type Db } from "./registry.js";
16
+ import { type AuthContext, type AuthLog, type Db } from "./context.js";
16
17
  import { type SignupMode } from "../types.js";
17
18
  export type SocialProvider = {
18
19
  clientId: string;
19
20
  clientSecret: string;
20
21
  };
21
- export type CreateAuthOptions = {
22
+ export type CreateAuthOptions<S extends AuthSchema = AuthSchema> = {
22
23
  /** The Product's Drizzle database. */
23
24
  db: Db;
24
- /** The Product's tables from `authSchema()`, the same objects its `schema/` re-exports. */
25
- schema: AuthSchema;
25
+ /** The Product's tables from `authSchema()`, the same objects its `schema/` re-exports; its options are read off them. */
26
+ schema: S;
26
27
  /** BETTER_AUTH_SECRET. */
27
28
  secret: string;
28
29
  /** The app's own address (ORIGIN), e.g. https://blueprint.vonall.ch. */
29
30
  origin: string;
30
31
  appName: string;
31
- /** LOCALE, e.g. "en-GB" or "de-CH": dates in mails. */
32
+ /** LOCALE, e.g. "en-GB" or "de-CH": dates in mails and on the pages. */
32
33
  locale: string;
33
34
  signupMode: SignupMode;
34
35
  /** The Product's mailer (`@vonall/mail`); off without SMTP, which the signup mode `open` and the reset respect. */
35
36
  mailer: Mailer;
36
- /** The Admin option: the admin plugin and roles, the first account becomes admin. Matches `authSchema({ admin })`. */
37
- admin?: boolean;
38
37
  /** Social logins with keys; a provider without keys is left out (its button never shows). */
39
38
  social?: {
40
39
  github?: SocialProvider | null;
@@ -60,13 +59,18 @@ export type Auth<P extends BetterAuthPlugin[] = []> = ReturnType<typeof betterAu
60
59
  plugins: [...BasePlugins, ...P];
61
60
  }>>;
62
61
  /**
63
- * The Product's instance as `registerAuth()` takes it: built on `authOptions()`, with any plugins (their types stay the
62
+ * The Product's instance as `register()` takes it: built on `wiring.options`, with any plugins (their types stay the
64
63
  * Product's; this package uses the instance through its own `Auth` type).
65
64
  */
66
65
  export type AuthLike = {
67
66
  handler: (request: Request) => Promise<Response>;
68
67
  api: object;
69
68
  $context: Promise<object>;
69
+ options: {
70
+ plugins?: {
71
+ id: string;
72
+ }[];
73
+ };
70
74
  };
71
75
  /** The admin plugin's columns, named here because the plugin's type doesn't carry them into the session's user. */
72
76
  export type AdminFields = {
@@ -78,18 +82,19 @@ export type AdminFields = {
78
82
  /** The user and the session as the hooks put them into `locals` (a Product's `app.d.ts` uses these). */
79
83
  export type AuthUser = Auth["$Infer"]["Session"]["user"] & AdminFields;
80
84
  export type AuthSession = Auth["$Infer"]["Session"]["session"];
81
- export declare function authOptions(options: CreateAuthOptions): BetterAuthOptions;
82
- export type RegisteredAuth = {
83
- /** The hooks handle: the session into `locals.user`/`locals.session`, and better-auth's own routes (/api/auth/*). */
84
- handle: Handle;
85
+ export type RegisteredAuth<S extends AuthSchema = AuthSchema> = {
86
+ /** For everything that works with accounts outside a request: `createMcp`, scripts, tests. */
87
+ ctx: AuthContext<S>;
88
+ /** The hooks handle: the context into `locals.auth`, the session into `locals.user`/`locals.session`, and better-auth's own routes (/api/auth/*). */
89
+ sessionHandle: Handle;
85
90
  /** For `src/routes/api/auth/[...auth]/+server.ts`: `export const GET = apiHandler, POST = apiHandler`. */
86
91
  apiHandler: RequestHandler;
87
- /** The social logins that have keys; the login, signup and settings pages show only these. */
88
- providers: {
89
- github: boolean;
90
- google: boolean;
91
- };
92
92
  };
93
- /** Hands the Product's instance to this package (after `authOptions()`) and returns the handles. */
94
- export declare function registerAuth(auth: AuthLike): RegisteredAuth;
93
+ export type AuthWiring<S extends AuthSchema = AuthSchema> = {
94
+ /** For `betterAuth({ ...options, plugins: […] })`. */
95
+ options: BetterAuthOptions;
96
+ /** Hands the Product's instance to this package; checks its plugins against the schema's options. */
97
+ register(auth: AuthLike): RegisteredAuth<S>;
98
+ };
99
+ export declare function createAuth<S extends AuthSchema>(options: CreateAuthOptions<S>): AuthWiring<S>;
95
100
  export {};
@@ -7,11 +7,11 @@ import { twoFactor } from "better-auth/plugins/two-factor";
7
7
  import { sveltekitCookies, svelteKitHandler } from "better-auth/svelte-kit";
8
8
  import { m } from "../messages.js";
9
9
  import { roles, ac } from "../permissions.js";
10
+ import { consoleLog } from "./context.js";
10
11
  import { hashPassword, verifyPassword } from "./crypto.js";
11
- import { consoleLog, registry, setRegistry } from "./registry.js";
12
- import { UserRepo } from "./repositories/UserRepo.js";
13
- import { EINLADUNG_COOKIE, EinladungService } from "./service/einladung.service.js";
14
- import { sendPasswordResetEmail, sendVerifyEmail } from "./service/mail.js";
12
+ import { userRepo } from "./repositories/UserRepo.js";
13
+ import { EINLADUNG_COOKIE, einladungService } from "./service/einladung.service.js";
14
+ import { authMails } from "./service/mail.js";
15
15
  import { MIN_PASSWORD_LENGTH } from "../types.js";
16
16
  import { nameFromEmail } from "../util/names.js";
17
17
  /** The Einladung token the browser carries from its link (`/einladung/[token]`), if any. */
@@ -24,9 +24,10 @@ function einladungAusCookie() {
24
24
  }
25
25
  }
26
26
  const hasKeys = (p) => Boolean(p?.clientId && p.clientSecret && p.clientId !== "dummy");
27
- export function authOptions(options) {
28
- const { secret, origin, appName, signupMode, admin = false, social = {}, rateLimitRules = {}, log = consoleLog } = options;
29
- setRegistry({
27
+ export function createAuth(options) {
28
+ const { secret, origin, appName, signupMode, social = {}, rateLimitRules = {}, log = consoleLog } = options;
29
+ let instance = null;
30
+ const ctx = {
30
31
  db: options.db,
31
32
  schema: options.schema,
32
33
  appName,
@@ -34,13 +35,21 @@ export function authOptions(options) {
34
35
  locale: options.locale,
35
36
  signupMode,
36
37
  mailer: options.mailer,
37
- admin,
38
38
  log,
39
- auth: null,
39
+ admin: "role" in options.schema.user,
40
+ mcp: "oauthClient" in options.schema,
40
41
  providers: { github: hasKeys(social.github), google: hasKeys(social.google) },
41
- });
42
+ get auth() {
43
+ if (!instance)
44
+ throw new Error("@vonall/auth: createAuth().register(auth) has not run; the app's auth wiring (src/lib/auth.ts) calls it after betterAuth()");
45
+ return instance;
46
+ },
47
+ };
48
+ const users = userRepo(ctx);
49
+ const einladungen = einladungService(ctx);
50
+ const mails = authMails(ctx);
42
51
  const baseURL = origin || "http://localhost";
43
- return {
52
+ const betterAuthOptions = {
44
53
  appName,
45
54
  baseURL,
46
55
  secret: secret || "dummy-secret-for-build-purposes-only",
@@ -73,7 +82,7 @@ export function authOptions(options) {
73
82
  before: async (user) => {
74
83
  // The signup mode decides: an Einladung, or (open) mail for the confirmation link. The very first account
75
84
  // (the admin) needs neither and counts as confirmed.
76
- const { state, firstAccount } = await EinladungService.signup(einladungAusCookie());
85
+ const { state, firstAccount } = await einladungen.signup(einladungAusCookie());
77
86
  if (state === "needs_einladung")
78
87
  throw new APIError("FORBIDDEN", { message: m().needsEinladung });
79
88
  if (state === "no_mail")
@@ -87,14 +96,14 @@ export function authOptions(options) {
87
96
  },
88
97
  after: async (user) => {
89
98
  // The first user becomes admin
90
- if (admin && (await UserRepo.countUsers()) === 1) {
99
+ if (ctx.admin && (await users.countUsers()) === 1) {
91
100
  log.info("First user created, bootstrapping global role to admin");
92
- await UserRepo.setRole(user.id, "admin");
101
+ await users.setRole(user.id, "admin");
93
102
  }
94
103
  const token = einladungAusCookie();
95
104
  if (!token)
96
105
  return;
97
- const angenommen = await EinladungService.annehmen(user.id, token);
106
+ const angenommen = await einladungen.annehmen(user.id, token);
98
107
  if (angenommen.isErr())
99
108
  log.warn("Einladung could not be accepted after signup", { userId: user.id, kind: angenommen.error.kind });
100
109
  getRequestEvent().cookies.delete(EINLADUNG_COOKIE, { path: "/" });
@@ -109,7 +118,7 @@ export function authOptions(options) {
109
118
  // Signup mode open: anyone may sign up, so the email address is confirmed before the account can be used
110
119
  requireEmailVerification: signupMode === "open",
111
120
  sendResetPassword: async ({ user, url }) => {
112
- await sendPasswordResetEmail(user, url);
121
+ await mails.passwordReset(user, url);
113
122
  },
114
123
  },
115
124
  emailVerification: {
@@ -118,7 +127,7 @@ export function authOptions(options) {
118
127
  autoSignInAfterVerification: true,
119
128
  sendVerificationEmail: async ({ user, url }) => {
120
129
  if (!user.emailVerified)
121
- await sendVerifyEmail(user, url);
130
+ await mails.verify(user, url);
122
131
  },
123
132
  },
124
133
  socialProviders: {
@@ -134,21 +143,29 @@ export function authOptions(options) {
134
143
  cookieCache: { enabled: false },
135
144
  },
136
145
  };
137
- }
138
- /** Hands the Product's instance to this package (after `authOptions()`) and returns the handles. */
139
- export function registerAuth(auth) {
140
- const current = registry();
141
- setRegistry({ ...current, auth: auth });
142
- const instance = auth;
143
- const handle = async ({ event, resolve }) => {
144
- const session = await instance.api.getSession({ headers: event.request.headers });
145
- if (session) {
146
- event.locals.session = session.session;
147
- event.locals.user = session.user;
148
- }
149
- const { building } = await import("$app/env");
150
- return svelteKitHandler({ event, resolve, auth: instance, building });
146
+ return {
147
+ options: betterAuthOptions,
148
+ register(auth) {
149
+ // The instance's plugins must match the schema's options, or the columns and the plugin disagree at runtime
150
+ const plugins = new Set((auth.options.plugins ?? []).map((p) => p.id));
151
+ if (plugins.has("admin") !== ctx.admin) {
152
+ throw new Error(`@vonall/auth: ${ctx.admin ? "authSchema({ admin: true }) but the instance has no admin plugin" : "the instance has the admin plugin but authSchema() was built without admin"}`);
153
+ }
154
+ if (plugins.has("oauth-provider") && !ctx.mcp)
155
+ throw new Error("@vonall/auth: the instance has the MCP plugin but authSchema() was built without mcp (its tables are missing)");
156
+ instance = auth;
157
+ const sessionHandle = async ({ event, resolve }) => {
158
+ event.locals.auth = ctx;
159
+ const session = await ctx.auth.api.getSession({ headers: event.request.headers });
160
+ if (session) {
161
+ event.locals.session = session.session;
162
+ event.locals.user = session.user;
163
+ }
164
+ const { building } = await import("$app/env");
165
+ return svelteKitHandler({ event, resolve, auth: ctx.auth, building });
166
+ };
167
+ const apiHandler = ({ request }) => auth.handler(request);
168
+ return { ctx, sessionHandle, apiHandler };
169
+ },
151
170
  };
152
- const apiHandler = ({ request }) => auth.handler(request);
153
- return { handle, apiHandler, providers: current.providers };
154
171
  }
@@ -1,8 +1,8 @@
1
- export { authOptions, registerAuth, type AdminFields, type Auth, type AuthLike, type AuthSession, type AuthUser, type CreateAuthOptions, type RegisteredAuth, type SocialProvider } from "./create.js";
1
+ export { authContext, contextOf, type AuthContext, type AuthLog, type Db } from "./context.js";
2
+ export { createAuth, type AdminFields, type Auth, type AuthLike, type AuthSession, type AuthUser, type AuthWiring, type CreateAuthOptions, type RegisteredAuth, type SocialProvider } from "./create.js";
2
3
  export { adminCommand, adminQuery, adminSchemaCommand, adminSchemaQuery, protectedCommand, protectedForm, protectedQuery, protectedSchemaCommand, protectedSchemaQuery, requireAdmin, requireUser, } from "./helpers.js";
3
- export { authLayoutLoad, einladungLoad, homeLayoutLoad, loginLoad, manageUserLoad, signupLoad } from "./loads.js";
4
- export { authInstance, registry as authRegistry, type AuthLog, type Db } from "./registry.js";
5
- export { EinladungRepo } from "./repositories/EinladungRepo.js";
6
- export { UserRepo } from "./repositories/UserRepo.js";
7
- export { EINLADUNG_COOKIE, EINLADUNG_GUELTIG_TAGE, EinladungService, type EinladungError } from "./service/einladung.service.js";
8
- export { sendEinladungEmail, sendPasswordResetEmail, sendVerifyEmail } from "./service/mail.js";
4
+ export { authLayoutLoad, einladungLoad, homeLayoutLoad, loginLoad, manageUserLoad, signupLoad, type AuthUiData } from "./loads.js";
5
+ export { einladungRepo, type EinladungRepo } from "./repositories/EinladungRepo.js";
6
+ export { userRepo, type UserRepo } from "./repositories/UserRepo.js";
7
+ export { EINLADUNG_COOKIE, EINLADUNG_GUELTIG_TAGE, einladungService, type EinladungError, type EinladungService } from "./service/einladung.service.js";
8
+ export { authMails, type AuthMails } from "./service/mail.js";
@@ -1,8 +1,8 @@
1
- export { authOptions, registerAuth } from "./create.js";
1
+ export { authContext, contextOf } from "./context.js";
2
+ export { createAuth } from "./create.js";
2
3
  export { adminCommand, adminQuery, adminSchemaCommand, adminSchemaQuery, protectedCommand, protectedForm, protectedQuery, protectedSchemaCommand, protectedSchemaQuery, requireAdmin, requireUser, } from "./helpers.js";
3
4
  export { authLayoutLoad, einladungLoad, homeLayoutLoad, loginLoad, manageUserLoad, signupLoad } from "./loads.js";
4
- export { authInstance, registry as authRegistry } from "./registry.js";
5
- export { EinladungRepo } from "./repositories/EinladungRepo.js";
6
- export { UserRepo } from "./repositories/UserRepo.js";
7
- export { EINLADUNG_COOKIE, EINLADUNG_GUELTIG_TAGE, EinladungService } from "./service/einladung.service.js";
8
- export { sendEinladungEmail, sendPasswordResetEmail, sendVerifyEmail } from "./service/mail.js";
5
+ export { einladungRepo } from "./repositories/EinladungRepo.js";
6
+ export { userRepo } from "./repositories/UserRepo.js";
7
+ export { EINLADUNG_COOKIE, EINLADUNG_GUELTIG_TAGE, einladungService } from "./service/einladung.service.js";
8
+ export { authMails } from "./service/mail.js";
@@ -1,16 +1,25 @@
1
1
  /**
2
2
  * The `load` functions of the auth routes, for a Product's `+page.server.ts`/`+layout.server.ts` to re-export:
3
- * `export { loginLoad as load } from "@vonall/auth/server";`
3
+ * `export { loginLoad as load } from "@vonall/auth/server";`. The layouts' loads also carry what the components need
4
+ * to know about the app (`authUi`: its name, locale and signup mode), so the Product passes nothing twice.
4
5
  */
5
6
  import { type ServerLoadEvent } from "@sveltejs/kit";
7
+ import type { SignupMode } from "../types.js";
6
8
  type Event = Pick<ServerLoadEvent, "locals" | "url" | "request" | "cookies" | "params">;
9
+ /** What the components read from the page data (`authUi()` in `@vonall/auth/ui`). */
10
+ export type AuthUiData = {
11
+ appName: string;
12
+ locale: string;
13
+ signupMode: SignupMode;
14
+ };
7
15
  /** `/auth`: which social logins have keys, and whether a password reset mail can be sent (the link is hidden otherwise). */
8
- export declare function authLayoutLoad(): Promise<{
16
+ export declare function authLayoutLoad({ locals }: Event): Promise<{
9
17
  providers: {
10
18
  github: boolean;
11
19
  google: boolean;
12
20
  };
13
21
  mail: boolean;
22
+ authUi: AuthUiData;
14
23
  }>;
15
24
  /** `/auth/login`. */
16
25
  export declare function loginLoad({ locals, url, request }: Event): Promise<{}>;
@@ -30,12 +39,14 @@ export declare function einladungLoad({ params, cookies, locals, url }: Event):
30
39
  fehler: string;
31
40
  einladung: null;
32
41
  eingeloggt: boolean;
42
+ authUi: AuthUiData;
33
43
  } | {
34
44
  fehler: null;
35
45
  einladung: {
36
46
  inviterName: string;
37
47
  };
38
48
  eingeloggt: boolean;
49
+ authUi: AuthUiData;
39
50
  }>;
40
51
  /** `/home`: login required; the user, the session (never its token), the providers and whether the user may invite. */
41
52
  export declare function homeLayoutLoad({ locals }: Event): {
@@ -49,6 +60,7 @@ export declare function homeLayoutLoad({ locals }: Event): {
49
60
  google: boolean;
50
61
  };
51
62
  einladen: boolean;
63
+ authUi: AuthUiData;
52
64
  };
53
65
  /** `/home/manage_user` (the Admin option): admins only. */
54
66
  export declare function manageUserLoad({ locals }: Event): {