@bitbaum/accountkit 0.0.0-stage → 0.6.3

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -2
  3. package/dist/AccountMenu.d.ts +76 -0
  4. package/dist/AccountMenu.d.ts.map +1 -0
  5. package/dist/AccountMenu.js +156 -0
  6. package/dist/AccountMenu.js.map +1 -0
  7. package/dist/SignInError.d.ts +50 -0
  8. package/dist/SignInError.d.ts.map +1 -0
  9. package/dist/SignInError.js +34 -0
  10. package/dist/SignInError.js.map +1 -0
  11. package/dist/auth/client.d.ts +19 -0
  12. package/dist/auth/client.d.ts.map +1 -0
  13. package/dist/auth/client.js +17 -0
  14. package/dist/auth/client.js.map +1 -0
  15. package/dist/auth/factor.d.ts +9 -0
  16. package/dist/auth/factor.d.ts.map +1 -0
  17. package/dist/auth/factor.js +107 -0
  18. package/dist/auth/factor.js.map +1 -0
  19. package/dist/auth/index.d.ts +6 -0
  20. package/dist/auth/index.d.ts.map +1 -0
  21. package/dist/auth/index.js +6 -0
  22. package/dist/auth/index.js.map +1 -0
  23. package/dist/auth/labels.d.ts +114 -0
  24. package/dist/auth/labels.d.ts.map +1 -0
  25. package/dist/auth/labels.js +132 -0
  26. package/dist/auth/labels.js.map +1 -0
  27. package/dist/auth/limits.d.ts +28 -0
  28. package/dist/auth/limits.d.ts.map +1 -0
  29. package/dist/auth/limits.js +90 -0
  30. package/dist/auth/limits.js.map +1 -0
  31. package/dist/auth/mail.d.ts +10 -0
  32. package/dist/auth/mail.d.ts.map +1 -0
  33. package/dist/auth/mail.js +45 -0
  34. package/dist/auth/mail.js.map +1 -0
  35. package/dist/auth/methods.d.ts +71 -0
  36. package/dist/auth/methods.d.ts.map +1 -0
  37. package/dist/auth/methods.js +92 -0
  38. package/dist/auth/methods.js.map +1 -0
  39. package/dist/auth/next.d.ts +73 -0
  40. package/dist/auth/next.d.ts.map +1 -0
  41. package/dist/auth/next.js +131 -0
  42. package/dist/auth/next.js.map +1 -0
  43. package/dist/auth/react.d.ts +174 -0
  44. package/dist/auth/react.d.ts.map +1 -0
  45. package/dist/auth/react.js +396 -0
  46. package/dist/auth/react.js.map +1 -0
  47. package/dist/auth/recovery.d.ts +17 -0
  48. package/dist/auth/recovery.d.ts.map +1 -0
  49. package/dist/auth/recovery.js +85 -0
  50. package/dist/auth/recovery.js.map +1 -0
  51. package/dist/auth/roles.d.ts +19 -0
  52. package/dist/auth/roles.d.ts.map +1 -0
  53. package/dist/auth/roles.js +29 -0
  54. package/dist/auth/roles.js.map +1 -0
  55. package/dist/auth/schema.d.ts +1923 -0
  56. package/dist/auth/schema.d.ts.map +1 -0
  57. package/dist/auth/schema.js +120 -0
  58. package/dist/auth/schema.js.map +1 -0
  59. package/dist/auth/server.d.ts +56 -0
  60. package/dist/auth/server.d.ts.map +1 -0
  61. package/dist/auth/server.js +229 -0
  62. package/dist/auth/server.js.map +1 -0
  63. package/dist/auth/translations.d.ts +9 -0
  64. package/dist/auth/translations.d.ts.map +1 -0
  65. package/dist/auth/translations.js +513 -0
  66. package/dist/auth/translations.js.map +1 -0
  67. package/dist/identity.d.ts +50 -0
  68. package/dist/identity.d.ts.map +1 -0
  69. package/dist/identity.js +76 -0
  70. package/dist/identity.js.map +1 -0
  71. package/dist/index.d.ts +4 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +4 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/orangecat.d.ts +333 -0
  76. package/dist/orangecat.d.ts.map +1 -0
  77. package/dist/orangecat.js +508 -0
  78. package/dist/orangecat.js.map +1 -0
  79. package/dist/paths.d.ts +3 -0
  80. package/dist/paths.d.ts.map +1 -0
  81. package/dist/paths.js +11 -0
  82. package/dist/paths.js.map +1 -0
  83. package/package.json +145 -4
  84. package/styles.css +383 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bitbaum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,204 @@
