ideal-auth 1.0.0 → 1.2.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 +11 -4
- package/dist/auth-instance.d.ts +1 -0
- package/dist/auth-instance.js +35 -12
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/session/seal.js +1 -0
- package/dist/types.d.ts +8 -1
- package/package.json +1 -1
- package/skills/ideal-auth/SKILL.md +39 -7
package/README.md
CHANGED
|
@@ -88,7 +88,12 @@ await session.logout();
|
|
|
88
88
|
|
|
89
89
|
### `createAuth(config)`
|
|
90
90
|
|
|
91
|
-
Returns a function `auth()` that creates an `AuthInstance` on each call.
|
|
91
|
+
Returns a function `auth(options?)` that creates an `AuthInstance` on each call. Pass `{ autoTouch: true }` to enable automatic session extension for that request.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const session = auth(); // default — read-only check/user/id
|
|
95
|
+
const session = auth({ autoTouch: true }); // auto-extends session past halfway on check/user/id
|
|
96
|
+
```
|
|
92
97
|
|
|
93
98
|
#### Config
|
|
94
99
|
|
|
@@ -106,6 +111,7 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
|
|
|
106
111
|
| `session.cookieName` | `string` | No | `'ideal_session'` |
|
|
107
112
|
| `session.maxAge` | `number` (seconds) | No | `604800` (7 days) |
|
|
108
113
|
| `session.rememberMaxAge` | `number` (seconds) | No | `2592000` (30 days) |
|
|
114
|
+
| `session.autoTouch` | `boolean` | No | `false`. Auto-extend sessions past halfway on `check()`/`user()`/`id()`. Enable for Express/Hono. Disable for Next.js. |
|
|
109
115
|
| `session.cookie` | `Partial<ConfigurableCookieOptions>` | No | secure in prod, sameSite lax, path / (`httpOnly` is always `true` — not configurable) |
|
|
110
116
|
|
|
111
117
|
#### AuthInstance Methods
|
|
@@ -116,9 +122,10 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
|
|
|
116
122
|
| `loginById(id, options?)` | `Promise<void>` | Resolve user by ID, then set session cookie |
|
|
117
123
|
| `attempt(credentials, options?)` | `Promise<boolean>` | Find user, verify password, login if valid |
|
|
118
124
|
| `logout()` | `Promise<void>` | Delete session cookie |
|
|
119
|
-
| `check()` | `Promise<boolean>` | Is the session valid? |
|
|
120
|
-
| `user()` | `Promise<User \| null>` | Get the authenticated user |
|
|
121
|
-
| `id()` | `Promise<string \| null>` | Get the authenticated user's ID |
|
|
125
|
+
| `check()` | `Promise<boolean>` | Is the session valid? (read-only) |
|
|
126
|
+
| `user()` | `Promise<User \| null>` | Get the authenticated user (read-only) |
|
|
127
|
+
| `id()` | `Promise<string \| null>` | Get the authenticated user's ID (read-only) |
|
|
128
|
+
| `touch()` | `Promise<void>` | Extend session expiry. Reseals past halfway (`autoTouch: false`) or immediately (`autoTouch: true`). |
|
|
122
129
|
|
|
123
130
|
All login methods accept an optional `LoginOptions` object:
|
|
124
131
|
|
package/dist/auth-instance.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ interface AuthInstanceDeps<TUser extends AnyUser> {
|
|
|
6
6
|
maxAge: number;
|
|
7
7
|
rememberMaxAge: number;
|
|
8
8
|
cookieOptions: ConfigurableCookieOptions;
|
|
9
|
+
autoTouch: boolean;
|
|
9
10
|
resolveUser?: (id: string) => Promise<TUser | null | undefined>;
|
|
10
11
|
sessionFields?: (keyof TUser & string)[];
|
|
11
12
|
hash?: HashInstance;
|
package/dist/auth-instance.js
CHANGED
|
@@ -3,6 +3,7 @@ import { buildCookieOptions } from './session/cookie';
|
|
|
3
3
|
export function createAuthInstance(deps) {
|
|
4
4
|
let cachedPayload;
|
|
5
5
|
let cachedUser;
|
|
6
|
+
let didAutoTouch = false;
|
|
6
7
|
async function readSession() {
|
|
7
8
|
if (cachedPayload !== undefined)
|
|
8
9
|
return cachedPayload;
|
|
@@ -12,8 +13,31 @@ export function createAuthInstance(deps) {
|
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
14
15
|
cachedPayload = await unseal(raw, deps.secret);
|
|
16
|
+
// Auto-touch: reseal past halfway when enabled
|
|
17
|
+
if (deps.autoTouch && cachedPayload && !didAutoTouch) {
|
|
18
|
+
const elapsed = Math.floor(Date.now() / 1000) - cachedPayload.iat;
|
|
19
|
+
if (elapsed >= cachedPayload.ttl / 2) {
|
|
20
|
+
await resealSession(cachedPayload);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
15
23
|
return cachedPayload;
|
|
16
24
|
}
|
|
25
|
+
async function resealSession(session) {
|
|
26
|
+
didAutoTouch = true;
|
|
27
|
+
const ttl = session.ttl;
|
|
28
|
+
const now = Math.floor(Date.now() / 1000);
|
|
29
|
+
const newPayload = {
|
|
30
|
+
uid: session.uid,
|
|
31
|
+
iat: session.iat, // preserve original issued-at for passwordChangedAt checks
|
|
32
|
+
exp: now + ttl,
|
|
33
|
+
ttl,
|
|
34
|
+
...(session.data !== undefined && { data: session.data }),
|
|
35
|
+
};
|
|
36
|
+
const sealed = await seal(newPayload, deps.secret);
|
|
37
|
+
const opts = buildCookieOptions(ttl, deps.cookieOptions);
|
|
38
|
+
await deps.cookie.set(deps.cookieName, sealed, opts);
|
|
39
|
+
cachedPayload = newPayload;
|
|
40
|
+
}
|
|
17
41
|
function pickSessionData(user) {
|
|
18
42
|
if (!deps.sessionFields)
|
|
19
43
|
return undefined;
|
|
@@ -32,6 +56,7 @@ export function createAuthInstance(deps) {
|
|
|
32
56
|
uid: String(user.id),
|
|
33
57
|
iat: now,
|
|
34
58
|
exp: now + maxAge,
|
|
59
|
+
ttl: maxAge,
|
|
35
60
|
data: pickSessionData(user),
|
|
36
61
|
};
|
|
37
62
|
const sealed = await seal(payload, deps.secret);
|
|
@@ -125,18 +150,16 @@ export function createAuthInstance(deps) {
|
|
|
125
150
|
const session = await readSession();
|
|
126
151
|
if (!session)
|
|
127
152
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await deps.cookie.set(deps.cookieName, sealed, opts);
|
|
139
|
-
cachedPayload = newPayload;
|
|
153
|
+
if (didAutoTouch)
|
|
154
|
+
return; // already resealed by autoTouch on this request
|
|
155
|
+
// autoTouch enabled: reseal immediately (user opted in to cookie writes)
|
|
156
|
+
// autoTouch disabled: only reseal past halfway (conservative)
|
|
157
|
+
if (!deps.autoTouch) {
|
|
158
|
+
const elapsed = Math.floor(Date.now() / 1000) - session.iat;
|
|
159
|
+
if (elapsed < session.ttl / 2)
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await resealSession(session);
|
|
140
163
|
},
|
|
141
164
|
};
|
|
142
165
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyUser, AuthInstance, AuthConfig } from './types';
|
|
1
|
+
import type { AnyUser, AuthInstance, AuthConfig, AuthFactoryOptions } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Create an auth factory.
|
|
4
4
|
*
|
|
@@ -9,4 +9,4 @@ import type { AnyUser, AuthInstance, AuthConfig } from './types';
|
|
|
9
9
|
* type SessionUser = { id: string; email: string; name: string };
|
|
10
10
|
* createAuth<SessionUser>({ ... })
|
|
11
11
|
*/
|
|
12
|
-
export declare function createAuth<TUser extends AnyUser>(config: AuthConfig<TUser>): () => AuthInstance<TUser>;
|
|
12
|
+
export declare function createAuth<TUser extends AnyUser>(config: AuthConfig<TUser>): (options?: AuthFactoryOptions) => AuthInstance<TUser>;
|
package/dist/auth.js
CHANGED
|
@@ -27,13 +27,15 @@ export function createAuth(config) {
|
|
|
27
27
|
if (config.sessionFields && config.sessionFields.filter((f) => f !== 'id').length === 0) {
|
|
28
28
|
throw new Error('sessionFields must contain at least one field besides id');
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
const configAutoTouch = config.session?.autoTouch ?? false;
|
|
31
|
+
return (options) => createAuthInstance({
|
|
31
32
|
secret: config.secret,
|
|
32
33
|
cookie: config.cookie,
|
|
33
34
|
cookieName: config.session?.cookieName ?? SESSION_DEFAULTS.cookieName,
|
|
34
35
|
maxAge: config.session?.maxAge ?? SESSION_DEFAULTS.maxAge,
|
|
35
36
|
rememberMaxAge: config.session?.rememberMaxAge ?? SESSION_DEFAULTS.rememberMaxAge,
|
|
36
37
|
cookieOptions: config.session?.cookie ?? {},
|
|
38
|
+
autoTouch: options?.autoTouch ?? configAutoTouch,
|
|
37
39
|
resolveUser: config.resolveUser,
|
|
38
40
|
sessionFields: config.sessionFields,
|
|
39
41
|
hash: config.hash,
|
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, AuthConfigWithResolveUser, AuthConfigWithSessionFields, 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, AuthFactoryOptions, HashInstance, TokenVerifierConfig, TokenVerifierInstance, RateLimitStore, RateLimiterConfig, RateLimitResult, TOTPConfig, TOTPInstance, RecoveryCodeResult, } from './types';
|
package/dist/session/seal.js
CHANGED
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
|
+
ttl: number;
|
|
25
26
|
data?: Record<string, unknown>;
|
|
26
27
|
}
|
|
27
28
|
export interface LoginOptions {
|
|
@@ -35,6 +36,8 @@ interface AuthConfigBase<TUser extends AnyUser> {
|
|
|
35
36
|
maxAge?: number;
|
|
36
37
|
rememberMaxAge?: number;
|
|
37
38
|
cookie?: Partial<ConfigurableCookieOptions>;
|
|
39
|
+
/** Automatically extend session on read when past the halfway point. Default: false. */
|
|
40
|
+
autoTouch?: boolean;
|
|
38
41
|
};
|
|
39
42
|
hash?: HashInstance;
|
|
40
43
|
resolveUserByCredentials?: (credentials: Record<string, any>) => Promise<AnyUser | null | undefined>;
|
|
@@ -75,6 +78,10 @@ export interface AuthConfigWithSessionFields<TUser extends AnyUser, K extends ke
|
|
|
75
78
|
sessionFields: K[];
|
|
76
79
|
}
|
|
77
80
|
export type AuthConfig<TUser extends AnyUser = AnyUser> = AuthConfigWithResolveUser<TUser> | AuthConfigWithSessionFields<TUser>;
|
|
81
|
+
export interface AuthFactoryOptions {
|
|
82
|
+
/** Override autoTouch for this request. When true, check()/user()/id() auto-extend the session past the halfway point. */
|
|
83
|
+
autoTouch?: boolean;
|
|
84
|
+
}
|
|
78
85
|
export interface HashConfig {
|
|
79
86
|
rounds?: number;
|
|
80
87
|
}
|
|
@@ -86,7 +93,7 @@ export interface AuthInstance<TUser extends AnyUser = AnyUser> {
|
|
|
86
93
|
check(): Promise<boolean>;
|
|
87
94
|
user(): Promise<TUser | null>;
|
|
88
95
|
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. */
|
|
96
|
+
/** Re-seal the session cookie with a fresh expiry. When autoTouch is disabled (default), only reseals past the halfway point. No database call needed. Does nothing if no valid session exists or if already resealed on this request. */
|
|
90
97
|
touch(): Promise<void>;
|
|
91
98
|
}
|
|
92
99
|
export interface HashInstance {
|
package/package.json
CHANGED
|
@@ -64,9 +64,14 @@ Generate encryption key: `bunx ideal-auth encryption-key` (for encrypting TOTP s
|
|
|
64
64
|
|
|
65
65
|
## API Reference
|
|
66
66
|
|
|
67
|
-
### `createAuth(config): () => AuthInstance`
|
|
67
|
+
### `createAuth(config): (options?) => AuthInstance`
|
|
68
68
|
|
|
69
|
-
Returns a factory function. Call `auth()` per request to get an `AuthInstance` scoped to that request's cookies. The instance caches the session payload and user — call it once per request and reuse.
|
|
69
|
+
Returns a factory function. Call `auth()` per request to get an `AuthInstance` scoped to that request's cookies. Pass `{ autoTouch: true }` to enable automatic session extension for that request. The instance caches the session payload and user — call it once per request and reuse.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const session = auth(); // read-only check/user/id
|
|
73
|
+
const session = auth({ autoTouch: true }); // auto-extends past halfway on check/user/id
|
|
74
|
+
```
|
|
70
75
|
|
|
71
76
|
#### AuthConfig
|
|
72
77
|
|
|
@@ -167,19 +172,46 @@ type LoginOptions = {
|
|
|
167
172
|
};
|
|
168
173
|
```
|
|
169
174
|
|
|
170
|
-
#### Session Extension
|
|
175
|
+
#### Session Extension
|
|
171
176
|
|
|
172
|
-
|
|
177
|
+
Three ways to extend sessions for active users:
|
|
178
|
+
|
|
179
|
+
**Global `autoTouch`** — config level. For Express, Hono, Elysia, SvelteKit where every route can write cookies:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const auth = createAuth<User>({
|
|
183
|
+
session: { autoTouch: true },
|
|
184
|
+
...
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Per-request `autoTouch`** — pass when calling `auth()`. Ideal for Next.js where middleware can write cookies but Server Components can't:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Next.js middleware — autoTouch for this request only
|
|
192
|
+
const session = auth({ autoTouch: true });
|
|
193
|
+
await session.check(); // auto-extends past halfway
|
|
194
|
+
|
|
195
|
+
// Next.js Server Component — default, read-only
|
|
196
|
+
const session = auth();
|
|
197
|
+
await session.check(); // no cookie writes
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Manual `touch()`** — explicit call in middleware. When `autoTouch` is false (default), only reseals past halfway. When `autoTouch` is true, reseals immediately:
|
|
173
201
|
|
|
174
202
|
```typescript
|
|
175
|
-
// In middleware — where cookie writes are allowed
|
|
176
203
|
const session = auth();
|
|
177
204
|
if (await session.check()) {
|
|
178
|
-
await session.touch();
|
|
205
|
+
await session.touch();
|
|
179
206
|
}
|
|
180
207
|
```
|
|
181
208
|
|
|
182
|
-
|
|
209
|
+
| | `autoTouch: false` (default) | `autoTouch: true` |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `check()`/`user()`/`id()` | Read-only | Auto-reseals past halfway |
|
|
212
|
+
| `touch()` | Reseals past halfway | Reseals immediately |
|
|
213
|
+
|
|
214
|
+
`touch()` preserves original `iat` — `passwordChangedAt` invalidation still works. Does not update user data — use `auth().login(updatedUser)` for that.
|
|
183
215
|
|
|
184
216
|
#### `attempt()` — Two Modes
|
|
185
217
|
|