ideal-auth 0.6.1 → 1.0.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
@@ -552,18 +552,6 @@ cookie: {
552
552
 
553
553
  Zero framework imports. Works in Node, Bun, Deno, and edge runtimes.
554
554
 
555
- ## Claude Code
556
-
557
- If you use [Claude Code](https://claude.com/claude-code), install the ideal-auth plugin so your AI assistant knows the full API, cookie bridge patterns, security best practices, and implementation guides:
558
-
559
- ```bash
560
- claude plugin install github:ramonmalcolm10/ideal-auth
561
- ```
562
-
563
- After installing, Claude Code will automatically help with auth setup, login/registration flows, middleware, 2FA, password reset, rate limiting, and more — using the correct patterns for your framework.
564
-
565
- ---
566
-
567
555
  ## Support
568
556
 
569
557
  If this saved you time, consider supporting the project:
@@ -7,7 +7,7 @@ interface AuthInstanceDeps<TUser extends AnyUser> {
7
7
  rememberMaxAge: number;
8
8
  cookieOptions: ConfigurableCookieOptions;
9
9
  resolveUser?: (id: string) => Promise<TUser | null | undefined>;
10
- sessionFields?: readonly (keyof TUser & string)[];
10
+ sessionFields?: (keyof TUser & string)[];
11
11
  hash?: HashInstance;
12
12
  resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
13
13
  credentialKey: string;
@@ -121,5 +121,22 @@ export function createAuthInstance(deps) {
121
121
  const session = await readSession();
122
122
  return session?.uid ?? null;
123
123
  },
124
+ async touch() {
125
+ const session = await readSession();
126
+ if (!session)
127
+ return;
128
+ const maxAge = session.exp - session.iat;
129
+ const now = Math.floor(Date.now() / 1000);
130
+ const newPayload = {
131
+ uid: session.uid,
132
+ iat: session.iat, // preserve original issued-at for passwordChangedAt checks
133
+ exp: now + maxAge,
134
+ ...(session.data !== undefined && { data: session.data }),
135
+ };
136
+ const sealed = await seal(newPayload, deps.secret);
137
+ const opts = buildCookieOptions(maxAge, deps.cookieOptions);
138
+ await deps.cookie.set(deps.cookieName, sealed, opts);
139
+ cachedPayload = newPayload;
140
+ },
124
141
  };
125
142
  }
package/dist/auth.d.ts CHANGED
@@ -2,12 +2,11 @@ import type { AnyUser, AuthInstance, AuthConfig } from './types';
2
2
  /**
3
3
  * Create an auth factory.
4
4
  *
5
- * @example
6
- * // resolveUser user() returns SafeUser
7
- * createAuth<SafeUser>({ resolveUser: ... })
5
+ * `TUser` is the session user type — what `user()` returns.
6
+ * Do not include sensitive fields like password in this type.
8
7
  *
9
- * // sessionFields — user() returns Pick<DbUser, 'id' | 'email' | 'name'>
10
- * const sessionFields = ['email', 'name'] as const;
11
- * createAuth<DbUser, (typeof sessionFields)[number]>({ sessionFields, ... })
8
+ * @example
9
+ * type SessionUser = { id: string; email: string; name: string };
10
+ * createAuth<SessionUser>({ ... })
12
11
  */
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>>;
12
+ export declare function createAuth<TUser extends AnyUser>(config: AuthConfig<TUser>): () => AuthInstance<TUser>;
package/dist/auth.js CHANGED
@@ -7,13 +7,12 @@ const SESSION_DEFAULTS = {
7
7
  /**
8
8
  * Create an auth factory.
9
9
  *
10
- * @example
11
- * // resolveUser user() returns SafeUser
12
- * createAuth<SafeUser>({ resolveUser: ... })
10
+ * `TUser` is the session user type — what `user()` returns.
11
+ * Do not include sensitive fields like password in this type.
13
12
  *
14
- * // sessionFields — user() returns Pick<DbUser, 'id' | 'email' | 'name'>
15
- * const sessionFields = ['email', 'name'] as const;
16
- * createAuth<DbUser, (typeof sessionFields)[number]>({ sessionFields, ... })
13
+ * @example
14
+ * type SessionUser = { id: string; email: string; name: string };
15
+ * createAuth<SessionUser>({ ... })
17
16
  */
18
17
  export function createAuth(config) {
19
18
  if (!config.secret || config.secret.length < 32) {
package/dist/types.d.ts CHANGED
@@ -72,20 +72,22 @@ export interface AuthConfigWithResolveUser<TUser extends AnyUser> extends AuthCo
72
72
  export interface AuthConfigWithSessionFields<TUser extends AnyUser, K extends keyof TUser & string = keyof TUser & string> extends AuthConfigBase<TUser> {
73
73
  /** Cannot use `resolveUser` together with `sessionFields`. */
74
74
  resolveUser?: never;
75
- sessionFields: readonly K[];
75
+ sessionFields: K[];
76
76
  }
77
77
  export type AuthConfig<TUser extends AnyUser = AnyUser> = AuthConfigWithResolveUser<TUser> | AuthConfigWithSessionFields<TUser>;
78
78
  export interface HashConfig {
79
79
  rounds?: number;
80
80
  }
81
- export interface AuthInstance<TUser extends AnyUser = AnyUser, TSessionUser = TUser> {
81
+ export interface AuthInstance<TUser extends AnyUser = AnyUser> {
82
82
  login(user: TUser, options?: LoginOptions): Promise<void>;
83
83
  loginById(id: string, options?: LoginOptions): Promise<void>;
84
84
  attempt(credentials: Record<string, any>, options?: LoginOptions): Promise<boolean>;
85
85
  logout(): Promise<void>;
86
86
  check(): Promise<boolean>;
87
- user(): Promise<TSessionUser | null>;
87
+ user(): Promise<TUser | null>;
88
88
  id(): Promise<string | null>;
89
+ /** Re-seal the session cookie with a fresh expiry. No database call needed. Does nothing if no valid session exists. */
90
+ touch(): Promise<void>;
89
91
  }
90
92
  export interface HashInstance {
91
93
  make(password: string): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "0.6.1",
3
+ "version": "1.0.0",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -51,5 +51,32 @@
51
51
  "type": "git",
52
52
  "url": "git+https://github.com/ramonmalcolm10/ideal-auth.git"
53
53
  },
54
- "license": "MIT"
54
+ "license": "MIT",
55
+ "keywords": [
56
+ "auth",
57
+ "authentication",
58
+ "session",
59
+ "cookie",
60
+ "login",
61
+ "password",
62
+ "bcrypt",
63
+ "argon2",
64
+ "hash",
65
+ "totp",
66
+ "2fa",
67
+ "two-factor",
68
+ "mfa",
69
+ "rate-limit",
70
+ "csrf",
71
+ "encryption",
72
+ "iron-session",
73
+ "nextjs",
74
+ "sveltekit",
75
+ "express",
76
+ "hono",
77
+ "nuxt",
78
+ "bun",
79
+ "passkey",
80
+ "webauthn"
81
+ ]
55
82
  }
@@ -71,33 +71,28 @@ Returns a factory function. Call `auth()` per request to get an `AuthInstance` s
71
71
  #### AuthConfig
72
72
 
73
73
  ```typescript
74
- // Session modeprovide 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>;
85
- hash?: HashInstance;
86
- attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
87
- // ...session options
88
- };
89
-
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
100
- };
74
+ // TUser = session user type what user() returns. Do not include password.
75
+ // Provide exactly ONE of resolveUser or sessionFields.
76
+ // TypeScript will error if you provide both or neither.
77
+
78
+ type AuthConfig<TUser> =
79
+ | {
80
+ resolveUser: (id: string) => Promise<TUser | null | undefined>;
81
+ sessionFields?: never;
82
+ // resolveUserByCredentials can return any shape — only needs id + passwordField
83
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
84
+ hash?: HashInstance;
85
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
86
+ // ...session, secret, cookie
87
+ }
88
+ | {
89
+ resolveUser?: never;
90
+ sessionFields: (keyof TUser & string)[];
91
+ resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
92
+ hash?: HashInstance;
93
+ attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
94
+ // ...session, secret, cookie
95
+ };
101
96
  ```
102
97
 
103
98
  #### Session Modes
@@ -124,29 +119,24 @@ const auth = createAuth<SafeUser>({
124
119
  const user = await auth().user(); // Type: SafeUser | null
125
120
  ```
126
121
 
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]`:
122
+ **Cookie-backed (`sessionFields`):** Cookie stores user ID + declared fields. `user()` returns `TUser | null`. Since `TUser` should not include password, the type is already safe.
130
123
 
131
124
  ```typescript
132
- type DbUser = { id: string; email: string; name: string; role: string; password: string };
133
-
134
- const sessionFields = ['email', 'name', 'role'] as const;
125
+ type SessionUser = { id: string; email: string; name: string; role: string }; // no password
135
126
 
136
- const auth = createAuth<DbUser, (typeof sessionFields)[number]>({
127
+ const auth = createAuth<SessionUser>({
137
128
  secret: process.env.IDEAL_AUTH_SECRET!,
138
129
  cookie: createCookieBridge(),
139
- sessionFields,
130
+ sessionFields: ['email', 'name', 'role'],
140
131
  resolveUserByCredentials: async (creds) => db.user.findFirst({ where: { email: creds.email } }),
141
132
  hash,
142
133
  });
143
- const user = await auth().user(); // Type: Pick<DbUser, 'id' | 'email' | 'name' | 'role'> | null
134
+ const user = await auth().user(); // Type: SessionUser | null
144
135
  ```
145
136
 
146
137
  Key rules:
138
+ - `TUser` is the session user type — what `user()` returns. Do not include password.
147
139
  - 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
140
  - `resolveUserByCredentials` returns `AnyUser` — doesn't need to match `TUser`, only needs `id` + password field
151
141
  - Password is stripped from the cached user after `attempt()` — even same-request `user()` won't expose it
152
142
  - Cookie limit is ~4KB — store basic fields only
@@ -164,6 +154,7 @@ Key rules:
164
154
  | `check()` | `Promise<boolean>` | Is the session valid? (fast, cached) |
165
155
  | `user()` | `Promise<TUser \| null>` | Get the authenticated user (from DB with `resolveUser`, or from cookie with `sessionFields`) |
166
156
  | `id()` | `Promise<string \| null>` | Get the authenticated user's ID |
157
+ | `touch()` | `Promise<void>` | Re-seal the session cookie with a fresh expiry. No database call needed. |
167
158
 
168
159
  #### LoginOptions
169
160
 
@@ -176,6 +167,20 @@ type LoginOptions = {
176
167
  };
177
168
  ```
178
169
 
170
+ #### Session Extension with `touch()`
171
+
172
+ Sessions have a fixed expiry. Call `touch()` in middleware to extend the session for active users:
173
+
174
+ ```typescript
175
+ // In middleware — where cookie writes are allowed
176
+ const session = auth();
177
+ if (await session.check()) {
178
+ await session.touch(); // re-seals cookie with fresh exp
179
+ }
180
+ ```
181
+
182
+ `touch()` re-seals with the same `maxAge` as the original session. No database call needed. `check()`, `user()`, and `id()` are read-only — they never write cookies. Only call `touch()` where cookie writes are allowed (middleware, route handlers, server actions — NOT Server Components).
183
+
179
184
  #### `attempt()` — Two Modes
180
185
 
181
186
  **Laravel-style (recommended):** Provide `hash` and `resolveUserByCredentials`. The `attempt()` method strips the credential key (default `'password'`) from credentials, looks up the user with remaining fields, and verifies the hash automatically.