@haruhimemoe/next-kit 0.1.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 (78) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -0
  4. package/dist/auth/create.d.ts +204 -0
  5. package/dist/auth/create.js +102 -0
  6. package/dist/auth/index.d.ts +13 -0
  7. package/dist/auth/index.js +13 -0
  8. package/dist/auth/indexes.d.ts +23 -0
  9. package/dist/auth/indexes.js +44 -0
  10. package/dist/auth/osu-id.d.ts +10 -0
  11. package/dist/auth/osu-id.js +10 -0
  12. package/dist/auth/osu.d.ts +80 -0
  13. package/dist/auth/osu.js +67 -0
  14. package/dist/auth/session.d.ts +48 -0
  15. package/dist/auth/session.js +32 -0
  16. package/dist/auth-react/RestoreSignedIn.d.ts +33 -0
  17. package/dist/auth-react/RestoreSignedIn.js +38 -0
  18. package/dist/auth-react/account-store.d.ts +55 -0
  19. package/dist/auth-react/account-store.js +78 -0
  20. package/dist/auth-react/index.d.ts +17 -0
  21. package/dist/auth-react/index.js +17 -0
  22. package/dist/auth-react/marker.d.ts +35 -0
  23. package/dist/auth-react/marker.js +30 -0
  24. package/dist/auth-react/sign-in.d.ts +24 -0
  25. package/dist/auth-react/sign-in.js +24 -0
  26. package/dist/auth-react/use-account.d.ts +52 -0
  27. package/dist/auth-react/use-account.js +52 -0
  28. package/dist/env/errors.d.ts +18 -0
  29. package/dist/env/errors.js +21 -0
  30. package/dist/env/index.d.ts +13 -0
  31. package/dist/env/index.js +13 -0
  32. package/dist/env/optional.d.ts +54 -0
  33. package/dist/env/optional.js +84 -0
  34. package/dist/env/osu-app.d.ts +24 -0
  35. package/dist/env/osu-app.js +32 -0
  36. package/dist/env/server-env.d.ts +63 -0
  37. package/dist/env/server-env.js +88 -0
  38. package/dist/mongo/client.d.ts +56 -0
  39. package/dist/mongo/client.js +72 -0
  40. package/dist/mongo/collections.d.ts +16 -0
  41. package/dist/mongo/collections.js +29 -0
  42. package/dist/mongo/duplicate.d.ts +16 -0
  43. package/dist/mongo/duplicate.js +16 -0
  44. package/dist/mongo/index.d.ts +13 -0
  45. package/dist/mongo/index.js +13 -0
  46. package/dist/mongo/indexes.d.ts +55 -0
  47. package/dist/mongo/indexes.js +81 -0
  48. package/dist/server/body.d.ts +57 -0
  49. package/dist/server/body.js +74 -0
  50. package/dist/server/budget.d.ts +39 -0
  51. package/dist/server/budget.js +46 -0
  52. package/dist/server/client-ip.d.ts +30 -0
  53. package/dist/server/client-ip.js +92 -0
  54. package/dist/server/counter.d.ts +80 -0
  55. package/dist/server/counter.js +76 -0
  56. package/dist/server/cross-site.d.ts +32 -0
  57. package/dist/server/cross-site.js +36 -0
  58. package/dist/server/errors.d.ts +57 -0
  59. package/dist/server/errors.js +55 -0
  60. package/dist/server/index.d.ts +20 -0
  61. package/dist/server/index.js +20 -0
  62. package/dist/server/machine-auth.d.ts +55 -0
  63. package/dist/server/machine-auth.js +74 -0
  64. package/dist/server/rate-limit.d.ts +75 -0
  65. package/dist/server/rate-limit.js +101 -0
  66. package/dist/server/safe-next.d.ts +36 -0
  67. package/dist/server/safe-next.js +42 -0
  68. package/dist/server/security-txt.d.ts +32 -0
  69. package/dist/server/security-txt.js +31 -0
  70. package/dist/testing/env.d.ts +29 -0
  71. package/dist/testing/env.js +33 -0
  72. package/dist/testing/index.d.ts +13 -0
  73. package/dist/testing/index.js +13 -0
  74. package/dist/testing/mongo.d.ts +44 -0
  75. package/dist/testing/mongo.js +46 -0
  76. package/dist/testing/msw.d.ts +18 -0
  77. package/dist/testing/msw.js +24 -0
  78. package/package.json +140 -0
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file src/auth/osu-id.ts
3
+ * @desc The osu! provider id on its own, so browser code (osuSignIn) can use it without loading
4
+ * the profile mapping.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Mon Sep 28, 2026
7
+ * @modified Mon Sep 28, 2026
8
+ */
9
+ /** better-auth genericOAuth provider id; also the last segment of the callback path. */
10
+ export declare const OSU_PROVIDER_ID = "osu";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file src/auth/osu-id.ts
3
+ * @desc The osu! provider id on its own, so browser code (osuSignIn) can use it without loading
4
+ * the profile mapping.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Mon Sep 28, 2026
7
+ * @modified Mon Sep 28, 2026
8
+ */
9
+ /** better-auth genericOAuth provider id; also the last segment of the callback path. */
10
+ export const OSU_PROVIDER_ID = "osu";
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @file src/auth/osu.ts
3
+ * @desc The osu! pieces of better-auth: the provider id, the extra user fields, the profile
4
+ * mapping (through @haruhimemoe/osu's toOsuUser, with a synthetic email since osu! gives
5
+ * none), the genericOAuth provider config (identify + public, PKCE, profile refreshed on
6
+ * every sign-in), and dropping OAuth tokens from account writes. Moved from packs and
7
+ * pools (src/lib/auth.ts, src/constants/auth.ts).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ import { OSU_PROVIDER_ID } from "./osu-id.js";
13
+ export { OSU_PROVIDER_ID };
14
+ /** The fields osu! fills in on every user row. */
15
+ export declare const OSU_USER_FIELDS: {
16
+ readonly osuId: {
17
+ readonly type: "number";
18
+ readonly required: true;
19
+ };
20
+ readonly username: {
21
+ readonly type: "string";
22
+ readonly required: true;
23
+ };
24
+ readonly avatarUrl: {
25
+ readonly type: "string";
26
+ readonly required: false;
27
+ };
28
+ readonly countryCode: {
29
+ readonly type: "string";
30
+ readonly required: false;
31
+ };
32
+ };
33
+ /**
34
+ * @function osuProfileToUser
35
+ * @param raw {unknown} the /api/v2/me profile better-auth fetched
36
+ * @returns the better-auth user fields. osu! OAuth returns no email, so each osu! id gets a stable
37
+ * synthetic one (the adapter requires an email). better-auth wants image left out, not
38
+ * null, when there is no avatar.
39
+ * @throws {z.ZodError} when the profile has no id or username (never guess an identity)
40
+ */
41
+ export declare const osuProfileToUser: (raw: unknown) => {
42
+ osuId: number;
43
+ username: string;
44
+ avatarUrl: string | null;
45
+ countryCode: string | null;
46
+ email: string;
47
+ emailVerified: false;
48
+ name: string;
49
+ image?: string;
50
+ };
51
+ /**
52
+ * @function withoutTokens
53
+ * @param account {T} an account write
54
+ * @returns {T} the same fields with accessToken, refreshToken and idToken null: neither app calls
55
+ * osu! as the user, so none keeps osu! tokens
56
+ */
57
+ export declare const withoutTokens: <T extends Record<string, unknown>>(account: T) => T;
58
+ /** osuProvider's input: the osu! OAuth app's credentials. */
59
+ export type OsuProviderOptions = {
60
+ clientId: string;
61
+ clientSecret: string;
62
+ };
63
+ /**
64
+ * @function osuProvider
65
+ * @param options {OsuProviderOptions} the osu! app's client id and secret
66
+ * @returns the genericOAuth config entry for osu!: identify + public scopes, PKCE, the profile
67
+ * mapped with osuProfileToUser and refreshed on every sign-in
68
+ */
69
+ export declare const osuProvider: ({ clientId, clientSecret }: OsuProviderOptions) => {
70
+ authorizationUrl: "https://osu.ppy.sh/oauth/authorize";
71
+ tokenUrl: "https://osu.ppy.sh/oauth/token";
72
+ userInfoUrl: "https://osu.ppy.sh/api/v2/me";
73
+ providerId: string;
74
+ clientId: string;
75
+ clientSecret: string;
76
+ scopes: ("identify" | "public")[];
77
+ pkce: boolean;
78
+ overrideUserInfo: boolean;
79
+ mapProfileToUser: typeof osuProfileToUser;
80
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file src/auth/osu.ts
3
+ * @desc The osu! pieces of better-auth: the provider id, the extra user fields, the profile
4
+ * mapping (through @haruhimemoe/osu's toOsuUser, with a synthetic email since osu! gives
5
+ * none), the genericOAuth provider config (identify + public, PKCE, profile refreshed on
6
+ * every sign-in), and dropping OAuth tokens from account writes. Moved from packs and
7
+ * pools (src/lib/auth.ts, src/constants/auth.ts).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ import { OSU_OAUTH, OSU_SIGN_IN_SCOPES, toOsuUser } from "@haruhimemoe/osu/shapes";
13
+ import { OSU_PROVIDER_ID } from "./osu-id.js";
14
+ export { OSU_PROVIDER_ID };
15
+ /** The fields osu! fills in on every user row. */
16
+ export const OSU_USER_FIELDS = {
17
+ osuId: { type: "number", required: true },
18
+ username: { type: "string", required: true },
19
+ avatarUrl: { type: "string", required: false },
20
+ countryCode: { type: "string", required: false },
21
+ };
22
+ /**
23
+ * @function osuProfileToUser
24
+ * @param raw {unknown} the /api/v2/me profile better-auth fetched
25
+ * @returns the better-auth user fields. osu! OAuth returns no email, so each osu! id gets a stable
26
+ * synthetic one (the adapter requires an email). better-auth wants image left out, not
27
+ * null, when there is no avatar.
28
+ * @throws {z.ZodError} when the profile has no id or username (never guess an identity)
29
+ */
30
+ export const osuProfileToUser = (raw) => {
31
+ const user = toOsuUser(raw);
32
+ return {
33
+ email: `${user.osuId}@osu.local`,
34
+ emailVerified: false,
35
+ name: user.username,
36
+ ...user,
37
+ ...(user.avatarUrl ? { image: user.avatarUrl } : {}),
38
+ };
39
+ };
40
+ /**
41
+ * @function withoutTokens
42
+ * @param account {T} an account write
43
+ * @returns {T} the same fields with accessToken, refreshToken and idToken null: neither app calls
44
+ * osu! as the user, so none keeps osu! tokens
45
+ */
46
+ export const withoutTokens = (account) => ({
47
+ ...account,
48
+ accessToken: null,
49
+ refreshToken: null,
50
+ idToken: null,
51
+ });
52
+ /**
53
+ * @function osuProvider
54
+ * @param options {OsuProviderOptions} the osu! app's client id and secret
55
+ * @returns the genericOAuth config entry for osu!: identify + public scopes, PKCE, the profile
56
+ * mapped with osuProfileToUser and refreshed on every sign-in
57
+ */
58
+ export const osuProvider = ({ clientId, clientSecret }) => ({
59
+ providerId: OSU_PROVIDER_ID,
60
+ clientId,
61
+ clientSecret,
62
+ ...OSU_OAUTH,
63
+ scopes: [...OSU_SIGN_IN_SCOPES],
64
+ pkce: true,
65
+ overrideUserInfo: true,
66
+ mapProfileToUser: osuProfileToUser,
67
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @file src/auth/session.ts
3
+ * @desc Reading the caller from a request's session cookie. Route handlers pass
4
+ * request.headers; server pages pass await headers(). Apps add their own rules on top
5
+ * (an admin list, refusing system accounts). Moved from packs and pools
6
+ * (getUserFromHeaders in src/lib/auth.ts).
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ /** The signed-in osu! user, as both apps pass it around. */
12
+ export type OsuSessionUser = {
13
+ id: string;
14
+ osuId: number;
15
+ username: string;
16
+ avatarUrl: string | null;
17
+ };
18
+ /** A session as better-auth returns it with the osu! user fields. */
19
+ export type OsuSession = {
20
+ user: {
21
+ id: string;
22
+ osuId: number;
23
+ username: string;
24
+ avatarUrl?: string | null | undefined;
25
+ };
26
+ };
27
+ /** What getOsuUser needs from a better-auth instance. */
28
+ export type SessionReader = {
29
+ api: {
30
+ getSession: (input: {
31
+ headers: Headers;
32
+ }) => Promise<OsuSession | null>;
33
+ };
34
+ };
35
+ /**
36
+ * @function toSessionUser
37
+ * @param session {OsuSession} a session better-auth returned
38
+ * @returns {OsuSessionUser} id, osu! id, username and avatar (null when there is none)
39
+ */
40
+ export declare const toSessionUser: ({ user }: OsuSession) => OsuSessionUser;
41
+ /**
42
+ * @function getOsuUser
43
+ * @param auth {SessionReader} the better-auth instance (createOsuAuth's)
44
+ * @param headers {Headers} request headers (the session cookie)
45
+ * @returns {Promise<OsuSessionUser | null>} the signed-in user, or null (no, forged or expired
46
+ * session)
47
+ */
48
+ export declare const getOsuUser: (auth: SessionReader, headers: Headers) => Promise<OsuSessionUser | null>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file src/auth/session.ts
3
+ * @desc Reading the caller from a request's session cookie. Route handlers pass
4
+ * request.headers; server pages pass await headers(). Apps add their own rules on top
5
+ * (an admin list, refusing system accounts). Moved from packs and pools
6
+ * (getUserFromHeaders in src/lib/auth.ts).
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ /**
12
+ * @function toSessionUser
13
+ * @param session {OsuSession} a session better-auth returned
14
+ * @returns {OsuSessionUser} id, osu! id, username and avatar (null when there is none)
15
+ */
16
+ export const toSessionUser = ({ user }) => ({
17
+ id: user.id,
18
+ osuId: user.osuId,
19
+ username: user.username,
20
+ avatarUrl: user.avatarUrl ?? null,
21
+ });
22
+ /**
23
+ * @function getOsuUser
24
+ * @param auth {SessionReader} the better-auth instance (createOsuAuth's)
25
+ * @param headers {Headers} request headers (the session cookie)
26
+ * @returns {Promise<OsuSessionUser | null>} the signed-in user, or null (no, forged or expired
27
+ * session)
28
+ */
29
+ export const getOsuUser = async (auth, headers) => {
30
+ const session = await auth.api.getSession({ headers });
31
+ return session ? toSessionUser(session) : null;
32
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @file src/auth-react/RestoreSignedIn.tsx
3
+ * @desc For pages that know on the server that the visitor is signed in (the sign-in page with a
4
+ * session, an account page): if this browser has no signed-in marker (a session from before
5
+ * the marker existed, or a cleared cookie), ask for the session once, which sets the marker
6
+ * and fixes the header. With `next`, the sign-in page then continues there. Moved from packs
7
+ * and pools (src/components/auth/RestoreSignedIn.tsx, body-identical).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ import { type ReactNode } from "react";
13
+ /** RestoreSignedIn's props. */
14
+ export type RestoreSignedInProps = {
15
+ /** The page-wide account store (createAccount's `store`). */
16
+ store: {
17
+ recheck: () => Promise<void>;
18
+ };
19
+ /** The app's marker check (createSignedInMarker's `has`). */
20
+ hasMarker: (cookieHeader: string) => boolean;
21
+ /** Where to go once the session is restored (the sign-in page's `next`). */
22
+ next?: string;
23
+ /** Shown while it goes on to `next`. */
24
+ pending?: ReactNode;
25
+ /** Test seam (default: document.cookie). */
26
+ readCookie?: () => string;
27
+ };
28
+ /**
29
+ * @function RestoreSignedIn
30
+ * @param props {RestoreSignedInProps} the store, the marker check, `next` and what to show
31
+ * @returns {ReactNode} nothing without `next`; `pending` while it goes on to `next`
32
+ */
33
+ export declare function RestoreSignedIn({ store, hasMarker, next, pending, readCookie, }: RestoreSignedInProps): ReactNode;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @file src/auth-react/RestoreSignedIn.tsx
3
+ * @desc For pages that know on the server that the visitor is signed in (the sign-in page with a
4
+ * session, an account page): if this browser has no signed-in marker (a session from before
5
+ * the marker existed, or a cleared cookie), ask for the session once, which sets the marker
6
+ * and fixes the header. With `next`, the sign-in page then continues there. Moved from packs
7
+ * and pools (src/components/auth/RestoreSignedIn.tsx, body-identical).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ "use client";
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ import { useRouter } from "next/navigation.js";
15
+ import { useEffect } from "react";
16
+ const readDocumentCookie = () => document.cookie;
17
+ const SIGNING_IN = _jsx("p", { className: "text-c3 text-sm", children: "Signing you in\u2026" });
18
+ /**
19
+ * @function RestoreSignedIn
20
+ * @param props {RestoreSignedInProps} the store, the marker check, `next` and what to show
21
+ * @returns {ReactNode} nothing without `next`; `pending` while it goes on to `next`
22
+ */
23
+ export function RestoreSignedIn({ store, hasMarker, next, pending = SIGNING_IN, readCookie = readDocumentCookie, }) {
24
+ const router = useRouter();
25
+ useEffect(() => {
26
+ if (next === undefined && hasMarker(readCookie()))
27
+ return;
28
+ let live = true;
29
+ store.recheck().then(() => {
30
+ if (live && next !== undefined)
31
+ router.replace(next);
32
+ });
33
+ return () => {
34
+ live = false;
35
+ };
36
+ }, [next, store, hasMarker, readCookie, router]);
37
+ return next === undefined ? null : pending;
38
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @file src/auth-react/account-store.ts
3
+ * @desc Who is signed in, for a header's account menu. The store asks the server only when the
4
+ * readable signed-in marker is present, once per page load, so anonymous visitors cost no
5
+ * request; another tab signing in or out is caught up when this one is shown again, at a
6
+ * request only when the marker changed. markSignedOut() updates every subscriber at once.
7
+ * Moved from packs and pools (src/hooks/useAccount.ts, body-identical).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ /** The account as the page sees it. */
13
+ export type Account = {
14
+ status: "loading";
15
+ } | {
16
+ status: "signed-out";
17
+ } | {
18
+ status: "signed-in";
19
+ user: {
20
+ id: string;
21
+ username: string;
22
+ avatarUrl: string | null;
23
+ };
24
+ };
25
+ /** The part of a better-auth session the store reads. */
26
+ export type SessionData = {
27
+ user: {
28
+ id: string;
29
+ username: string;
30
+ avatarUrl?: string | null;
31
+ };
32
+ };
33
+ /** What the store needs: a session fetch, a cookie read and a marker clear (tests pass fakes). */
34
+ export type AccountDeps = {
35
+ getSession: () => Promise<SessionData | null>;
36
+ readCookie: () => string;
37
+ hasMarker: (cookieHeader: string) => boolean;
38
+ clearMarker: () => void;
39
+ };
40
+ /** A useSyncExternalStore store of the account. */
41
+ export type AccountStore = {
42
+ subscribe: (listener: () => void) => () => void;
43
+ getSnapshot: () => Account;
44
+ markSignedOut: () => void;
45
+ /** Ask the server even without the marker (pages that know the user is signed in). */
46
+ recheck: () => Promise<void>;
47
+ };
48
+ /** Before the first answer, and during server render. */
49
+ export declare const LOADING: Account;
50
+ /**
51
+ * @function createAccountStore
52
+ * @param deps {AccountDeps} session fetch, cookie read, marker check and clear
53
+ * @returns {AccountStore} starts on the first subscription
54
+ */
55
+ export declare const createAccountStore: ({ getSession, readCookie, hasMarker, clearMarker, }: AccountDeps) => AccountStore;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @file src/auth-react/account-store.ts
3
+ * @desc Who is signed in, for a header's account menu. The store asks the server only when the
4
+ * readable signed-in marker is present, once per page load, so anonymous visitors cost no
5
+ * request; another tab signing in or out is caught up when this one is shown again, at a
6
+ * request only when the marker changed. markSignedOut() updates every subscriber at once.
7
+ * Moved from packs and pools (src/hooks/useAccount.ts, body-identical).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ /** Before the first answer, and during server render. */
13
+ export const LOADING = Object.freeze({ status: "loading" });
14
+ const SIGNED_OUT = Object.freeze({ status: "signed-out" });
15
+ /**
16
+ * @function createAccountStore
17
+ * @param deps {AccountDeps} session fetch, cookie read, marker check and clear
18
+ * @returns {AccountStore} starts on the first subscription
19
+ */
20
+ export const createAccountStore = ({ getSession, readCookie, hasMarker, clearMarker, }) => {
21
+ let state = LOADING;
22
+ let started = false;
23
+ const listeners = new Set();
24
+ const set = (next) => {
25
+ state = next;
26
+ for (const listener of listeners)
27
+ listener();
28
+ };
29
+ // Asks the server; a session there also (re)sets the marker cookie.
30
+ const load = () => getSession().then((data) => {
31
+ if (!data) {
32
+ clearMarker();
33
+ set(SIGNED_OUT);
34
+ return;
35
+ }
36
+ const { id, username, avatarUrl } = data.user;
37
+ set({ status: "signed-in", user: { id, username, avatarUrl: avatarUrl ?? null } });
38
+ }, () => set(SIGNED_OUT));
39
+ const sync = () => {
40
+ if (document.visibilityState === "hidden")
41
+ return;
42
+ const marked = hasMarker(readCookie());
43
+ if (!marked && state.status === "signed-in")
44
+ set(SIGNED_OUT);
45
+ else if (marked && state.status === "signed-out")
46
+ void load();
47
+ };
48
+ const start = () => {
49
+ if (started)
50
+ return;
51
+ started = true;
52
+ if (typeof document !== "undefined")
53
+ document.addEventListener("visibilitychange", sync);
54
+ if (!hasMarker(readCookie())) {
55
+ set(SIGNED_OUT);
56
+ return;
57
+ }
58
+ void load();
59
+ };
60
+ return {
61
+ subscribe: (listener) => {
62
+ listeners.add(listener);
63
+ start();
64
+ return () => {
65
+ listeners.delete(listener);
66
+ };
67
+ },
68
+ getSnapshot: () => state,
69
+ markSignedOut: () => {
70
+ clearMarker();
71
+ set(SIGNED_OUT);
72
+ },
73
+ recheck: () => {
74
+ started = true;
75
+ return load();
76
+ },
77
+ };
78
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @file src/auth-react/index.ts
3
+ * @desc @haruhimemoe/next-kit/auth-react: the browser side of osu! sign-in. The signed-in marker
4
+ * cookie, the account store and useAccount, RestoreSignedIn, what the sign-in button
5
+ * sends, and where to go after sign-in. No "use client" here: the hook and component
6
+ * files carry it, so a server page can still call safeNextPath from this entry point.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ export { OSU_PROVIDER_ID } from "../auth/osu-id.js";
12
+ export { DEFAULT_SIGN_IN_PATH, type SafeNextOptions, safeNextPath, signInHref, } from "../server/safe-next.js";
13
+ export { type Account, type AccountDeps, type AccountStore, createAccountStore, LOADING, type SessionData, } from "./account-store.js";
14
+ export { createSignedInMarker, markerMaxAge, type SignedInMarker } from "./marker.js";
15
+ export { RestoreSignedIn, type RestoreSignedInProps } from "./RestoreSignedIn.js";
16
+ export { type OsuSignIn, osuSignIn } from "./sign-in.js";
17
+ export { type AccountKit, type BoundRestoreProps, createAccount, type SessionClient, sessionFetcher, useAccount, } from "./use-account.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @file src/auth-react/index.ts
3
+ * @desc @haruhimemoe/next-kit/auth-react: the browser side of osu! sign-in. The signed-in marker
4
+ * cookie, the account store and useAccount, RestoreSignedIn, what the sign-in button
5
+ * sends, and where to go after sign-in. No "use client" here: the hook and component
6
+ * files carry it, so a server page can still call safeNextPath from this entry point.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ export { OSU_PROVIDER_ID } from "../auth/osu-id.js";
12
+ export { DEFAULT_SIGN_IN_PATH, safeNextPath, signInHref, } from "../server/safe-next.js";
13
+ export { createAccountStore, LOADING, } from "./account-store.js";
14
+ export { createSignedInMarker, markerMaxAge } from "./marker.js";
15
+ export { RestoreSignedIn } from "./RestoreSignedIn.js";
16
+ export { osuSignIn } from "./sign-in.js";
17
+ export { createAccount, sessionFetcher, useAccount, } from "./use-account.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @file src/auth-react/marker.ts
3
+ * @desc A readable "this browser may be signed in" cookie. It holds no secret; it only tells the
4
+ * page whether asking the server for the session is worth a request, so visitors who never
5
+ * sign in cost none. The server sets it with the session and clears it on sign-out or a
6
+ * missing session (createOsuAuth in @haruhimemoe/next-kit/auth). Browser-safe. Moved from
7
+ * packs and pools (src/lib/signed-in-marker.ts), which differed only in the cookie name.
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ /** The marker for one cookie name. */
13
+ export type SignedInMarker = {
14
+ /** The cookie's name, like "pools-signed-in". */
15
+ name: string;
16
+ /** True when a Cookie header (or document.cookie) has the marker set. */
17
+ has: (cookieHeader: string) => boolean;
18
+ /** Expires the marker on a document (tests pass a stand-in). */
19
+ clear: (target?: {
20
+ cookie: string;
21
+ }) => void;
22
+ };
23
+ /**
24
+ * @function markerMaxAge
25
+ * @param expiresAt {Date | string} when the session expires
26
+ * @param now {number} current time in ms (tests)
27
+ * @returns {number} whole seconds until then, at least 0
28
+ */
29
+ export declare const markerMaxAge: (expiresAt: Date | string, now?: number) => number;
30
+ /**
31
+ * @function createSignedInMarker
32
+ * @param name {string} the cookie's name, one per app
33
+ * @returns {SignedInMarker} has and clear for that cookie
34
+ */
35
+ export declare const createSignedInMarker: (name: string) => SignedInMarker;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file src/auth-react/marker.ts
3
+ * @desc A readable "this browser may be signed in" cookie. It holds no secret; it only tells the
4
+ * page whether asking the server for the session is worth a request, so visitors who never
5
+ * sign in cost none. The server sets it with the session and clears it on sign-out or a
6
+ * missing session (createOsuAuth in @haruhimemoe/next-kit/auth). Browser-safe. Moved from
7
+ * packs and pools (src/lib/signed-in-marker.ts), which differed only in the cookie name.
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Mon Sep 28, 2026
10
+ * @modified Mon Sep 28, 2026
11
+ */
12
+ /**
13
+ * @function markerMaxAge
14
+ * @param expiresAt {Date | string} when the session expires
15
+ * @param now {number} current time in ms (tests)
16
+ * @returns {number} whole seconds until then, at least 0
17
+ */
18
+ export const markerMaxAge = (expiresAt, now = Date.now()) => Math.max(0, Math.floor((new Date(expiresAt).getTime() - now) / 1000));
19
+ /**
20
+ * @function createSignedInMarker
21
+ * @param name {string} the cookie's name, one per app
22
+ * @returns {SignedInMarker} has and clear for that cookie
23
+ */
24
+ export const createSignedInMarker = (name) => ({
25
+ name,
26
+ has: (cookieHeader) => cookieHeader.split(/;\s*/).includes(`${name}=1`),
27
+ clear: (target = document) => {
28
+ target.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
29
+ },
30
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file src/auth-react/sign-in.ts
3
+ * @desc What a "Sign in with osu!" button sends better-auth: the provider, where to land, and an
4
+ * error page that keeps `next`, so a failed sign-in explains itself and can still go on
5
+ * (better-auth adds &error=<code>). pools did this; packs sent /signin?error=oauth and
6
+ * lost `next`.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ /** The body of authClient.signIn.social for osu!. */
12
+ export type OsuSignIn = {
13
+ provider: string;
14
+ callbackURL: string;
15
+ errorCallbackURL: string;
16
+ };
17
+ /**
18
+ * @function osuSignIn
19
+ * @param next {string} where to land after signing in (already checked with safeNextPath)
20
+ * @param signInPath {string} the sign-in page (default /signin)
21
+ * @returns {OsuSignIn} { provider: "osu", callbackURL: next, errorCallbackURL:
22
+ * "/signin?next=<next>" }
23
+ */
24
+ export declare const osuSignIn: (next: string, signInPath?: string) => OsuSignIn;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file src/auth-react/sign-in.ts
3
+ * @desc What a "Sign in with osu!" button sends better-auth: the provider, where to land, and an
4
+ * error page that keeps `next`, so a failed sign-in explains itself and can still go on
5
+ * (better-auth adds &error=<code>). pools did this; packs sent /signin?error=oauth and
6
+ * lost `next`.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ import { OSU_PROVIDER_ID } from "../auth/osu-id.js";
12
+ import { DEFAULT_SIGN_IN_PATH, signInHref } from "../server/safe-next.js";
13
+ /**
14
+ * @function osuSignIn
15
+ * @param next {string} where to land after signing in (already checked with safeNextPath)
16
+ * @param signInPath {string} the sign-in page (default /signin)
17
+ * @returns {OsuSignIn} { provider: "osu", callbackURL: next, errorCallbackURL:
18
+ * "/signin?next=<next>" }
19
+ */
20
+ export const osuSignIn = (next, signInPath = DEFAULT_SIGN_IN_PATH) => ({
21
+ provider: OSU_PROVIDER_ID,
22
+ callbackURL: next,
23
+ errorCallbackURL: signInHref(next, signInPath),
24
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @file src/auth-react/use-account.ts
3
+ * @desc The React side of the account store: useAccount (loading during server render), and
4
+ * createAccount, which wires a store to a better-auth client and the app's marker cookie
5
+ * the way packs and pools each did at the bottom of src/hooks/useAccount.ts.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Mon Sep 28, 2026
8
+ * @modified Mon Sep 28, 2026
9
+ */
10
+ import { type ReactNode } from "react";
11
+ import { type Account, type AccountStore, type SessionData } from "./account-store.js";
12
+ import type { SignedInMarker } from "./marker.js";
13
+ /** The part of a better-auth client the store calls. */
14
+ export type SessionClient = {
15
+ getSession: () => Promise<{
16
+ data?: unknown;
17
+ error?: unknown;
18
+ }>;
19
+ };
20
+ /**
21
+ * @function sessionFetcher
22
+ * @param client {SessionClient} the app's better-auth client
23
+ * @returns {() => Promise<SessionData | null>} a getSession that throws better-auth's error
24
+ */
25
+ export declare const sessionFetcher: (client: SessionClient) => () => Promise<SessionData | null>;
26
+ /**
27
+ * @function useAccount
28
+ * @param store {AccountStore} the page-wide store
29
+ * @returns {Account} current account state ("loading" during server render)
30
+ */
31
+ export declare const useAccount: (store: AccountStore) => Account;
32
+ /** RestoreSignedIn with the store and marker already bound: what a server page can render. */
33
+ export type BoundRestoreProps = {
34
+ next?: string;
35
+ pending?: ReactNode;
36
+ };
37
+ /** What createAccount returns: the store, and the hook, sign-out and component bound to it. */
38
+ export type AccountKit = {
39
+ store: AccountStore;
40
+ useAccount: () => Account;
41
+ markSignedOut: () => void;
42
+ RestoreSignedIn: (props: BoundRestoreProps) => ReactNode;
43
+ };
44
+ /**
45
+ * @function createAccount
46
+ * @param client {SessionClient} the app's better-auth client
47
+ * @param marker {SignedInMarker} the app's signed-in marker
48
+ * @returns {AccountKit} a page-wide store reading document.cookie, with its hook, sign-out and
49
+ * RestoreSignedIn (export them from the app's own "use client" module, so server pages
50
+ * render <RestoreSignedIn next={next} /> with only serializable props)
51
+ */
52
+ export declare const createAccount: (client: SessionClient, marker: SignedInMarker) => AccountKit;