ideal-auth 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,17 +12,19 @@ Provide a cookie bridge (3 functions) once during setup, and `auth().login(user)
12
12
  bun add ideal-auth
13
13
  ```
14
14
 
15
- ## Generate Secret
15
+ ## Generate Secrets
16
16
 
17
17
  ```bash
18
+ # Session secret (required — used by createAuth)
18
19
  bunx ideal-auth secret
19
- ```
20
+ # IDEAL_AUTH_SECRET=aLThikMgJKMBB5WZLE-lCaOQUdgPWU8BHRv99bkYaVY
20
21
 
21
- ```
22
- IDEAL_AUTH_SECRET=aLThikMgJKMBB5WZLE-lCaOQUdgPWU8BHRv99bkYaVY
22
+ # Encryption key (optional — used by encrypt/decrypt for data at rest)
23
+ bunx ideal-auth encryption-key
24
+ # ENCRYPTION_KEY=9546dd9fa461ce15f0aacd6e1b461b52
23
25
  ```
24
26
 
25
- Copy the output into your `.env` file. The secret must be at least 32 characters.
27
+ Copy the output into your `.env` file. `IDEAL_AUTH_SECRET` must be at least 32 characters. `ENCRYPTION_KEY` is only needed if you use `encrypt()`/`decrypt()` (e.g., encrypting TOTP secrets or access tokens at rest).
26
28
 
27
29
  ## Quick Start
28
30
 
@@ -94,12 +96,13 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
94
96
  | --- | --- | --- | --- |
95
97
  | `secret` | `string` | Yes | — |
96
98
  | `cookie` | `CookieBridge` | Yes | — |
97
- | `resolveUser` | `(id: string) => Promise<User \| null>` | Yes | — |
99
+ | `resolveUser` | `(id: string) => Promise<User \| null \| undefined>` | Yes (unless `sessionFields` is provided) | — |
100
+ | `sessionFields` | `(keyof User & string)[]` | Yes (unless `resolveUser` is provided) | — |
98
101
  | `hash` | `HashInstance` | No | — |
99
- | `resolveUserByCredentials` | `(creds: Record<string, any>) => Promise<User \| null>` | No | — |
102
+ | `resolveUserByCredentials` | `(creds: Record<string, any>) => Promise<User \| null \| undefined>` | No | — |
100
103
  | `credentialKey` | `string` | No | `'password'` |
101
104
  | `passwordField` | `string` | No | `'password'` |
102
- | `attemptUser` | `(creds: Record<string, any>) => Promise<User \| null>` | No | — |
105
+ | `attemptUser` | `(creds: Record<string, any>) => Promise<User \| null \| undefined>` | No | — |
103
106
  | `session.cookieName` | `string` | No | `'ideal_session'` |
104
107
  | `session.maxAge` | `number` (seconds) | No | `604800` (7 days) |
105
108
  | `session.rememberMaxAge` | `number` (seconds) | No | `2592000` (30 days) |
@@ -6,13 +6,13 @@ interface AuthInstanceDeps<TUser extends AnyUser> {
6
6
  maxAge: number;
7
7
  rememberMaxAge: number;
8
8
  cookieOptions: ConfigurableCookieOptions;
9
- resolveUser?: (id: string) => Promise<TUser | null>;
9
+ resolveUser?: (id: string) => Promise<TUser | null | undefined>;
10
10
  sessionFields?: (keyof TUser & string)[];
11
11
  hash?: HashInstance;
12
- resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
12
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
13
13
  credentialKey: string;
14
14
  passwordField: string;
15
- attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null>;
15
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
16
16
  }
17
17
  export declare function createAuthInstance<TUser extends AnyUser>(deps: AuthInstanceDeps<TUser>): AuthInstance<TUser>;
18
18
  export {};
@@ -106,7 +106,7 @@ export function createAuthInstance(deps) {
106
106
  }
107
107
  // Database-backed: resolve user via callback
108
108
  if (deps.resolveUser) {
109
- cachedUser = await deps.resolveUser(session.uid);
109
+ cachedUser = (await deps.resolveUser(session.uid)) ?? null;
110
110
  return cachedUser;
111
111
  }
