ideal-auth 0.5.1 → 0.6.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 +1 -1
- package/dist/auth-instance.d.ts +1 -1
- package/dist/auth-instance.js +13 -8
- package/dist/auth.d.ts +5 -2
- 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 +50 -37
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
|
@@ -9,7 +9,7 @@ interface AuthInstanceDeps<TUser extends AnyUser> {
|
|
|
9
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<
|
|
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,5 @@
|
|
|
1
|
-
import type { AnyUser, AuthInstance,
|
|
2
|
-
|
|
1
|
+
import type { AnyUser, AuthInstance, AuthConfigWithResolveUser, AuthConfigWithSessionFields } from './types';
|
|
2
|
+
/** Database-backed: `user()` returns `TUser` (the safe type from `resolveUser`). */
|
|
3
|
+
export declare function createAuth<TUser extends AnyUser>(config: AuthConfigWithResolveUser<TUser>): () => AuthInstance<TUser>;
|
|
4
|
+
/** Cookie-backed: `user()` returns only `id` + the declared `sessionFields`. */
|
|
5
|
+
export declare function createAuth<TUser extends AnyUser, K extends keyof TUser & string>(config: AuthConfigWithSessionFields<TUser, K>): () => AuthInstance<TUser, Pick<TUser, 'id' | K>>;
|
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: 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.0",
|
|
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,83 @@ 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.
|
|
117
128
|
|
|
118
129
|
```typescript
|
|
119
|
-
|
|
130
|
+
type DbUser = { id: string; email: string; name: string; role: string; password: string };
|
|
131
|
+
|
|
132
|
+
const auth = createAuth<DbUser>({
|
|
120
133
|
secret: process.env.IDEAL_AUTH_SECRET!,
|
|
121
134
|
cookie: createCookieBridge(),
|
|
122
135
|
sessionFields: ['email', 'name', 'role'],
|
|
136
|
+
resolveUserByCredentials: async (creds) => db.user.findFirst({ where: { email: creds.email } }),
|
|
123
137
|
hash,
|
|
124
|
-
resolveUserByCredentials: async (creds) =>
|
|
125
|
-
db.user.findUnique({ where: { email: creds.email } }),
|
|
126
138
|
});
|
|
127
|
-
|
|
139
|
+
const user = await auth().user(); // Type: Pick<DbUser, 'id' | 'email' | 'name' | 'role'> | null
|
|
128
140
|
```
|
|
129
141
|
|
|
130
142
|
Key rules:
|
|
131
|
-
- Provide exactly one of `resolveUser` or `sessionFields` — both or neither
|
|
132
|
-
- `
|
|
143
|
+
- Provide exactly one of `resolveUser` or `sessionFields` — TypeScript errors if both or neither
|
|
144
|
+
- `resolveUser` mode: `TUser` is the safe type. Only select non-sensitive fields in your query
|
|
145
|
+
- `sessionFields` mode: `user()` type is `Pick<TUser, 'id' | ...fields>` — password excluded automatically
|
|
146
|
+
- `resolveUserByCredentials` returns `AnyUser` — doesn't need to match `TUser`, only needs `id` + password field
|
|
147
|
+
- Password is stripped from the cached user after `attempt()` — even same-request `user()` won't expose it
|
|
133
148
|
- 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
149
|
- `loginById()` requires `resolveUser` — throws with `sessionFields`
|
|
137
|
-
-
|
|
150
|
+
- Never pass `user()` directly to the client — always pick the specific fields you need
|
|
138
151
|
|
|
139
152
|
#### AuthInstance Methods
|
|
140
153
|
|