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 +1 -1
- package/dist/auth-instance.d.ts +2 -2
- package/dist/auth-instance.js +13 -8
- package/dist/auth.d.ts +12 -1
- package/dist/auth.js +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +13 -7
- package/package.json +2 -1
- package/skills/ideal-auth/SKILL.md +55 -38
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<
|
|
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 | — |
|
package/dist/auth-instance.d.ts
CHANGED
|
@@ -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<
|
|
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>;
|
package/dist/auth-instance.js
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
74
|
-
if (!
|
|
78
|
+
const dbUser = await deps.resolveUserByCredentials(lookup);
|
|
79
|
+
if (!dbUser)
|
|
75
80
|
return false;
|
|
76
|
-
const storedHash =
|
|
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(
|
|
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
|
-
|
|
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<
|
|
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
|
-
/**
|
|
46
|
-
|
|
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:
|
|
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<
|
|
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.
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
attemptUser?: (credentials: Record<string, any>) => Promise<TUser | null | undefined>;
|
|
87
|
+
// ...session options
|
|
88
|
+
};
|
|
95
89
|
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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()`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
132
|
-
- `
|
|
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
|
-
-
|
|
154
|
+
- Never pass `user()` directly to the client — always pick the specific fields you need
|
|
138
155
|
|
|
139
156
|
#### AuthInstance Methods
|
|
140
157
|
|