112
112
  cachedUser = null;
package/dist/types.d.ts CHANGED
@@ -27,7 +27,7 @@ export interface SessionPayload {
27
27
  export interface LoginOptions {
28
28
  remember?: boolean;
29
29
  }
30
- export interface AuthConfig<TUser extends AnyUser = AnyUser> {
30
+ interface AuthConfigBase<TUser extends AnyUser> {
31
31
  secret: string;
32
32
  cookie: CookieBridge;
33
33
  session?: {
@@ -36,33 +36,39 @@ export interface AuthConfig<TUser extends AnyUser = AnyUser> {
36
36
  rememberMaxAge?: number;
37
37
  cookie?: Partial<ConfigurableCookieOptions>;
38
38
  };
39
- resolveUser?: (id: string) => Promise<TUser | null>;
40
- /**
41
- * Fields from the user object to store in the session cookie.
42
- * When provided, `resolveUser` is not needed — `user()` returns
43
- * the stored fields directly from the cookie.
44
- *
45
- * The `id` field is always stored. List only additional fields.
46
- * Keep the total small — session cookies have a ~4KB size limit.
47
- *
48
- * Cannot be used together with `resolveUser`.
49
- *
50
- * **Staleness:** Data is snapshotted at login time. If a user's role
51
- * or permissions change server-side, the cookie retains the old values
52
- * until the user re-logs in. For authorization-critical fields (role,
53
- * permissions, subscription tier), prefer `resolveUser` to get fresh
54
- * data on every request.
55
- *
56
- * **ID type:** `user()` always returns `id` as a `string` on subsequent
57
- * requests (read from cookie), even if the original `TUser.id` was a number.
58
- */
59
- sessionFields?: (keyof TUser & string)[];
60
39
  hash?: HashInstance;
61
- resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
40
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
62
41
  credentialKey?: string;
63
42
  passwordField?: string;
64
- attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null>;
65
- }
43
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
44
+ }
45
+ /** Database-backed: `user()` calls `resolveUser(id)` on every request. */
46
+ interface AuthConfigWithResolveUser<TUser extends AnyUser> extends AuthConfigBase<TUser> {
47
+ resolveUser: (id: string) => Promise<TUser | null | undefined>;
48
+ /** Cannot use `sessionFields` together with `resolveUser`. */
49
+ sessionFields?: never;
50
+ }
51
+ /**
52
+ * Cookie-backed: `user()` reads declared fields from the session cookie.
53
+ *
54
+ * The `id` field is always stored. List only additional fields.
55
+ * Keep the total small — session cookies have a ~4KB size limit.
56
+ *
57
+ * **Staleness:** Data is snapshotted at login time. If a user's role
58
+ * or permissions change server-side, the cookie retains the old values
59
+ * until the user re-logs in. For authorization-critical fields (role,
60
+ * permissions, subscription tier), prefer `resolveUser` to get fresh
61
+ * data on every request.
62
+ *
63
+ * **ID type:** `user()` always returns `id` as a `string` on subsequent
64
+ * requests (read from cookie), even if the original `TUser.id` was a number.
65
+ */
66
+ interface AuthConfigWithSessionFields<TUser extends AnyUser> extends AuthConfigBase<TUser> {
67
+ /** Cannot use `resolveUser` together with `sessionFields`. */
68
+ resolveUser?: never;
69
+ sessionFields: (keyof TUser & string)[];
70
+ }
71
+ export type AuthConfig<TUser extends AnyUser = AnyUser> = AuthConfigWithResolveUser<TUser> | AuthConfigWithSessionFields<TUser>;
66
72
  export interface HashConfig {
67
73
  rounds?: number;
68
74
  }
@@ -125,3 +131,4 @@ export interface RecoveryCodeResult {
125
131
  valid: boolean;
126
132
  remaining: string[];
127
133
  }
134
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ideal-auth
3
- description: Auth primitives for the JS ecosystem. Covers login, logout, session, register, signup, sign in, sign out, password, 2FA, two-factor, TOTP, MFA, ideal-auth, cookie bridge, middleware, route protection, rate limit, rate limiting, password reset, forgot password, email verification, remember me, recovery code, CSRF, encrypt, decrypt, hash, bcrypt, token, secret, session cookie, multi-tenant, cross-domain, tenant, transfer token, federated logout, OAuth redirect, central login, login session, attemptUser, sessionFields, cookie-backed session, resolveUser, token refresh, refresh token, access token, OIDC.
3
+ description: Auth primitives for the JS ecosystem. Covers login, logout, session, register, signup, sign in, sign out, password, 2FA, two-factor, TOTP, MFA, ideal-auth, cookie bridge, middleware, route protection, rate limit, rate limiting, password reset, forgot password, email verification, remember me, recovery code, CSRF, encrypt, decrypt, hash, bcrypt, token, secret, session cookie, multi-tenant, cross-domain, tenant, transfer token, federated logout, OAuth redirect, central login, login session, attemptUser, sessionFields, cookie-backed session, resolveUser, token refresh, refresh token, access token, OIDC, passkey, passkeys, WebAuthn, passwordless, biometric, fingerprint.
4
4
  ---
5
5
 
6
6
  You are an expert on `ideal-auth`, the auth primitives library for the JS ecosystem. You have complete knowledge of its API, patterns, security model, and framework integrations. Use this knowledge to help users implement authentication correctly.
@@ -1622,6 +1622,53 @@ await auth().login(user, { remember: true });
1622
1622
 
1623
1623
  ---
1624
1624
 
1625
+ ## Passkeys (WebAuthn)
1626
+
1627
+ Passkeys use public-key cryptography for passwordless authentication. ideal-auth handles the session after verification — the WebAuthn protocol is handled by `@simplewebauthn/server` and `@simplewebauthn/browser`.
1628
+
1629
+ No `hash` or `bcryptjs` needed — there are no passwords.
1630
+
1631
+ ### Flow
1632
+
1633
+ ```
1634
+ Registration: browser creates key pair → store public key in DB
1635
+ Authentication: server sends challenge → browser signs it → server verifies → auth().login(user)
1636
+ ```
1637
+
1638
+ ### After WebAuthn verification, create session with ideal-auth
1639
+
1640
+ ```ts
1641
+ import { verifyAuthenticationResponse } from '@simplewebauthn/server';
1642
+ import { auth } from './auth';
1643
+
1644
+ const verification = await verifyAuthenticationResponse({
1645
+ response: body,
1646
+ expectedChallenge,
1647
+ expectedOrigin: origin,
1648
+ expectedRPID: rpID,
1649
+ credential: { id: passkey.id, publicKey: Buffer.from(passkey.publicKey, 'base64url'), counter: passkey.counter },
1650
+ });
1651
+
1652
+ if (verification.verified) {
1653
+ // Update signature counter (replay protection)
1654
+ await db.passkey.update({ where: { id: passkey.id }, data: { counter: Number(verification.authenticationInfo.newCounter) } });
1655
+
1656
+ const user = await db.user.findUnique({ where: { id: passkey.userId } });
1657
+ await auth().login(user); // session created — done
1658
+ }
1659
+ ```
1660
+
1661
+ ### Key points
1662
+ - Install: `bun add @simplewebauthn/server @simplewebauthn/browser`
1663
+ - Store challenges in httpOnly cookies (5-min expiry), not client-side
1664
+ - Always update the signature counter after authentication
1665
+ - Set `rpID` to root domain (e.g., `example.com`) — works across subdomains
1666
+ - `userVerification: 'preferred'` for most apps, `'required'` for high-security
1667
+ - Passkeys with user verification can replace 2FA (biometric/PIN is the second factor)
1668
+ - Rate limit the options and verify endpoints
1669
+
1670
+ ---
1671
+
1625
1672
  ## Open Redirect Prevention
1626
1673
 
1627
1674
  Always validate redirect URLs after login. Never redirect to user-supplied absolute URLs.