ideal-auth 0.5.1 → 0.6.1

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
@@ -99,7 +99,7 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
99
99
  | `resolveUser` | `(id: string) => Promise<User \| null \| undefined>` | Yes (unless `sessionFields` is provided) | — |
100
100
  | `sessionFields` | `(keyof User & string)[]` | Yes (unless `resolveUser` is provided) | — |
101
101
  | `hash` | `HashInstance` | No | — |
102
- | `resolveUserByCredentials` | `(creds: Record<string, any>) => Promise<User \| null \| undefined>` | No | — |
102
+ | `resolveUserByCredentials` | `(creds: Record<string, any>) => Promise<AnyUser \| null \| undefined>` | No | — |
103
103
  | `credentialKey` | `string` | No | `'password'` |
104
104
  | `passwordField` | `string` | No | `'password'` |
105
105
  | `attemptUser` | `(creds: Record<string, any>) => Promise<User \| null \| undefined>` | No | — |
@@ -7,9 +7,9 @@ interface AuthInstanceDeps<TUser extends AnyUser> {
7
7
  rememberMaxAge: number;
8
8
  cookieOptions: ConfigurableCookieOptions;
9
9
  resolveUser?: (id: string) => Promise<TUser | null | undefined>;
10
- sessionFields?: (keyof TUser & string)[];
10
+ sessionFields?: readonly (keyof TUser & string)[];
11
11
  hash?: HashInstance;
12
- resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
12
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
13
13
  credentialKey: string;
14
14
  passwordField: string;
15
15
  attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
@@ -40,10 +40,15 @@ export function createAuthInstance(deps) {
40
40
  : buildCookieOptions(maxAge, deps.cookieOptions);
41
41
  await deps.cookie.set(deps.cookieName, sealed, opts);
42
42
  cachedPayload = payload;
43
- // When using sessionFields, only cache the picked fields (matching what's in the cookie)
44
- cachedUser = payload.data
45
- ? { id: user.id, ...payload.data }
46
- : user;
43
+ if (payload.data) {
44
+ // sessionFields mode: cache only the picked fields
45
+ cachedUser = { id: user.id, ...payload.data };
46
+ }
47
+ else {
48
+ // resolveUser mode: strip the password field from cache
49
+ const { [deps.passwordField]: _, ...safeUser } = user;
50
+ cachedUser = safeUser;
51
+ }
47
52
  }
48
53
  return {
49
54
  async login(user, options) {
@@ -70,14 +75,14 @@ export function createAuthInstance(deps) {
70
75
  // Laravel-style: strip password, resolve user, verify hash
71
76
  if (deps.hash && deps.resolveUserByCredentials) {
72
77
  const { [deps.credentialKey]: password, ...lookup } = credentials;
73
- const user = await deps.resolveUserByCredentials(lookup);
74
- if (!user)
78
+ const dbUser = await deps.resolveUserByCredentials(lookup);
79
+ if (!dbUser)
75
80
  return false;
76
- const storedHash = user[deps.passwordField];
81
+ const storedHash = dbUser[deps.passwordField];
77
82
  if (!storedHash || !(await deps.hash.verify(password, storedHash))) {
78
83
  return false;
79
84
  }
80
- await writeSession(user, options);
85
+ await writeSession(dbUser, options);
81
86
  return true;
82
87
  }
83
88
  throw new Error('Provide either attemptUser() or both hash + resolveUserByCredentials in config to use attempt()');
package/dist/auth.d.ts CHANGED
@@ -1,2 +1,13 @@
1
1
  import type { AnyUser, AuthInstance, AuthConfig } from './types';
2
- export declare function createAuth<TUser extends AnyUser = AnyUser>(config: AuthConfig<TUser>): () => AuthInstance<TUser>;
2
+ /**
3
+ * Create an auth factory.
4
+ *
5
+ * @example
6
+ * // resolveUser — user() returns SafeUser
7
+ * createAuth<SafeUser>({ resolveUser: ... })
8
+ *
9
+ * // sessionFields — user() returns Pick<DbUser, 'id' | 'email' | 'name'>
10
+ * const sessionFields = ['email', 'name'] as const;
11
+ * createAuth<DbUser, (typeof sessionFields)[number]>({ sessionFields, ... })
12
+ */
13
+ export declare function createAuth<TUser extends AnyUser, K extends keyof TUser & string = keyof TUser & string>(config: AuthConfig<TUser>): () => AuthInstance<TUser, Pick<TUser, 'id' | K>>;
package/dist/auth.js CHANGED
@@ -4,6 +4,17 @@ const SESSION_DEFAULTS = {
4
4
  maxAge: 60 * 60 * 24 * 7, // 7 days
5
5
  rememberMaxAge: 60 * 60 * 24 * 30, // 30 days
6
6
  };
7
+ /**
8
+ * Create an auth factory.
9
+ *
10
+ * @example
11
+ * // resolveUser — user() returns SafeUser
12
+ * createAuth<SafeUser>({ resolveUser: ... })
13
+ *
14
+ * // sessionFields — user() returns Pick<DbUser, 'id' | 'email' | 'name'>
15
+ * const sessionFields = ['email', 'name'] as const;
16
+ * createAuth<DbUser, (typeof sessionFields)[number]>({ sessionFields, ... })
17
+ */
7
18
  export function createAuth(config) {
8
19
  if (!config.secret || config.secret.length < 32) {
9
20
  throw new Error('secret must be at least 32 characters');
package/dist/index.d.ts CHANGED
@@ -9,4 +9,4 @@ export { createRateLimiter } from './rate-limit';
9
9
  export { MemoryRateLimitStore } from './rate-limit/memory-store';
10
10
  export { createTOTP } from './totp';
11
11
  export { generateRecoveryCodes, verifyRecoveryCode } from './totp/recovery';
12
- export type { AnyUser, CookieBridge, ConfigurableCookieOptions, CookieOptions, SessionPayload, AuthConfig, HashConfig, LoginOptions, AuthInstance, HashInstance, TokenVerifierConfig, TokenVerifierInstance, RateLimitStore, RateLimiterConfig, RateLimitResult, TOTPConfig, TOTPInstance, RecoveryCodeResult, } from './types';
12
+ export type { AnyUser, CookieBridge, ConfigurableCookieOptions, CookieOptions, SessionPayload, AuthConfig, AuthConfigWithResolveUser, AuthConfigWithSessionFields, HashConfig, LoginOptions, AuthInstance, HashInstance, TokenVerifierConfig, TokenVerifierInstance, RateLimitStore, RateLimiterConfig, RateLimitResult, TOTPConfig, TOTPInstance, RecoveryCodeResult, } from './types';
package/dist/types.d.ts CHANGED
@@ -37,13 +37,19 @@ interface AuthConfigBase<TUser extends AnyUser> {
37
37
  cookie?: Partial<ConfigurableCookieOptions>;
38
38
  };
39
39
  hash?: HashInstance;
40
- resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
40
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
41
41
  credentialKey?: string;
42
42
  passwordField?: string;
43
43
  attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
44
44
  }
45
- /** Database-backed: `user()` calls `resolveUser(id)` on every request. */
46
- interface AuthConfigWithResolveUser<TUser extends AnyUser> extends AuthConfigBase<TUser> {
45
+ /**
46
+ * Database-backed: `user()` calls `resolveUser(id)` on every request.
47
+ *
48
+ * `TUser` is the safe user type returned by `resolveUser` — this is what `user()` exposes.
49
+ * It should NOT include sensitive fields like password.
50
+ * `resolveUserByCredentials` can return any shape (it only needs id + password field for verification).
51
+ */
52
+ export interface AuthConfigWithResolveUser<TUser extends AnyUser> extends AuthConfigBase<TUser> {
47
53
  resolveUser: (id: string) => Promise<TUser | null | undefined>;
48
54
  /** Cannot use `sessionFields` together with `resolveUser`. */
49
55
  sessionFields?: never;
@@ -63,22 +69,22 @@ interface AuthConfigWithResolveUser<TUser extends AnyUser> extends AuthConfigBas
63
69
  * **ID type:** `user()` always returns `id` as a `string` on subsequent
64
70
  * requests (read from cookie), even if the original `TUser.id` was a number.
65
71
  */
66
- interface AuthConfigWithSessionFields<TUser extends AnyUser> extends AuthConfigBase<TUser> {
72
+ export interface AuthConfigWithSessionFields<TUser extends AnyUser, K extends keyof TUser & string = keyof TUser & string> extends AuthConfigBase<TUser> {
67
73
  /** Cannot use `resolveUser` together with `sessionFields`. */
68
74
  resolveUser?: never;
69
- sessionFields: (keyof TUser & string)[];
75
+ sessionFields: readonly K[];
70
76
  }
71
77
  export type AuthConfig<TUser extends AnyUser = AnyUser> = AuthConfigWithResolveUser<TUser> | AuthConfigWithSessionFields<TUser>;
72
78
  export interface HashConfig {
73
79
  rounds?: number;
74
80
  }
75
- export interface AuthInstance<TUser extends AnyUser = AnyUser> {
81
+ export interface AuthInstance<TUser extends AnyUser = AnyUser, TSessionUser = TUser> {
76
82
  login(user: TUser, options?: LoginOptions): Promise<void>;
77
83
  loginById(id: string, options?: LoginOptions): Promise<void>;
78
84
  attempt(credentials: Record<string, any>, options?: LoginOptions): Promise<boolean>;
79
85
  logout(): Promise<void>;
80
86
  check(): Promise<boolean>;
81
- user(): Promise<TUser | null>;
87
+ user(): Promise<TSessionUser | null>;
82
88
  id(): Promise<string | null>;
83
89
  }
84
90
  export interface HashInstance {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/bcryptjs": "^3.0.0",
32
+ "@types/bun": "^1.3.11",
32
33
  "@types/node": "^20"
33
34
  },
34
35
  "peerDependencies": {
@@ -71,70 +71,87 @@ Returns a factory function. Call `auth()` per request to get an `AuthInstance` s
71
71
  #### AuthConfig
72
72
 
73
73
  ```typescript
74
- type AuthConfig<TUser extends AnyUser> = {
75
- secret: string; // 32+ chars, required throws if shorter
76
- cookie: CookieBridge; // required
77
-
78
- // Session mode — provide exactly ONE of these two:
79
- resolveUser?: (id: string) => Promise<TUser | null>; // DB-backed: user() calls this every request
80
- sessionFields?: (keyof TUser & string)[]; // Cookie-backed: user() reads from cookie, zero external calls
81
-
82
- // Session options
83
- session?: {
84
- cookieName?: string; // default: 'ideal_session'
85
- maxAge?: number; // default: 604800 (7 days, in seconds)
86
- rememberMaxAge?: number; // default: 2592000 (30 days, in seconds)
87
- cookie?: Partial<ConfigurableCookieOptions>;
88
- };
89
-
90
- // Laravel-style attempt (recommended)
74
+ // Session mode provide exactly ONE of resolveUser or sessionFields
75
+ // TypeScript will error if you provide both or neither
76
+
77
+ // resolveUser mode: TUser is the safe type returned by resolveUser and user()
78
+ type AuthConfigWithResolveUser<TUser> = {
79
+ secret: string;
80
+ cookie: CookieBridge;
81
+ resolveUser: (id: string) => Promise<TUser | null | undefined>;
82
+ sessionFields?: never;
83
+ // resolveUserByCredentials can return any shape — only needs id + passwordField
84
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
91
85
  hash?: HashInstance;
92
- resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
93
- credentialKey?: string; // default: 'password'
94
- passwordField?: string; // default: 'password'
86
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
87
+ // ...session options
88
+ };
95
89
 
96
- // Manual attempt (escape hatch takes precedence if both provided)
97
- attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null>;
90
+ // sessionFields mode: user() returns Pick<TUser, 'id' | K>
91
+ type AuthConfigWithSessionFields<TUser, K extends keyof TUser> = {
92
+ secret: string;
93
+ cookie: CookieBridge;
94
+ resolveUser?: never;
95
+ sessionFields: K[];
96
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
97
+ hash?: HashInstance;
98
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
99
+ // ...session options
98
100
  };
99
101
  ```
100
102
 
101
103
  #### Session Modes
102
104
 
103
- **Database-backed (`resolveUser`):** Cookie stores only user ID. `user()` calls `resolveUser(id)` every request. Best for real-time data freshness.
105
+ **Database-backed (`resolveUser`):** Cookie stores only user ID. `user()` calls `resolveUser(id)` every request. Type `TUser` as the safe type — only select non-sensitive columns.
104
106
 
105
107
  ```typescript
106
- const auth = createAuth<User>({
108
+ type SafeUser = { id: string; email: string; name: string; role: string };
109
+
110
+ const auth = createAuth<SafeUser>({
107
111
  secret: process.env.IDEAL_AUTH_SECRET!,
108
112
  cookie: createCookieBridge(),
109
- resolveUser: async (id) => db.user.findUnique({ where: { id } }),
113
+ // Returns SafeUser no password
114
+ resolveUser: async (id) => db.user.findFirst({
115
+ where: { id },
116
+ columns: { id: true, email: true, name: true, role: true },
117
+ }),
118
+ // Can return full DB row — only used internally for hash verification
119
+ resolveUserByCredentials: async (creds) => db.user.findFirst({
120
+ where: { email: creds.email },
121
+ }),
110
122
  hash,
111
- resolveUserByCredentials: async (creds) =>
112
- db.user.findUnique({ where: { email: creds.email } }),
113
123
  });
124
+ const user = await auth().user(); // Type: SafeUser | null
114
125
  ```
115
126
 
116
- **Cookie-backed (`sessionFields`):** Cookie stores user ID + declared fields. `user()` reads from cookie zero external calls. Best for performance, stateless apps, or apps without a database.
127
+ **Cookie-backed (`sessionFields`):** Cookie stores user ID + declared fields. `user()` returns `Pick<TUser, 'id' | K>` password excluded from the type.
128
+
129
+ Define fields once as `const` and derive the type with `(typeof sessionFields)[number]`:
117
130
 
118
131
  ```typescript
119
- const auth = createAuth<User>({
132
+ type DbUser = { id: string; email: string; name: string; role: string; password: string };
133
+
134
+ const sessionFields = ['email', 'name', 'role'] as const;
135
+
136
+ const auth = createAuth<DbUser, (typeof sessionFields)[number]>({
120
137
  secret: process.env.IDEAL_AUTH_SECRET!,
121
138
  cookie: createCookieBridge(),
122
- sessionFields: ['email', 'name', 'role'],
139
+ sessionFields,
140
+ resolveUserByCredentials: async (creds) => db.user.findFirst({ where: { email: creds.email } }),
123
141
  hash,
124
- resolveUserByCredentials: async (creds) =>
125
- db.user.findUnique({ where: { email: creds.email } }),
126
142
  });
127
- // user() returns { id, email, name, role } from cookie
143
+ const user = await auth().user(); // Type: Pick<DbUser, 'id' | 'email' | 'name' | 'role'> | null
128
144
  ```
129
145
 
130
146
  Key rules:
131
- - Provide exactly one of `resolveUser` or `sessionFields` — both or neither throws
132
- - `id` is always stored don't include it in `sessionFields`
147
+ - Provide exactly one of `resolveUser` or `sessionFields` — TypeScript errors if both or neither
148
+ - `resolveUser` mode: `TUser` is the safe type. Only select non-sensitive fields in your query
149
+ - `sessionFields` mode: `user()` type is `Pick<TUser, 'id' | ...fields>` — password excluded automatically
150
+ - `resolveUserByCredentials` returns `AnyUser` — doesn't need to match `TUser`, only needs `id` + password field
151
+ - Password is stripped from the cached user after `attempt()` — even same-request `user()` won't expose it
133
152
  - Cookie limit is ~4KB — store basic fields only
134
- - `sessionFields` data is a snapshot from login time — stale if user updates profile mid-session
135
- - To refresh, call `auth().login(updatedUser)` after the update
136
153
  - `loginById()` requires `resolveUser` — throws with `sessionFields`
137
- - Both modes work with `attempt()`, `attemptUser`, `hash + resolveUserByCredentials`
154
+ - Never pass `user()` directly to the client — always pick the specific fields you need
138
155
 
139
156
  #### AuthInstance Methods
140
157