ideal-auth 0.2.0 → 0.3.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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "ideal-auth",
3
+ "description": "Auth primitives for the JS ecosystem. Provides complete API reference, cookie bridge patterns for every framework, security best practices, and implementation guides for ideal-auth.",
4
+ "author": "Ramon Malcolm"
5
+ }
package/README.md CHANGED
@@ -477,7 +477,7 @@ const limiter = createRateLimiter({
477
477
 
478
478
  ## How It Works
479
479
 
480
- Sessions are **stateless, encrypted cookies** powered by iron-session (AES-256-GCM + HMAC integrity).
480
+ Sessions are **stateless, encrypted cookies** powered by iron-session (AES-256-CBC + HMAC integrity).
481
481
 
482
482
  1. **`login(user)`** — Creates a `SessionPayload { uid, iat, exp }`, seals it with iron-session, writes the encrypted string to the cookie via the bridge.
483
483
  2. **`check()` / `user()` / `id()`** — Reads the cookie via the bridge, unseals the payload, checks expiry. `user()` additionally calls `resolveUser(id)` to fetch the full user.
@@ -519,11 +519,23 @@ cookie: {
519
519
 
520
520
  | Package | Purpose |
521
521
  | --- | --- |
522
- | `iron-session` | Session sealing/unsealing (AES-256-GCM + HMAC) |
522
+ | `iron-session` | Session sealing/unsealing (AES-256-CBC + HMAC) |
523
523
  | `bcryptjs` | Password hashing |
524
524
 
525
525
  Zero framework imports. Works in Node, Bun, Deno, and edge runtimes.
526
526
 
527
+ ## Claude Code
528
+
529
+ 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:
530
+
531
+ ```bash
532
+ claude plugin install github:ramonmalcolm10/ideal-auth
533
+ ```
534
+
535
+ 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.
536
+
537
+ ---
538
+
527
539
  ## Support
528
540
 
529
541
  If this saved you time, consider supporting the project:
@@ -6,7 +6,8 @@ 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>;
10
+ sessionFields?: (keyof TUser & string)[];
10
11
  hash?: HashInstance;
11
12
  resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
12
13
  credentialKey: string;
@@ -14,6 +14,17 @@ export function createAuthInstance(deps) {
14
14
  cachedPayload = await unseal(raw, deps.secret);
15
15
  return cachedPayload;
16
16
  }
17
+ function pickSessionData(user) {
18
+ if (!deps.sessionFields)
19
+ return undefined;
20
+ const data = {};
21
+ for (const field of deps.sessionFields) {
22
+ if (field !== 'id' && field in user) {
23
+ data[field] = user[field];
24
+ }
25
+ }
26
+ return Object.keys(data).length > 0 ? data : undefined;
27
+ }
17
28
  async function writeSession(user, options) {
18
29
  const maxAge = options?.remember ? deps.rememberMaxAge : deps.maxAge;
19
30
  const now = Math.floor(Date.now() / 1000);
@@ -21,6 +32,7 @@ export function createAuthInstance(deps) {
21
32
  uid: String(user.id),
22
33
  iat: now,
23
34
  exp: now + maxAge,
35
+ data: pickSessionData(user),
24
36
  };
25
37
  const sealed = await seal(payload, deps.secret);
26
38
  const opts = options?.remember === false
@@ -28,13 +40,19 @@ export function createAuthInstance(deps) {
28
40
  : buildCookieOptions(maxAge, deps.cookieOptions);
29
41
  await deps.cookie.set(deps.cookieName, sealed, opts);
30
42
  cachedPayload = payload;
31
- cachedUser = user;
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;
32
47
  }
33
48
  return {
34
49
  async login(user, options) {
35
50
  await writeSession(user, options);
36
51
  },
37
52
  async loginById(id, options) {
53
+ if (!deps.resolveUser) {
54
+ throw new Error('loginById requires resolveUser — use login(user) instead when using sessionFields');
55
+ }
38
56
  const user = await deps.resolveUser(id);
39
57
  if (!user)
40
58
  throw new Error('Login failed');
@@ -81,7 +99,17 @@ export function createAuthInstance(deps) {
81
99
  cachedUser = null;
82
100
  return null;
83
101
  }
84
- cachedUser = await deps.resolveUser(session.uid);
102
+ // Cookie-backed: reconstruct user from session data
103
+ if (deps.sessionFields && session.data) {
104
+ cachedUser = { id: session.uid, ...session.data };
105
+ return cachedUser;
106
+ }
107
+ // Database-backed: resolve user via callback
108
+ if (deps.resolveUser) {
109
+ cachedUser = await deps.resolveUser(session.uid);
110
+ return cachedUser;
111
+ }
112
+ cachedUser = null;
85
113
  return cachedUser;
86
114
  },
87
115
  async id() {
package/dist/auth.js CHANGED
@@ -8,6 +8,15 @@ export function createAuth(config) {
8
8
  if (!config.secret || config.secret.length < 32) {
9
9
  throw new Error('secret must be at least 32 characters');
10
10
  }
11
+ if (config.resolveUser && config.sessionFields) {
12
+ throw new Error('Provide either resolveUser or sessionFields, not both');
13
+ }
14
+ if (!config.resolveUser && !config.sessionFields) {
15
+ throw new Error('Provide either resolveUser or sessionFields');
16
+ }
17
+ if (config.sessionFields && config.sessionFields.filter((f) => f !== 'id').length === 0) {
18
+ throw new Error('sessionFields must contain at least one field besides id');
19
+ }
11
20
  return () => createAuthInstance({
12
21
  secret: config.secret,
13
22
  cookie: config.cookie,
@@ -16,6 +25,7 @@ export function createAuth(config) {
16
25
  rememberMaxAge: config.session?.rememberMaxAge ?? SESSION_DEFAULTS.rememberMaxAge,
17
26
  cookieOptions: config.session?.cookie ?? {},
18
27
  resolveUser: config.resolveUser,
28
+ sessionFields: config.sessionFields,
19
29
  hash: config.hash,
20
30
  resolveUserByCredentials: config.resolveUserByCredentials,
21
31
  credentialKey: config.credentialKey ?? 'password',
@@ -11,7 +11,12 @@ export async function unseal(sealed, secret) {
11
11
  return null;
12
12
  if (data.exp < Math.floor(Date.now() / 1000))
13
13
  return null;
14
- return data;
14
+ return {
15
+ uid: data.uid,
16
+ iat: data.iat,
17
+ exp: data.exp,
18
+ ...(data.data !== undefined && { data: data.data }),
19
+ };
15
20
  }
16
21
  catch {
17
22
  return null;
package/dist/types.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface SessionPayload {
22
22
  uid: string;
23
23
  iat: number;
24
24
  exp: number;
25
+ data?: Record<string, unknown>;
25
26
  }
26
27
  export interface LoginOptions {
27
28
  remember?: boolean;
@@ -35,7 +36,27 @@ export interface AuthConfig<TUser extends AnyUser = AnyUser> {
35
36
  rememberMaxAge?: number;
36
37
  cookie?: Partial<ConfigurableCookieOptions>;
37
38
  };
38
- resolveUser: (id: string) => Promise<TUser | null>;
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)[];
39
60
  hash?: HashInstance;
40
61
  resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<TUser | null>;
41
62
  credentialKey?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideal-auth",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Auth primitives for the JS ecosystem. Zero framework dependencies.",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -20,7 +20,9 @@
20
20
  "ideal-auth": "./dist/bin/ideal-auth.js"
21
21
  },
22
22
  "files": [
23
- "dist"
23
+ "dist",
24
+ ".claude-plugin",
25
+ "skills"
24
26
  ],
25
27
  "dependencies": {
26
28
  "iron-session": "^8.0.4",