ideal-auth 0.7.0 → 1.1.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 +5 -3
- package/dist/auth-instance.d.ts +1 -0
- package/dist/auth-instance.js +40 -0
- package/dist/auth.js +1 -0
- package/dist/session/seal.js +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +29 -2
- package/skills/ideal-auth/SKILL.md +30 -0
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
|
|
|
106
106
|
| `session.cookieName` | `string` | No | `'ideal_session'` |
|
|
107
107
|
| `session.maxAge` | `number` (seconds) | No | `604800` (7 days) |
|
|
108
108
|
| `session.rememberMaxAge` | `number` (seconds) | No | `2592000` (30 days) |
|
|
109
|
+
| `session.autoTouch` | `boolean` | No | `false`. Auto-extend sessions past halfway on `check()`/`user()`/`id()`. Enable for Express/Hono. Disable for Next.js. |
|
|
109
110
|
| `session.cookie` | `Partial<ConfigurableCookieOptions>` | No | secure in prod, sameSite lax, path / (`httpOnly` is always `true` — not configurable) |
|
|
110
111
|
|
|
111
112
|
#### AuthInstance Methods
|
|
@@ -116,9 +117,10 @@ Returns a function `auth()` that creates an `AuthInstance` on each call.
|
|
|
116
117
|
| `loginById(id, options?)` | `Promise<void>` | Resolve user by ID, then set session cookie |
|
|
117
118
|
| `attempt(credentials, options?)` | `Promise<boolean>` | Find user, verify password, login if valid |
|
|
118
119
|
| `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 |
|
|
120
|
+
| `check()` | `Promise<boolean>` | Is the session valid? (read-only) |
|
|
121
|
+
| `user()` | `Promise<User \| null>` | Get the authenticated user (read-only) |
|
|
122
|
+
| `id()` | `Promise<string \| null>` | Get the authenticated user's ID (read-only) |
|
|
123
|
+
| `touch()` | `Promise<void>` | Extend session expiry. Reseals past halfway (`autoTouch: false`) or immediately (`autoTouch: true`). |
|
|
122
124
|
|
|
123
125
|
All login methods accept an optional `LoginOptions` object:
|
|
124
126
|
|
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);
|
|
@@ -121,5 +146,20 @@ export function createAuthInstance(deps) {
|
|
|
121
146
|
const session = await readSession();
|
|
122
147
|
return session?.uid ?? null;
|
|
123
148
|
},
|
|
149
|
+
async touch() {
|
|
150
|
+
const session = await readSession();
|
|
151
|
+
if (!session)
|
|
152
|
+
return;
|
|
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);
|
|
163
|
+
},
|
|
124
164
|
};
|
|
125
165
|
}
|
package/dist/auth.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createAuth(config) {
|
|
|
34
34
|
maxAge: config.session?.maxAge ?? SESSION_DEFAULTS.maxAge,
|
|
35
35
|
rememberMaxAge: config.session?.rememberMaxAge ?? SESSION_DEFAULTS.rememberMaxAge,
|
|
36
36
|
cookieOptions: config.session?.cookie ?? {},
|
|
37
|
+
autoTouch: config.session?.autoTouch ?? false,
|
|
37
38
|
resolveUser: config.resolveUser,
|
|
38
39
|
sessionFields: config.sessionFields,
|
|
39
40
|
hash: config.hash,
|
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>;
|
|
@@ -86,6 +89,8 @@ export interface AuthInstance<TUser extends AnyUser = AnyUser> {
|
|
|
86
89
|
check(): Promise<boolean>;
|
|
87
90
|
user(): Promise<TUser | null>;
|
|
88
91
|
id(): Promise<string | null>;
|
|
92
|
+
/** 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. */
|
|
93
|
+
touch(): Promise<void>;
|
|
89
94
|
}
|
|
90
95
|
export interface HashInstance {
|
|
91
96
|
make(password: string): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ideal-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.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
|
}
|
|
@@ -154,6 +154,7 @@ Key rules:
|
|
|
154
154
|
| `check()` | `Promise<boolean>` | Is the session valid? (fast, cached) |
|
|
155
155
|
| `user()` | `Promise<TUser \| null>` | Get the authenticated user (from DB with `resolveUser`, or from cookie with `sessionFields`) |
|
|
156
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. |
|
|
157
158
|
|
|
158
159
|
#### LoginOptions
|
|
159
160
|
|
|
@@ -166,6 +167,35 @@ type LoginOptions = {
|
|
|
166
167
|
};
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
#### Session Extension
|
|
171
|
+
|
|
172
|
+
Two options for keeping active users logged in:
|
|
173
|
+
|
|
174
|
+
**`autoTouch: true`** — automatic. Enable for Express, Hono, Elysia, SvelteKit. `check()`/`user()`/`id()` auto-reseal past halfway. Do NOT use with Next.js (Server Components can't write cookies).
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const auth = createAuth<User>({
|
|
178
|
+
session: { autoTouch: true },
|
|
179
|
+
...
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Manual `touch()`** — call in middleware. When `autoTouch` is false (default), only reseals past halfway. When `autoTouch` is true, reseals immediately.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const session = auth();
|
|
187
|
+
if (await session.check()) {
|
|
188
|
+
await session.touch();
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| | `autoTouch: false` (default) | `autoTouch: true` |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `check()`/`user()`/`id()` | Read-only | Auto-reseals past halfway |
|
|
195
|
+
| `touch()` | Reseals past halfway | Reseals immediately |
|
|
196
|
+
|
|
197
|
+
`touch()` preserves original `iat` — `passwordChangedAt` invalidation still works. Does not update user data — use `auth().login(updatedUser)` for that.
|
|
198
|
+
|
|
169
199
|
#### `attempt()` — Two Modes
|
|
170
200
|
|
|
171
201
|
**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.
|