1
- # Temporary Holding Version
1
+ # @bitbaum/accountkit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One account for every bitbaum product: the account menu (who is signed in, where to go, how to sign out), and **Sign in with OrangeCat** (`@bitbaum/accountkit/orangecat`).
4
+
5
+ Each product had written its own — Loki's `AccountMenu`, OrangeCat's `UserProfileDropdown` — and each had learned half of what the other had. Loki took initials only from the name; OrangeCat never rebuilt a name from the email address; OrangeCat had arrow keys and a 44px target, Loki had neither; Loki's public pages had no menu at all, so a signed-in person could not find sign-out. This is the one menu, with both halves.
6
+
7
+ It knows nothing about how you sign in. NextAuth, Supabase, better-auth: you pass the person, the places, and a sign-out function.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pnpm add github:bitbaum/accountkit#v0.6.2
13
+ ```
14
+
15
+ `dist/` is committed, so the `github:` install needs no build step and no `allowBuilds` entry.
16
+
17
+ ## Password recovery
18
+
19
+ `SignInForm` includes password recovery when `signInMethods(env, mail.configured)`
20
+ reports `passwordReset`. It uses the product's Better Auth client and sends the
21
+ reset link back to its existing `signInPath`, preserving the local `next` path.
22
+ That page must remain reachable when signed out and must render `SignInForm`
23
+ when the reset callback carries `token` or `error=INVALID_TOKEN`. The form removes
24
+ the token from browser history after reading it. Products with a separate reset
25
+ route can render `PasswordRecoveryForm` from `@bitbaum/accountkit/auth/react`.
26
+
27
+ The standard mail adapter is `createAuthMailer` from `@bitbaum/accountkit/auth/mail`:
28
+
29
+ ```ts
30
+ import { createAuthMailer } from "@bitbaum/accountkit/auth/mail";
31
+ const mail = createAuthMailer({ site: { title: "Example", url: "https://example.ch" } });
32
+ ```
33
+
34
+ Install its optional peer `@bitbaum/mail-kit`. The adapter reads deployment
35
+ configuration lazily, uses the fleet sender and the product's reply address,
36
+ and supports subject overrides. It rejects malformed or foreign-origin links
37
+ and never logs a token, address or credential. Configuration still needs a real
38
+ Resend key and working sender; this adapter does not claim delivery readiness.
39
+
40
+ The sender must actually deliver account mail: `createAuthRuntime({ mail })`
41
+ calls `mail.send("reset", to, url)`. A deployment with `mail.configured=false`
42
+ does not offer recovery and disables its server request handler. A no-op sender
43
+ does not implement recovery. `authLabelsFor(locale)` includes recovery words;
44
+ older product label overrides remain compatible through the English fallback.
45
+
46
+ The package checks the complete token/callback/reset journey against Postgres,
47
+ including account-existence parity, expiry, one-time use, session revocation and
48
+ external-redirect rejection. Its browser suite checks the forms at 320px, 390px
49
+ and desktop width. Consumers must also verify their own page wiring and delivery.
50
+
51
+ ```ts
52
+ import "@bitbaum/accountkit/styles.css";
53
+ import { AccountMenu } from "@bitbaum/accountkit";
54
+ ```
55
+
56
+ ## Use
57
+
58
+ ```tsx
59
+ <AccountMenu
60
+ user={{ name: session.user.name, email: session.user.email, image: session.user.image }}
61
+ groups={[{ id: "nav", items: [{ id: "settings", label: "Settings", href: "/settings" }] }]}
62
+ extra={<ThemeToggle />}
63
+ onSignOut={() => signOut({ callbackUrl: "/sign-in" })}
64
+ renderLink={(props) => <Link {...props} />}
65
+ />
66
+ ```
67
+
68
+ - **`onSignOut`** may return a promise. While it runs the row says so; if it rejects, the menu stays open and says sign-out did not complete. A sign-out that fails silently is the one thing this menu must never do.
69
+ - **`renderLink`** keeps your router (Next `<Link>`); the default is a plain `<a>`.
70
+ - **`extra`** is for rows that are not places, such as a theme switch.
71
+ - **`labels`** translates every string: `labels={{ trigger: "Konto: {name}", signOut: "Abmelden" }}`.
72
+
73
+ ## What is shared, and not configurable
74
+
75
+ - **The name is never built from the email.** No name: `@handle`. No handle: "Account".
76
+ - **Initials come from the name only.** No name and no photo: a person glyph.
77
+ - **A photo that fails to load** falls back to initials, then the glyph.
78
+ - **44px** trigger and rows. The panel never runs off a 320–390px screen.
79
+ - **Escape** closes and returns focus to the trigger, wherever focus was. **Arrows**, **Home** and **End** move between items. A click outside closes.
80
+
81
+ ## Theming
82
+
83
+ Every colour, radius and font is an `--acct-*` variable. The defaults read `@bitbaum/design-tokens` at zero specificity, so apps on those tokens match with no work and an app's own `:root` always wins. An app whose tokens are full colours rather than HSL triplets (Loki's are OKLCH) maps them once:
84
+
85
+ ```css
86
+ :root {
87
+ --acct-panel: var(--surface-overlay);
88
+ --acct-fg: var(--text-primary);
89
+ /* … */
90
+ }
91
+ ```
92
+
93
+ ## Accounts (`@bitbaum/accountkit/auth`)
94
+
95
+ Every product owns its accounts, and they are the same account system everywhere: email and password, a magic link, passkeys, two-factor codes, the shared bitbaum sign-in and Sign in with OrangeCat (each shown only when configured). Built on [better-auth](https://www.better-auth.com) 1.x, so a product writes none of it.
96
+
97
+ ```ts
98
+ // lib/db/schema.ts — one identical migration in every product
99
+ export * from "@bitbaum/accountkit/auth/schema";
100
+
101
+ // lib/auth.ts
102
+ import { createAuthRuntime } from "@bitbaum/accountkit/auth/next";
103
+ export const { GET, POST, getSession, requireUser, authStatus, getAuth } = createAuthRuntime({
104
+ site: { url: "https://app.example.ch", title: "Example", slug: "example" },
105
+ mail: { configured, send }, // your mail-kit sender; never log the link
106
+ isDatabaseConfigured: (env) => Boolean(env.DATABASE_URL),
107
+ getDatabase: () => db, // Drizzle, built on first use
108
+ });
109
+
110
+ // app/api/auth/[...all]/route.ts
111
+ export { GET, POST } from "@/lib/auth";
112
+
113
+ // lib/auth-client.ts
114
+ import { createAuthClient } from "better-auth/react";
115
+ import { authClientPlugins } from "@bitbaum/accountkit/auth/client";
116
+ export const authClient = createAuthClient({ plugins: authClientPlugins() });
117
+ ```
118
+
119
+ The screens are shared too (`@bitbaum/accountkit/auth/react`): `SignInForm`, `SignUpForm` and `AccountPanel`. They hold the logic and take the product's words (`labels`: `authLabelsFor(locale)` from `@bitbaum/accountkit/auth` has English, German, French, Italian, Romansh and Russian, so a product keeps no copy of its own; override `account.roles` if its roles have other names) and look (`classes`; without them the `acct-auth-*` classes in `styles.css` apply, on `--acct-*` variables):
120
+
121
+ ```tsx
122
+ <SignInForm client={authClient} methods={signInMethods(process.env, mailConfigured)}
123
+ labels={myLabels} classes={{ button: "btn-primary", input: "field" }}
124
+ next="/de/matters" signInPath="/de/sign-in" signUpLink={<Link href="/sign-up">…</Link>} />
125
+ ```
126
+
127
+ A product whose rows were keyed on an OrangeCat actor id before it had accounts passes `legacyIdentityProvider: "orangecat"`: a person whose first way in was OrangeCat keeps that id as `user.actorId`, so nothing has to be migrated; everyone else's `actorId` is their user id.
128
+
129
+ What is the same everywhere, and not configurable per product: 12-character passwords; sessions of 14 days; no IP addresses stored; an address is never an identity (a new method joins an account only when the signed-in person links it); provider tokens are never kept; a sign-up cannot choose its role; one rate limiter keyed on the proxy's hop (limitkit), with a stricter bucket for anything that sends mail. `signInMethods(env, mailConfigured)` says which buttons to show, as booleans.
130
+
131
+ Needs `BETTER_AUTH_SECRET` (32+ characters; a product moving off next-auth may keep its `AUTH_SECRET`) and a database; without them every auth route answers 503 `accounts_unavailable` instead of crashing. Peer dependencies: `better-auth`, `@better-auth/passkey`, `drizzle-orm`, and `next` for `/auth/next`.
132
+
133
+ ## Verify
134
+
135
+ ```sh
136
+ pnpm run verify # format, lint, types, build, unit + render tests, real-browser checks
137
+ ```
138
+
139
+ The browser checks run the demo at 390px in light and dark: 44px targets, no sideways scroll with the menu open, Escape after a mouse open, arrow keys, outside click, a broken photo, an anonymous account, and a failing sign-out.
140
+
141
+ ## Sign in with OrangeCat — `@bitbaum/accountkit/orangecat`
142
+
143
+ Nine apps wrote this themselves and each learned something the others had not. This is all of it, once. Server-only, no next-auth import (the types are structural), so it serves Auth.js v5 and next-auth v4.
144
+
145
+ ```ts
146
+ import { orangecatClient, orangecatProvider, syncOcSession } from "@bitbaum/accountkit/orangecat";
147
+
148
+ const orangecat = orangecatClient(); // ORANGECAT_OAUTH_CLIENT_ID / _SECRET / _ISSUER, or null
149
+
150
+ export const { handlers, auth, signIn, signOut } = NextAuth({
151
+ session: { strategy: "jwt" },
152
+ pages: { error: "/auth/error" },
153
+ providers: orangecat ? [orangecatProvider(orangecat)] : [],
154
+ callbacks: {
155
+ jwt: ({ token, account, profile }) => syncOcSession({ token, account, profile }, orangecat),
156
+ session: ({ session, token }) => ({ ...session, actorId: token.actorId }),
157
+ },
158
+ });
159
+ ```
160
+
161
+ What is not configurable, and why:
162
+
163
+ - **`client_secret_post` and PKCE.** OrangeCat's token endpoint accepts only `client_secret_post`; the Auth.js default is answered with a 400 that reads "client_id is required". PKCE is required even with a secret.
164
+ - **`profile()` has no `email`.** OrangeCat auto-confirms addresses, so `email_verified` proves nothing. With an `email`, Auth.js looks a new account up by it and either links (takeover) or refuses. The address rides along as `contactEmail`, contact data only. The identity is `sub` (`token.actorId`), never the email.
165
+ - **A revoked grant ends the session.** `syncOcSession` refreshes once the access token expires; `invalid_grant` drops `actorId` (read that, never `token.sub`, as "signed in"); any other failure keeps the session and retries in 60 s. Safe inside `jwt()` since orangecat#1212 (confidential clients keep their refresh token).
166
+
167
+ Entry: `signIn("orangecat", { redirectTo: returnPath(from) }, authorizationParams({ mode, email, provider }))` → `prompt=create`, `login_hint`, `idp_hint` (google, github — the providers OrangeCat has switched on). `returnPath` accepts only same-origin paths.
168
+
169
+ Apps with their own user table:
170
+
171
+ - `withOrangecatIdentity(adapter, store, { keepTokens })` — an OrangeCat sign-in resolves by `sub` alone; a new sub always creates a new user, stored with its OrangeCat address when free and `orangecat-<sub>@users.invalid` when taken. Linking an existing account is Auth.js's own path (sign in, then sign in with OrangeCat). `keepTokens: true` also writes the accounts row (for an app that calls OrangeCat on the person's behalf).
172
+ - `resolveOrangecatUser(store, identity)` — the same, for a callback (next-auth v4, no adapter). Survives the insert race.
173
+ - `decideOrangecatSignIn(input, policy)` — the pure rule, with `whenEmailTaken` / `whenNoEmail: "refuse"` for an app that prefers to send the person to connect from their profile.
174
+ - `mintLinkToken` / `readLinkToken` — evig's "Connect OrangeCat": a 10-minute signed cookie set from inside the session, so the identity that comes back attaches to that account and no other.
175
+ - `emailEarnsPromotion(provider)` — never ADMIN_EMAILS promotion for an OrangeCat login.
176
+ - `mayReceivePasswordReset({ email, hasPassword, orangecatLinked })` — no reset link for an account OrangeCat created, or for a placeholder.
177
+
178
+ ### The error screen
179
+
180
+ ```tsx
181
+ import { SignInError } from "@bitbaum/accountkit";
182
+
183
+ // app/auth/error/page.tsx — Auth.js `pages.error`
184
+ <SignInError error={searchParams.error} retry={restartSignIn} retryFields={{ from }} home="/" />
185
+ ```
186
+
187
+ **Import `@bitbaum/accountkit/styles.css`** (once, in the root layout or globals) and map the `--acct-*` variables onto the app's tokens, or the screen renders as bare text — solon shipped exactly that once, and no lint or typecheck can see it. Look at it at 390px before merging.
188
+
189
+ **Try again restarts sign-in** (a server action calling `signIn("orangecat", …)`, or a GET route that does). It never goes home, and it never prints the provider's error code. No hooks: renders in a server component.
190
+
191
+ MIT.
192
+
193
+ ### Request-dependent sessions
194
+
195
+ `getSession()` is a server-component request API. It reads Next headers before
196
+ checking deployment readiness and lets framework rendering signals propagate.
197
+ A build without account secrets must not cache a signed-out page or redirect for
198
+ all future sessions. Calls outside a request are errors, not signed-out users.
199
+
200
+ ### Second-factor policy
201
+
202
+ An enabled second factor applies to password, email-link, passkey and OIDC sign-in. The server reuses BetterAuth's native signed challenge, trusted-device validation, attempt limits and expiry. A primary credential alone does not leave an authenticated session while the challenge is pending.
203
+
204
+ Shared forms complete both JSON `twoFactorRedirect` responses and app-local `?twoFactor=1` redirects. Custom clients must handle those states and supply their local sign-in screen as `errorCallbackURL` for email/OIDC callbacks. `SignUpForm` accepts an optional `signInPath` for nonstandard route names; standard `/sign-up` routes infer the sibling `/sign-in`. Challenge URLs contain no bearer credential.
@@ -0,0 +1,76 @@
1
+ import { type ReactNode } from "react";
2
+ import { type AccountUser } from "./identity.js";
3
+ /** A place to go. Rendered through `renderLink` so a Next app keeps <Link>. */
4
+ export type AccountLinkItem = {
5
+ id: string;
6
+ label: string;
7
+ href: string;
8
+ icon?: ReactNode;
9
+ };
10
+ /** Something to do. Closes the menu, then runs. */
11
+ export type AccountActionItem = {
12
+ id: string;
13
+ label: string;
14
+ onSelect: () => void | Promise<unknown>;
15
+ icon?: ReactNode;
16
+ };
17
+ export type AccountItem = AccountLinkItem | AccountActionItem;
18
+ /** Items are grouped; a divider separates groups. */
19
+ export type AccountGroup = {
20
+ id: string;
21
+ items: AccountItem[];
22
+ };
23
+ export type AccountMenuLabels = {
24
+ /** Accessible name of the trigger; "{name}" is replaced. */
25
+ trigger: string;
26
+ menu: string;
27
+ signOut: string;
28
+ signingOut: string;
29
+ signOutFailed: string;
30
+ };
31
+ export type RenderLinkProps = {
32
+ href: string;
33
+ className: string;
34
+ role: "menuitem";
35
+ tabIndex: -1;
36
+ onClick: () => void;
37
+ children: ReactNode;
38
+ };
39
+ export type AccountMenuProps = {
40
+ user: AccountUser;
41
+ /**
42
+ * Called when the person signs out. May return a promise; while it is
43
+ * pending the row says so, and if it rejects the menu stays open and says
44
+ * the sign-out did not complete — a dead sign-out is the one failure this
45
+ * menu must never hide.
46
+ */
47
+ onSignOut: () => void | Promise<unknown>;
48
+ groups?: AccountGroup[];
49
+ /**
50
+ * Rows that are not menu items — a theme switch, a status line. Rendered
51
+ * between the groups and Sign out, not navigable by arrow keys.
52
+ */
53
+ extra?: ReactNode;
54
+ /** Defaults to a plain <a>. Pass `(p) => <Link {...p} />` in a Next app. */
55
+ renderLink?: (props: RenderLinkProps) => ReactNode;
56
+ labels?: Partial<AccountMenuLabels>;
57
+ /** Which edge of the trigger the panel aligns to. */
58
+ align?: "end" | "start";
59
+ className?: string;
60
+ };
61
+ /**
62
+ * The account menu: who is signed in, where to go, how to sign out.
63
+ *
64
+ * Identity, not navigation: the app's sidebar is for places in the product;
65
+ * this is for the person. Nothing should appear in both.
66
+ *
67
+ * Behaviour that is shared and NOT configurable, because each item shipped
68
+ * broken in at least one app:
69
+ * - a 44px trigger (Loki's was 36px, under the touch-target floor);
70
+ * - a photo that fails to load falls back to initials, then a glyph;
71
+ * - Escape closes and returns focus to the trigger; arrows, Home and End
72
+ * move between items; a click outside closes;
73
+ * - sign-out shows that it is happening, and says so when it fails.
74
+ */
75
+ export declare function AccountMenu({ user, onSignOut, groups, extra, renderLink, labels: labelOverrides, align, className, }: AccountMenuProps): import("react").JSX.Element;
76
+ //# sourceMappingURL=AccountMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountMenu.d.ts","sourceRoot":"","sources":["../src/AccountMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAEvB,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB,CAAC;AAEF,mDAAmD;AACnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,iBAAiB,CAAC;AAE9D,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE,CAAC;AAEhE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAUF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,CAAC,CAAC,CAAC;IACb,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACnD,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACpC,qDAAqD;IACrD,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA+BF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,SAAS,EACT,MAAW,EACX,KAAK,EACL,UAAwB,EACxB,MAAM,EAAE,cAAc,EACtB,KAAa,EACb,SAAS,GACV,EAAE,gBAAgB,+BA8OlB"}
@@ -0,0 +1,156 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Fragment, useCallback, useEffect, useId, useRef, useState, } from "react";
4
+ import { displayNameFor, initialsFor, nextMenuIndex, secondaryLineFor, } from "./identity.js";
5
+ const DEFAULT_LABELS = {
6
+ trigger: "Account: {name}",
7
+ menu: "Account",
8
+ signOut: "Sign out",
9
+ signingOut: "Signing out…",
10
+ signOutFailed: "Sign-out did not complete. Try again.",
11
+ };
12
+ function defaultLink(props) {
13
+ return _jsx("a", { ...props });
14
+ }
15
+ function isLink(item) {
16
+ return "href" in item;
17
+ }
18
+ /** A person outline — drawn when there is neither a photo nor a name. */
19
+ function PersonGlyph() {
20
+ return (_jsxs("svg", { className: "acct-glyph", viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "8", r: "4" }), _jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })] }));
21
+ }
22
+ /**
23
+ * The account menu: who is signed in, where to go, how to sign out.
24
+ *
25
+ * Identity, not navigation: the app's sidebar is for places in the product;
26
+ * this is for the person. Nothing should appear in both.
27
+ *
28
+ * Behaviour that is shared and NOT configurable, because each item shipped
29
+ * broken in at least one app:
30
+ * - a 44px trigger (Loki's was 36px, under the touch-target floor);
31
+ * - a photo that fails to load falls back to initials, then a glyph;
32
+ * - Escape closes and returns focus to the trigger; arrows, Home and End
33
+ * move between items; a click outside closes;
34
+ * - sign-out shows that it is happening, and says so when it fails.
35
+ */
36
+ export function AccountMenu({ user, onSignOut, groups = [], extra, renderLink = defaultLink, labels: labelOverrides, align = "end", className, }) {
37
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
38
+ const [open, setOpen] = useState(false);
39
+ const [imageFailed, setImageFailed] = useState(false);
40
+ const [signingOut, setSigningOut] = useState(false);
41
+ const [signOutError, setSignOutError] = useState(false);
42
+ const rootRef = useRef(null);
43
+ const triggerRef = useRef(null);
44
+ const panelRef = useRef(null);
45
+ const panelId = useId();
46
+ const name = displayNameFor(user);
47
+ const initials = initialsFor(user);
48
+ const secondary = secondaryLineFor(user);
49
+ const showImage = Boolean(user.image) && !imageFailed;
50
+ // A new image URL deserves a fresh attempt.
51
+ useEffect(() => setImageFailed(false), [user.image]);
52
+ const items = useCallback(() => Array.from(panelRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? []), []);
53
+ const close = useCallback((restoreFocus) => {
54
+ setOpen(false);
55
+ if (restoreFocus)
56
+ triggerRef.current?.focus();
57
+ }, []);
58
+ // A click anywhere outside closes — a listener, not a full-page scrim, so
59
+ // the click still reaches whatever it was aimed at.
60
+ //
61
+ // Escape is listened for on the DOCUMENT, not the panel. Opened with the
62
+ // mouse, focus stays on the trigger, so a panel-only handler never sees the
63
+ // key: the browser check caught Escape doing nothing after a click.
64
+ useEffect(() => {
65
+ if (!open)
66
+ return;
67
+ const onPointerDown = (event) => {
68
+ if (!rootRef.current?.contains(event.target))
69
+ close(false);
70
+ };
71
+ const onKeyDown = (event) => {
72
+ if (event.key === "Escape") {
73
+ event.preventDefault();
74
+ close(true);
75
+ }
76
+ };
77
+ document.addEventListener("pointerdown", onPointerDown);
78
+ document.addEventListener("keydown", onKeyDown);
79
+ return () => {
80
+ document.removeEventListener("pointerdown", onPointerDown);
81
+ document.removeEventListener("keydown", onKeyDown);
82
+ };
83
+ }, [open, close]);
84
+ function focusAt(index) {
85
+ const list = items();
86
+ list[index]?.focus();
87
+ }
88
+ function openWithFocus(index) {
89
+ setOpen(true);
90
+ setSignOutError(false);
91
+ // After render: the panel does not exist yet.
92
+ requestAnimationFrame(() => {
93
+ const list = items();
94
+ focusAt(index === "first" ? 0 : list.length - 1);
95
+ });
96
+ }
97
+ function onTriggerKeyDown(event) {
98
+ if (event.key === "ArrowDown") {
99
+ event.preventDefault();
100
+ openWithFocus("first");
101
+ }
102
+ else if (event.key === "ArrowUp") {
103
+ event.preventDefault();
104
+ openWithFocus("last");
105
+ }
106
+ }
107
+ function onPanelKeyDown(event) {
108
+ // Escape is handled by the document listener above, wherever focus is.
109
+ if (event.key === "Tab") {
110
+ close(false);
111
+ return;
112
+ }
113
+ if (event.key === "ArrowDown" ||
114
+ event.key === "ArrowUp" ||
115
+ event.key === "Home" ||
116
+ event.key === "End") {
117
+ event.preventDefault();
118
+ const list = items();
119
+ const current = list.indexOf(document.activeElement);
120
+ focusAt(nextMenuIndex(current, event.key, list.length));
121
+ }
122
+ }
123
+ async function signOut() {
124
+ setSigningOut(true);
125
+ setSignOutError(false);
126
+ try {
127
+ await onSignOut();
128
+ // On success the app usually navigates away; closing covers the case
129
+ // where it does not.
130
+ setOpen(false);
131
+ }
132
+ catch {
133
+ setSignOutError(true);
134
+ }
135
+ finally {
136
+ setSigningOut(false);
137
+ }
138
+ }
139
+ return (_jsxs("div", { ref: rootRef, className: ["acct", className].filter(Boolean).join(" "), "data-align": align, children: [_jsx("button", { ref: triggerRef, type: "button", className: "acct-trigger", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": open ? panelId : undefined, "aria-label": labels.trigger.replace("{name}", name), title: name, onClick: () => {
140
+ setOpen((v) => !v);
141
+ setSignOutError(false);
142
+ }, onKeyDown: onTriggerKeyDown, children: showImage ? (_jsx("img", { className: "acct-avatar", src: user.image ?? undefined, alt: "", onError: () => setImageFailed(true) })) : initials ? (_jsx("span", { className: "acct-initials", "aria-hidden": "true", children: initials })) : (_jsx(PersonGlyph, {})) }), open && (_jsxs("div", { ref: panelRef, id: panelId, className: "acct-panel", role: "menu", "aria-label": labels.menu, onKeyDown: onPanelKeyDown, children: [_jsxs("div", { className: "acct-identity", children: [_jsx("p", { className: "acct-name", children: name }), secondary && _jsx("p", { className: "acct-secondary", children: secondary })] }), groups
143
+ .filter((g) => g.items.length > 0)
144
+ .map((group) => (_jsx("div", { className: "acct-group", role: "group", children: group.items.map((item) => isLink(item) ? (_jsx(Fragment, { children: renderLink({
145
+ href: item.href,
146
+ className: "acct-row",
147
+ role: "menuitem",
148
+ tabIndex: -1,
149
+ onClick: () => close(false),
150
+ children: (_jsxs(_Fragment, { children: [item.icon && (_jsx("span", { className: "acct-icon", children: item.icon })), _jsx("span", { children: item.label })] })),
151
+ }) }, item.id)) : (_jsxs("button", { type: "button", className: "acct-row", role: "menuitem", tabIndex: -1, onClick: () => {
152
+ close(false);
153
+ void item.onSelect();
154
+ }, children: [item.icon && (_jsx("span", { className: "acct-icon", children: item.icon })), _jsx("span", { children: item.label })] }, item.id))) }, group.id))), extra && _jsx("div", { className: "acct-group acct-extra", children: extra }), _jsxs("div", { className: "acct-group", children: [_jsx("button", { type: "button", className: "acct-row acct-signout", role: "menuitem", tabIndex: -1, disabled: signingOut, "aria-busy": signingOut, onClick: () => void signOut(), children: _jsx("span", { children: signingOut ? labels.signingOut : labels.signOut }) }), signOutError && (_jsx("p", { className: "acct-error", role: "alert", children: labels.signOutFailed }))] })] }))] }));
155
+ }
156
+ //# sourceMappingURL=AccountMenu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountMenu.js","sourceRoot":"","sources":["../src/AccountMenu.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Where Auth.js sends a sign-in that did not finish (`pages.error`).
4
+ *
5
+ * Every app had a different one: loki, skif and substrata showed Auth.js's
6
+ * bare "Error" page; heidi's "Try again" went to the home page, which is not
7
+ * trying again. Solon's was right, and this is it: "Try again" STARTS the
8
+ * sign-in again, directly, without another screen in between.
9
+ *
10
+ * It never prints the provider's error code. The codes mean nothing to a
11
+ * visitor and are a gift to anyone probing the endpoint; the operator's copy
12
+ * belongs in the server log.
13
+ *
14
+ * No hooks, no "use client": it renders in a server component, and `retry`
15
+ * may be a server action.
16
+ */
17
+ export type SignInErrorKind = "failed" | "denied" | "configuration";
18
+ /** Auth.js's `?error=` code → what the person is told. Anything unknown is "failed". */
19
+ export declare function signInErrorKind(error: string | null | undefined): SignInErrorKind;
20
+ export type SignInErrorLabels = {
21
+ kicker: string;
22
+ failedTitle: string;
23
+ failedBody: string;
24
+ deniedTitle: string;
25
+ deniedBody: string;
26
+ configurationTitle: string;
27
+ configurationBody: string;
28
+ tryAgain: string;
29
+ home: string;
30
+ };
31
+ export type SignInErrorProps = {
32
+ /** Auth.js's `?error=` search param, as received. Never shown. */
33
+ error?: string | null;
34
+ /**
35
+ * How "Try again" restarts sign-in: a server action that calls
36
+ * `signIn("orangecat", …)`, or the URL of a GET route that does.
37
+ * Never the home page.
38
+ */
39
+ retry: string | ((formData: FormData) => void | Promise<void>);
40
+ /** Hidden fields posted with a server-action retry (`from`, `locale`). */
41
+ retryFields?: Record<string, string>;
42
+ /** Where "Back to the start" goes. */
43
+ home?: string;
44
+ /** More ways on, shown first — e.g. "Add an email on OrangeCat" for a denial. */
45
+ children?: ReactNode;
46
+ labels?: Partial<SignInErrorLabels>;
47
+ className?: string;
48
+ };
49
+ export declare function SignInError({ error, retry, retryFields, home, children, labels: overrides, className, }: SignInErrorProps): import("react").JSX.Element;
50
+ //# sourceMappingURL=SignInError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SignInError.d.ts","sourceRoot":"","sources":["../src/SignInError.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,CAAC;AAEpE,wFAAwF;AACxF,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,eAAe,CAIjB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAiBF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,WAAW,EACX,IAAU,EACV,QAAQ,EACR,MAAM,EAAE,SAAS,EACjB,SAAS,GACV,EAAE,gBAAgB,+BA6ClB"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Auth.js's `?error=` code → what the person is told. Anything unknown is "failed". */
3
+ export function signInErrorKind(error) {
4
+ if (error === "AccessDenied")
5
+ return "denied";
6
+ if (error === "Configuration")
7
+ return "configuration";
8
+ return "failed";
9
+ }
10
+ const DEFAULT_LABELS = {
11
+ kicker: "Sign in",
12
+ failedTitle: "Sign-in did not finish",
13
+ failedBody: "Nothing was changed. Start again — it usually works the second time. If it keeps failing, OrangeCat may be briefly unavailable.",
14
+ deniedTitle: "This account cannot sign in here",
15
+ deniedBody: "OrangeCat signed you in, but this app could not accept the account.",
16
+ configurationTitle: "Sign-in is not available right now",
17
+ configurationBody: "Sign-in is not set up on this server yet. Nothing you did caused this.",
18
+ tryAgain: "Try again",
19
+ home: "Back to the start",
20
+ };
21
+ export function SignInError({ error, retry, retryFields, home = "/", children, labels: overrides, className, }) {
22
+ const labels = { ...DEFAULT_LABELS, ...overrides };
23
+ const kind = signInErrorKind(error);
24
+ const title = labels[`${kind}Title`];
25
+ const body = labels[`${kind}Body`];
26
+ // Retrying a server with no client configured fails the same way again.
27
+ const canRetry = kind !== "configuration";
28
+ // A denial is answered elsewhere first (the app's children), so the retry
29
+ // steps back to a secondary button there.
30
+ const retryClass = `acct-signin-button ${kind === "denied" && children ? "is-secondary" : ""}`;
31
+ return (_jsxs("section", { className: `acct-signin-error ${className ?? ""}`.trim(), "aria-labelledby": "acct-signin-error-title", "data-kind": kind, children: [_jsx("p", { className: "acct-signin-kicker", children: labels.kicker }), _jsx("h1", { id: "acct-signin-error-title", className: "acct-signin-title", children: title }), _jsx("p", { className: "acct-signin-body", children: body }), _jsxs("div", { className: "acct-signin-actions", children: [children, canRetry &&
32
+ (typeof retry === "string" ? (_jsx("a", { className: retryClass, href: retry, children: labels.tryAgain })) : (_jsxs("form", { action: retry, children: [Object.entries(retryFields ?? {}).map(([name, value]) => (_jsx("input", { type: "hidden", name: name, value: value }, name))), _jsx("button", { type: "submit", className: retryClass, children: labels.tryAgain })] }))), _jsx("a", { className: "acct-signin-home", href: home, children: labels.home })] })] }));
33
+ }
34
+ //# sourceMappingURL=SignInError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SignInError.js","sourceRoot":"","sources":["../src/SignInError.tsx"],"names":[],"mappings":";AAoBA,wFAAwF;AACxF,MAAM,UAAU,eAAe,CAC7B,KAAgC;IAEhC,IAAI,KAAK,KAAK,cAAc;QAAE,OAAO,QAAQ,CAAC;IAC9C,IAAI,KAAK,KAAK,eAAe;QAAE,OAAO,eAAe,CAAC;IACtD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAcD,MAAM,cAAc,GAAsB;IACxC,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,wBAAwB;IACrC,UAAU,EACR,iIAAiI;IACnI,WAAW,EAAE,kCAAkC;IAC/C,UAAU,EACR,qEAAqE;IACvE,kBAAkB,EAAE,oCAAoC;IACxD,iBAAiB,EACf,wEAAwE;IAC1E,QAAQ,EAAE,WAAW;IACrB,IAAI,EAAE,mBAAmB;CAC1B,CAAC;AAqBF,MAAM,UAAU,WAAW,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,WAAW,EACX,IAAI,GAAG,GAAG,EACV,QAAQ,EACR,MAAM,EAAE,SAAS,EACjB,SAAS,GACQ;IACjB,MAAM,MAAM,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,SAAS,EAAE,CAAC;IACnD,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC;IACnC,wEAAwE;IACxE,MAAM,QAAQ,GAAG,IAAI,KAAK,eAAe,CAAC;IAC1C,0EAA0E;IAC1E,0CAA0C;IAC1C,MAAM,UAAU,GAAG,sBAAsB,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAE/F,OAAO,CACL,mBACE,SAAS,EAAE,qBAAqB,SAAS,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,qBACxC,yBAAyB,eAC9B,IAAI,aAEf,YAAG,SAAS,EAAC,oBAAoB,YAAE,MAAM,CAAC,MAAM,GAAK,EACrD,aAAI,EAAE,EAAC,yBAAyB,EAAC,SAAS,EAAC,mBAAmB,YAC3D,KAAK,GACH,EACL,YAAG,SAAS,EAAC,kBAAkB,YAAE,IAAI,GAAK,EAC1C,eAAK,SAAS,EAAC,qBAAqB,aACjC,QAAQ,EACR,QAAQ;wBACP,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAC3B,YAAG,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,YAClC,MAAM,CAAC,QAAQ,GACd,CACL,CAAC,CAAC,CAAC,CACF,gBAAM,MAAM,EAAE,KAAK,aAChB,MAAM,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CACxD,gBAAkB,IAAI,EAAC,QAAQ,EAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,IAA5C,IAAI,CAA4C,CAC7D,CAAC,EACF,iBAAQ,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAE,UAAU,YACxC,MAAM,CAAC,QAAQ,GACT,IACJ,CACR,CAAC,EACJ,YAAG,SAAS,EAAC,kBAAkB,EAAC,IAAI,EAAE,IAAI,YACvC,MAAM,CAAC,IAAI,GACV,IACA,IACE,CACX,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { passkeyClient } from "@better-auth/passkey/client";
2
+ import { magicLinkClient, twoFactorClient } from "better-auth/client/plugins";
3
+ /**
4
+ * The plugins the browser client needs, the same in every product:
5
+ *
6
+ * import { createAuthClient } from "better-auth/react";
7
+ * import { authClientPlugins } from "@bitbaum/accountkit/auth/client";
8
+ * export const authClient = createAuthClient({ plugins: authClientPlugins() });
9
+ *
10
+ * The client itself is created by the product, because its fully inferred type
11
+ * cannot be written into a published declaration file. It talks only to this
12
+ * origin (/api/auth), which is all a strict CSP's `connect-src 'self'` allows.
13
+ */
14
+ export declare function authClientPlugins(): [
15
+ ReturnType<typeof twoFactorClient>,
16
+ ReturnType<typeof passkeyClient>,
17
+ ReturnType<typeof magicLinkClient>
18
+ ];
19
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/auth/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,IAAI;IACnC,UAAU,CAAC,OAAO,eAAe,CAAC;IAClC,UAAU,CAAC,OAAO,aAAa,CAAC;IAChC,UAAU,CAAC,OAAO,eAAe,CAAC;CACnC,CAEA"}
@@ -0,0 +1,17 @@
1
+ import { passkeyClient } from "@better-auth/passkey/client";
2
+ import { magicLinkClient, twoFactorClient } from "better-auth/client/plugins";
3
+ /**
4
+ * The plugins the browser client needs, the same in every product:
5
+ *
6
+ * import { createAuthClient } from "better-auth/react";
7
+ * import { authClientPlugins } from "@bitbaum/accountkit/auth/client";
8
+ * export const authClient = createAuthClient({ plugins: authClientPlugins() });
9
+ *
10
+ * The client itself is created by the product, because its fully inferred type
11
+ * cannot be written into a published declaration file. It talks only to this
12
+ * origin (/api/auth), which is all a strict CSP's `connect-src 'self'` allows.
13
+ */
14
+ export function authClientPlugins() {
15
+ return [twoFactorClient(), passkeyClient(), magicLinkClient()];
16
+ }
17
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/auth/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB;IAK/B,OAAO,CAAC,eAAe,EAAE,EAAE,aAAa,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { twoFactor } from "better-auth/plugins";
2
+ /** Resolve only app-local challenge UI and return URLs; never carry bearer values. */
3
+ export declare function factorDestination(origin: string, screen: unknown, next: unknown): string;
4
+ /** Reuse BetterAuth's challenge/trusted-device policy for every primary method. */
5
+ export declare function factorForAllMethods(site: {
6
+ title: string;
7
+ url: string;
8
+ }): ReturnType<typeof twoFactor>;
9
+ //# sourceMappingURL=factor.d.ts.map