peta-auth 0.1.0 → 0.1.2
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 +138 -34
- package/dist/crypto-Ln_Mj_zp.d.mts +19 -0
- package/dist/csrf.d.mts +10 -0
- package/dist/csrf.mjs +13 -0
- package/dist/elysia.d.mts +35 -5
- package/dist/elysia.mjs +10 -1
- package/dist/hono.d.mts +8 -8
- package/dist/hono.mjs +8 -1
- package/dist/index.d.mts +19 -2
- package/dist/index.mjs +28 -1
- package/dist/jwt.d.mts +11 -0
- package/dist/jwt.mjs +28 -0
- package/dist/nuxt.d.mts +4 -3
- package/dist/nuxt.mjs +8 -2
- package/dist/{session-DYH_m3lO.d.mts → session-z20gaFVT.d.mts} +2 -19
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ bun add elysia # for peta-auth/elysia
|
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
import { Hono } from 'hono'
|
|
29
|
-
import { session } from 'peta-auth/hono'
|
|
29
|
+
import { session, requireSession } from 'peta-auth/hono'
|
|
30
30
|
|
|
31
31
|
const app = new Hono()
|
|
32
32
|
|
|
@@ -35,19 +35,18 @@ app.use('*', session({
|
|
|
35
35
|
cookieName: 'my-session',
|
|
36
36
|
}))
|
|
37
37
|
|
|
38
|
-
app.get('/profile', (c) => {
|
|
39
|
-
const s = c.var.session
|
|
40
|
-
if (!s.user) return c.json({ error: 'unauthorized' }, 401)
|
|
41
|
-
return c.json(s.user)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
38
|
app.post('/login', async (c) => {
|
|
45
39
|
const { name } = await c.req.json()
|
|
46
|
-
|
|
40
|
+
c.var.session.user = { name }
|
|
47
41
|
await c.var.session.save()
|
|
48
42
|
return c.json({ ok: true })
|
|
49
43
|
})
|
|
50
44
|
|
|
45
|
+
// Everything below requireSession returns 401 if not logged in
|
|
46
|
+
app.use('/api/*', requireSession())
|
|
47
|
+
|
|
48
|
+
app.get('/api/profile', (c) => c.json(c.var.session.user))
|
|
49
|
+
|
|
51
50
|
app.post('/logout', (c) => {
|
|
52
51
|
c.var.session.destroy()
|
|
53
52
|
return c.json({ ok: true })
|
|
@@ -60,25 +59,22 @@ Run with `bun run file.ts` — Bun auto-starts the server.
|
|
|
60
59
|
|
|
61
60
|
```ts
|
|
62
61
|
import { Elysia } from 'elysia'
|
|
63
|
-
import { session } from 'peta-auth/elysia'
|
|
62
|
+
import { session, requireSession } from 'peta-auth/elysia'
|
|
64
63
|
|
|
65
64
|
new Elysia()
|
|
66
65
|
.use(session({
|
|
67
66
|
password: process.env.SESSION_SECRET!,
|
|
68
67
|
cookieName: 'my-session',
|
|
69
68
|
}))
|
|
70
|
-
.get('/profile', ({ session: s }) =>
|
|
71
|
-
!s.user ? Response.json({ error: 'unauthorized' }, { status: 401 })
|
|
72
|
-
: Response.json(s.user))
|
|
73
69
|
.post('/login', async ({ session: s, body }: any) => {
|
|
74
70
|
s.user = { name: body.name }
|
|
75
71
|
await s.save()
|
|
76
72
|
return Response.json({ ok: true })
|
|
77
73
|
})
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
})
|
|
74
|
+
.get('/public', () => Response.json({ message: 'public' }))
|
|
75
|
+
// Everything after requireSession is guarded
|
|
76
|
+
.use(requireSession())
|
|
77
|
+
.get('/profile', ({ session: s }) => Response.json(s.user))
|
|
82
78
|
.listen(3000)
|
|
83
79
|
```
|
|
84
80
|
|
|
@@ -86,14 +82,14 @@ new Elysia()
|
|
|
86
82
|
|
|
87
83
|
```ts
|
|
88
84
|
// server/api/profile.get.ts
|
|
89
|
-
import { useSession } from 'peta-auth/nuxt'
|
|
85
|
+
import { useSession, requireSession } from 'peta-auth/nuxt'
|
|
90
86
|
|
|
91
87
|
export default defineEventHandler(async (event) => {
|
|
92
88
|
const session = await useSession(event, {
|
|
93
89
|
password: process.env.NUXT_SESSION_PASSWORD!,
|
|
94
90
|
cookieName: 'nuxt-session',
|
|
95
91
|
})
|
|
96
|
-
|
|
92
|
+
requireSession(event, session)
|
|
97
93
|
return session.user
|
|
98
94
|
})
|
|
99
95
|
```
|
|
@@ -140,6 +136,33 @@ session({ password: { 1: 'old-pw', 2: 'new-pw' }, cookieName: 'my-session' })
|
|
|
140
136
|
// new cookies use key 2, old cookies still decrypt with key 1
|
|
141
137
|
```
|
|
142
138
|
|
|
139
|
+
### Typed sessions
|
|
140
|
+
|
|
141
|
+
Add a generic type parameter to get full IntelliSense on your session data:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Hono
|
|
145
|
+
app.use('*', session<{ user: { name: string }; views: number }>({ password, cookieName }))
|
|
146
|
+
// c.var.session.user.name → string
|
|
147
|
+
// c.var.session.views → number
|
|
148
|
+
|
|
149
|
+
// Elysia
|
|
150
|
+
app.use(session<{ user: { name: string } }>({ password, cookieName }))
|
|
151
|
+
|
|
152
|
+
// Nuxt
|
|
153
|
+
const session = await useSession<{ user: { name: string } }>(event, { password, cookieName })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Without a generic parameter, session data defaults to `Record<string, unknown>`.
|
|
157
|
+
|
|
158
|
+
### `requireSession()` guard
|
|
159
|
+
|
|
160
|
+
Returns 401 if the session has no user data. Works per-framework:
|
|
161
|
+
|
|
162
|
+
- **Hono**: `app.use('/protected/*', requireSession())` — middleware, path-patterned
|
|
163
|
+
- **Elysia**: `app.use(requireSession())` — guards all routes defined after it
|
|
164
|
+
- **Nuxt**: `requireSession(event, session)` — throws `createError({ statusCode: 401 })`
|
|
165
|
+
|
|
143
166
|
### Session object
|
|
144
167
|
|
|
145
168
|
```ts
|
|
@@ -151,14 +174,93 @@ interface IronSession {
|
|
|
151
174
|
}
|
|
152
175
|
```
|
|
153
176
|
|
|
154
|
-
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## JWT
|
|
180
|
+
|
|
181
|
+
Sign and verify HS256 JWTs using the same password infrastructure.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { signJWT, verifyJWT } from 'peta-auth/jwt'
|
|
185
|
+
|
|
186
|
+
// Sign
|
|
187
|
+
const token = await signJWT({ userId: 42, role: 'admin' }, {
|
|
188
|
+
password: process.env.JWT_SECRET!,
|
|
189
|
+
exp: 3600, // optional, seconds from now
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Verify
|
|
193
|
+
const payload = await verifyJWT<{ userId: number; role: string }>(token, {
|
|
194
|
+
password: process.env.JWT_SECRET!,
|
|
195
|
+
})
|
|
196
|
+
if (!payload) throw new Error('invalid or expired token')
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `exp` defaults to no expiry if omitted
|
|
200
|
+
- Supports password rotation (tries all keys on verify, signs with highest)
|
|
201
|
+
- Requires password at least 32 characters
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## CSRF protection
|
|
206
|
+
|
|
207
|
+
Generate and validate CSRF tokens stored in the session.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { generateCsrf, validateCsrf } from 'peta-auth/csrf'
|
|
211
|
+
|
|
212
|
+
// On a form page — generate token and store in session
|
|
213
|
+
const token = await generateCsrf(session)
|
|
214
|
+
await session.save()
|
|
215
|
+
// → render form with hidden field: <input name="_csrf" value="${token}" />
|
|
216
|
+
|
|
217
|
+
// On form submission — validate
|
|
218
|
+
if (!validateCsrf(session, body._csrf)) {
|
|
219
|
+
throw new Error('CSRF mismatch')
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- Uses `crypto.randomUUID()` for token generation
|
|
224
|
+
- Stores token in session under `_csrfToken` (configurable via `{ key: 'myKey' }`)
|
|
225
|
+
- You must call `session.save()` after `generateCsrf()`
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Password Reset
|
|
230
|
+
|
|
231
|
+
Helpers for forgot/reset password flows using short-lived JWTs.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { createPasswordResetToken, verifyPasswordResetToken, resetPassword } from 'peta-auth'
|
|
235
|
+
|
|
236
|
+
// Generate a token (e.g., in a "forgot password" endpoint)
|
|
237
|
+
const token = await createPasswordResetToken(user.email, {
|
|
238
|
+
password: process.env.SECRET!,
|
|
239
|
+
exp: 3600, // optional, default 1 hour
|
|
240
|
+
})
|
|
241
|
+
// → email token as a link: https://example.com/reset?token=${token}
|
|
242
|
+
|
|
243
|
+
// Verify a token (e.g., in a "reset password" endpoint)
|
|
244
|
+
const payload = await verifyPasswordResetToken(token, process.env.SECRET!)
|
|
245
|
+
if (!payload) throw new Error('Invalid or expired token')
|
|
246
|
+
// payload.userId → the email passed to createPasswordResetToken
|
|
247
|
+
|
|
248
|
+
// Combined: verify token + hash new password
|
|
249
|
+
const result = await resetPassword(token, newPassword, process.env.SECRET!)
|
|
250
|
+
if (!result) throw new Error('Invalid or expired token')
|
|
251
|
+
users.set(result.userId, { ...user, hash: result.hash })
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Low-level
|
|
155
257
|
|
|
156
258
|
```ts
|
|
157
259
|
import { createSessionFromAdapter, sealData, unsealData } from 'peta-auth'
|
|
158
260
|
import { hashPassword, verifyPassword } from 'peta-auth'
|
|
159
261
|
```
|
|
160
262
|
|
|
161
|
-
- **`createSessionFromAdapter(adapter, options)`** — takes a `SessionAdapter` (`{ getCookie, setCookie }`). Used internally by all framework adapters.
|
|
263
|
+
- **`createSessionFromAdapter<T>(adapter, options)`** — takes a `SessionAdapter` (`{ getCookie, setCookie }`). Used internally by all framework adapters.
|
|
162
264
|
- **`sealData(data, { password, ttl? })` / `unsealData<T>(seal, { password, ttl? })`** — encrypt/decrypt arbitrary data.
|
|
163
265
|
- **`hashPassword(password, { cost? })` / `verifyPassword(hash, password)`** — bcrypt hashing via `bcryptjs`. Default cost: 10.
|
|
164
266
|
|
|
@@ -186,9 +288,6 @@ const githubHandler = defineOAuthGitHubEventHandler({
|
|
|
186
288
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
187
289
|
},
|
|
188
290
|
async onSuccess({ user, tokens }) {
|
|
189
|
-
// The user is authenticated — redirect back to your app.
|
|
190
|
-
// To create a session, use the `request` parameter:
|
|
191
|
-
// onSuccess({ user, tokens, request })
|
|
192
291
|
return new Response(null, { status: 302, headers: { Location: '/' } })
|
|
193
292
|
},
|
|
194
293
|
})
|
|
@@ -223,15 +322,20 @@ async onSuccess({ user, tokens, request }) {
|
|
|
223
322
|
Full runnable examples in [`examples/`](./examples). All work with zero config (demo password fallback built in):
|
|
224
323
|
|
|
225
324
|
```bash
|
|
226
|
-
bun run examples/hono-basic.ts
|
|
227
|
-
bun run examples/
|
|
228
|
-
bun run examples/
|
|
229
|
-
bun run examples/
|
|
230
|
-
bun run examples/
|
|
231
|
-
bun run examples/
|
|
232
|
-
bun run examples/
|
|
233
|
-
bun run examples/
|
|
234
|
-
|
|
325
|
+
bun run examples/hono-basic.ts # Hono — session CRUD, views counter
|
|
326
|
+
bun run examples/hono-guard.ts # Hono — requireSession guard
|
|
327
|
+
bun run examples/elysia-basic.ts # Elysia — session CRUD, views counter
|
|
328
|
+
bun run examples/elysia-guard.ts # Elysia — requireSession guard
|
|
329
|
+
bun run examples/password-auth.ts # Hono — signup + login with bcrypt
|
|
330
|
+
bun run examples/password-reset.ts # Hono — forgot/reset password flow
|
|
331
|
+
bun run examples/jwt-basic.ts # JWT sign + verify + tamper detection
|
|
332
|
+
bun run examples/csrf-basic.ts # Hono — CSRF token form example
|
|
333
|
+
bun run examples/oauth-github.ts # Hono — GitHub OAuth
|
|
334
|
+
bun run examples/oauth-google.ts # Hono — Google OAuth (PKCE)
|
|
335
|
+
bun run examples/elysia-password.ts # Elysia — signup + login with bcrypt
|
|
336
|
+
bun run examples/elysia-oauth-github.ts # Elysia — GitHub OAuth
|
|
337
|
+
bun run examples/elysia-oauth-google.ts # Elysia — Google OAuth (PKCE)
|
|
338
|
+
cd examples/nuxt # Nuxt — server routes with useSession
|
|
235
339
|
```
|
|
236
340
|
|
|
237
341
|
---
|
|
@@ -250,7 +354,7 @@ Session data is serialized, encrypted with AES-256-CBC, integrity-protected with
|
|
|
250
354
|
## Scripts
|
|
251
355
|
|
|
252
356
|
```bash
|
|
253
|
-
bun test #
|
|
254
|
-
bun run build # tsdown → dist/ (
|
|
357
|
+
bun test # 65 tests across 12 files
|
|
358
|
+
bun run build # tsdown → dist/ (21 files, 30 kB)
|
|
255
359
|
bun run prepublish # build + publish
|
|
256
360
|
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/crypto.d.ts
|
|
2
|
+
type PasswordsMap = Record<string, string>;
|
|
3
|
+
type Password = PasswordsMap | string;
|
|
4
|
+
declare const sealData: (data: unknown, {
|
|
5
|
+
password,
|
|
6
|
+
ttl
|
|
7
|
+
}: {
|
|
8
|
+
password: Password;
|
|
9
|
+
ttl?: number;
|
|
10
|
+
}) => Promise<string>;
|
|
11
|
+
declare const unsealData: <T>(seal: string, {
|
|
12
|
+
password,
|
|
13
|
+
ttl
|
|
14
|
+
}: {
|
|
15
|
+
password: Password;
|
|
16
|
+
ttl?: number;
|
|
17
|
+
}) => Promise<T>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { sealData as n, unsealData as r, Password as t };
|
package/dist/csrf.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as IronSession } from "./session-z20gaFVT.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/csrf.d.ts
|
|
4
|
+
interface CSRFOptions {
|
|
5
|
+
key?: string;
|
|
6
|
+
}
|
|
7
|
+
declare function generateCsrf(session: IronSession, options?: CSRFOptions): Promise<string>;
|
|
8
|
+
declare function validateCsrf(session: IronSession, token: string, options?: CSRFOptions): boolean;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { CSRFOptions, generateCsrf, validateCsrf };
|
package/dist/csrf.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/csrf.ts
|
|
2
|
+
async function generateCsrf(session, options) {
|
|
3
|
+
const key = options?.key ?? "_csrfToken";
|
|
4
|
+
const token = crypto.randomUUID();
|
|
5
|
+
session[key] = token;
|
|
6
|
+
return token;
|
|
7
|
+
}
|
|
8
|
+
function validateCsrf(session, token, options) {
|
|
9
|
+
const stored = session[options?.key ?? "_csrfToken"];
|
|
10
|
+
return typeof stored === "string" && stored === token;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { generateCsrf, validateCsrf };
|
package/dist/elysia.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { r as SessionOptions, t as IronSession } from "./session-
|
|
1
|
+
import { r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
|
|
2
2
|
import { Elysia } from "elysia";
|
|
3
3
|
|
|
4
4
|
//#region src/elysia.d.ts
|
|
5
|
-
declare function session(options: SessionOptions): Elysia<"", {
|
|
5
|
+
declare function session<T extends Record<string, unknown> = Record<string, unknown>>(options: SessionOptions): Elysia<"", {
|
|
6
6
|
decorator: {};
|
|
7
7
|
store: {};
|
|
8
8
|
derive: {};
|
|
@@ -19,13 +19,13 @@ declare function session(options: SessionOptions): Elysia<"", {
|
|
|
19
19
|
response: {};
|
|
20
20
|
}, {}, {
|
|
21
21
|
derive: {
|
|
22
|
-
readonly session:
|
|
22
|
+
readonly session: T & IronSession;
|
|
23
23
|
};
|
|
24
24
|
resolve: {};
|
|
25
25
|
schema: {};
|
|
26
26
|
standaloneSchema: {};
|
|
27
27
|
response: import("elysia").ExtractErrorFromHandle<{
|
|
28
|
-
readonly session:
|
|
28
|
+
readonly session: T & IronSession;
|
|
29
29
|
}>;
|
|
30
30
|
}, {
|
|
31
31
|
derive: {};
|
|
@@ -34,5 +34,35 @@ declare function session(options: SessionOptions): Elysia<"", {
|
|
|
34
34
|
standaloneSchema: {};
|
|
35
35
|
response: {};
|
|
36
36
|
}>;
|
|
37
|
+
declare function requireSession(): (app: Elysia) => Elysia<"", {
|
|
38
|
+
decorator: {};
|
|
39
|
+
store: {};
|
|
40
|
+
derive: {};
|
|
41
|
+
resolve: {};
|
|
42
|
+
}, {
|
|
43
|
+
typebox: {};
|
|
44
|
+
error: {};
|
|
45
|
+
}, {
|
|
46
|
+
schema: {};
|
|
47
|
+
standaloneSchema: {};
|
|
48
|
+
macro: {};
|
|
49
|
+
macroFn: {};
|
|
50
|
+
parser: {};
|
|
51
|
+
response: {};
|
|
52
|
+
}, {}, {
|
|
53
|
+
derive: {};
|
|
54
|
+
resolve: {};
|
|
55
|
+
schema: {};
|
|
56
|
+
standaloneSchema: {};
|
|
57
|
+
response: {};
|
|
58
|
+
}, {
|
|
59
|
+
derive: {};
|
|
60
|
+
resolve: {};
|
|
61
|
+
schema: {};
|
|
62
|
+
standaloneSchema: {};
|
|
63
|
+
response: {
|
|
64
|
+
200: Response;
|
|
65
|
+
};
|
|
66
|
+
}>;
|
|
37
67
|
//#endregion
|
|
38
|
-
export { session };
|
|
68
|
+
export { requireSession, session };
|
package/dist/elysia.mjs
CHANGED
|
@@ -13,5 +13,14 @@ function session(options) {
|
|
|
13
13
|
}, options) };
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
function requireSession() {
|
|
17
|
+
return (app) => app.onBeforeHandle((context) => {
|
|
18
|
+
const session = context.session;
|
|
19
|
+
if (!Object.keys(session).some((k) => k !== "save" && k !== "destroy" && k !== "updateConfig")) return new Response(JSON.stringify({ error: "unauthorized" }), {
|
|
20
|
+
status: 401,
|
|
21
|
+
headers: { "Content-Type": "application/json" }
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
16
25
|
//#endregion
|
|
17
|
-
export { session };
|
|
26
|
+
export { requireSession, session };
|
package/dist/hono.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { r as SessionOptions, t as IronSession } from "./session-
|
|
1
|
+
import { r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
|
|
2
2
|
import { MiddlewareHandler } from "hono";
|
|
3
3
|
|
|
4
4
|
//#region src/hono.d.ts
|
|
5
|
-
declare
|
|
6
|
-
|
|
7
|
-
session: IronSession;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
declare function
|
|
5
|
+
declare function session<T extends Record<string, unknown> = Record<string, unknown>>(options: SessionOptions): MiddlewareHandler<{
|
|
6
|
+
Variables: {
|
|
7
|
+
session: T & IronSession;
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
declare function requireSession(): MiddlewareHandler;
|
|
11
11
|
//#endregion
|
|
12
|
-
export { session };
|
|
12
|
+
export { requireSession, session };
|
package/dist/hono.mjs
CHANGED
|
@@ -11,5 +11,12 @@ function session(options) {
|
|
|
11
11
|
await next();
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
function requireSession() {
|
|
15
|
+
return createMiddleware(async (c, next) => {
|
|
16
|
+
const s = c.var.session;
|
|
17
|
+
if (!Object.keys(s).some((k) => k !== "save" && k !== "destroy" && k !== "updateConfig")) return c.json({ error: "unauthorized" }, 401);
|
|
18
|
+
await next();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
14
21
|
//#endregion
|
|
15
|
-
export { session };
|
|
22
|
+
export { requireSession, session };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as sealData, r as unsealData, t as Password } from "./crypto-Ln_Mj_zp.mjs";
|
|
2
|
+
import { i as createSessionFromAdapter, n as SessionAdapter, r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
|
|
3
|
+
import { CSRFOptions, generateCsrf, validateCsrf } from "./csrf.mjs";
|
|
4
|
+
import { JWTOptions, signJWT, verifyJWT } from "./jwt.mjs";
|
|
2
5
|
|
|
3
6
|
//#region src/password.d.ts
|
|
4
7
|
interface HashOptions {
|
|
@@ -7,4 +10,18 @@ interface HashOptions {
|
|
|
7
10
|
declare function hashPassword(password: string, options?: HashOptions): Promise<string>;
|
|
8
11
|
declare function verifyPassword(hash: string, password: string): Promise<boolean>;
|
|
9
12
|
//#endregion
|
|
10
|
-
|
|
13
|
+
//#region src/reset-password.d.ts
|
|
14
|
+
interface PasswordResetOptions {
|
|
15
|
+
password: Password;
|
|
16
|
+
exp?: number;
|
|
17
|
+
}
|
|
18
|
+
declare function createPasswordResetToken(userId: string, options: PasswordResetOptions): Promise<string>;
|
|
19
|
+
declare function verifyPasswordResetToken(token: string, password: Password): Promise<{
|
|
20
|
+
userId: string;
|
|
21
|
+
} | null>;
|
|
22
|
+
declare function resetPassword(token: string, newPassword: string, password: Password): Promise<{
|
|
23
|
+
userId: string;
|
|
24
|
+
hash: string;
|
|
25
|
+
} | null>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { type CSRFOptions, type IronSession, type JWTOptions, type Password, type PasswordResetOptions, type SessionAdapter, type SessionOptions, createPasswordResetToken, createSessionFromAdapter, generateCsrf, hashPassword, resetPassword, sealData, signJWT, unsealData, validateCsrf, verifyJWT, verifyPassword, verifyPasswordResetToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { n as sealData, r as unsealData, t as createSessionFromAdapter } from "./session-DSwf3XPH.mjs";
|
|
2
|
+
import { generateCsrf, validateCsrf } from "./csrf.mjs";
|
|
3
|
+
import { signJWT, verifyJWT } from "./jwt.mjs";
|
|
2
4
|
import { compareSync, genSaltSync, hashSync } from "bcryptjs";
|
|
3
5
|
//#region src/password.ts
|
|
4
6
|
async function hashPassword(password, options = {}) {
|
|
@@ -8,4 +10,29 @@ async function verifyPassword(hash, password) {
|
|
|
8
10
|
return compareSync(password, hash);
|
|
9
11
|
}
|
|
10
12
|
//#endregion
|
|
11
|
-
|
|
13
|
+
//#region src/reset-password.ts
|
|
14
|
+
const DEFAULT_EXPIRY = 3600;
|
|
15
|
+
async function createPasswordResetToken(userId, options) {
|
|
16
|
+
return signJWT({
|
|
17
|
+
userId,
|
|
18
|
+
purpose: "password-reset"
|
|
19
|
+
}, {
|
|
20
|
+
password: options.password,
|
|
21
|
+
exp: options.exp ?? DEFAULT_EXPIRY
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async function verifyPasswordResetToken(token, password) {
|
|
25
|
+
const payload = await verifyJWT(token, { password });
|
|
26
|
+
if (!payload || payload.purpose !== "password-reset") return null;
|
|
27
|
+
return { userId: payload.userId };
|
|
28
|
+
}
|
|
29
|
+
async function resetPassword(token, newPassword, password) {
|
|
30
|
+
const payload = await verifyPasswordResetToken(token, password);
|
|
31
|
+
if (!payload) return null;
|
|
32
|
+
return {
|
|
33
|
+
userId: payload.userId,
|
|
34
|
+
hash: await hashPassword(newPassword)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { createPasswordResetToken, createSessionFromAdapter, generateCsrf, hashPassword, resetPassword, sealData, signJWT, unsealData, validateCsrf, verifyJWT, verifyPassword, verifyPasswordResetToken };
|
package/dist/jwt.d.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { t as Password } from "./crypto-Ln_Mj_zp.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/jwt.d.ts
|
|
4
|
+
interface JWTOptions {
|
|
5
|
+
password: Password;
|
|
6
|
+
exp?: number;
|
|
7
|
+
}
|
|
8
|
+
declare function signJWT(payload: Record<string, unknown>, options: JWTOptions): Promise<string>;
|
|
9
|
+
declare function verifyJWT<T = Record<string, unknown>>(token: string, options: JWTOptions): Promise<T | null>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { JWTOptions, signJWT, verifyJWT };
|
package/dist/jwt.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
//#region src/jwt.ts
|
|
3
|
+
function toPasswordMap(password) {
|
|
4
|
+
return typeof password === "string" ? { 1: password } : password;
|
|
5
|
+
}
|
|
6
|
+
function toKey(secret) {
|
|
7
|
+
return new TextEncoder().encode(secret);
|
|
8
|
+
}
|
|
9
|
+
async function signJWT(payload, options) {
|
|
10
|
+
const map = toPasswordMap(options.password);
|
|
11
|
+
const secret = map[Math.max(...Object.keys(map).map(Number)).toString()];
|
|
12
|
+
if (!secret || secret.length < 32) throw new Error("peta-auth/jwt: password must be at least 32 characters");
|
|
13
|
+
const jwt = new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
|
|
14
|
+
if (options.exp !== void 0) jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + options.exp);
|
|
15
|
+
return jwt.sign(toKey(secret));
|
|
16
|
+
}
|
|
17
|
+
async function verifyJWT(token, options) {
|
|
18
|
+
for (const secret of Object.values(toPasswordMap(options.password))) {
|
|
19
|
+
if (!secret) continue;
|
|
20
|
+
try {
|
|
21
|
+
const { payload } = await jose.jwtVerify(token, toKey(secret));
|
|
22
|
+
return payload;
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { signJWT, verifyJWT };
|
package/dist/nuxt.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { r as SessionOptions, t as IronSession } from "./session-
|
|
1
|
+
import { r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
|
|
2
2
|
import { H3Event } from "h3";
|
|
3
3
|
|
|
4
4
|
//#region src/nuxt.d.ts
|
|
5
|
-
declare function useSession(event: H3Event, options: SessionOptions): Promise<IronSession>;
|
|
5
|
+
declare function useSession<T extends Record<string, unknown> = Record<string, unknown>>(event: H3Event, options: SessionOptions): Promise<T & IronSession>;
|
|
6
|
+
declare function requireSession(_event: H3Event, session: IronSession): void;
|
|
6
7
|
//#endregion
|
|
7
|
-
export { useSession };
|
|
8
|
+
export { requireSession, useSession };
|
package/dist/nuxt.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as createSessionFromAdapter } from "./session-DSwf3XPH.mjs";
|
|
2
|
-
import { appendHeader, getCookie } from "h3";
|
|
2
|
+
import { appendHeader, createError, getCookie } from "h3";
|
|
3
3
|
//#region src/nuxt.ts
|
|
4
4
|
function useSession(event, options) {
|
|
5
5
|
const password = options.password ?? process.env.NUXT_SESSION_PASSWORD;
|
|
@@ -14,5 +14,11 @@ function useSession(event, options) {
|
|
|
14
14
|
cookieOptions: options?.cookieOptions
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
function requireSession(_event, session) {
|
|
18
|
+
if (!Object.keys(session).some((k) => k !== "save" && k !== "destroy" && k !== "updateConfig")) throw createError({
|
|
19
|
+
statusCode: 401,
|
|
20
|
+
statusMessage: "unauthorized"
|
|
21
|
+
});
|
|
22
|
+
}
|
|
17
23
|
//#endregion
|
|
18
|
-
export { useSession };
|
|
24
|
+
export { requireSession, useSession };
|
|
@@ -1,23 +1,6 @@
|
|
|
1
|
+
import { t as Password } from "./crypto-Ln_Mj_zp.mjs";
|
|
1
2
|
import { SerializeOptions } from "cookie";
|
|
2
3
|
|
|
3
|
-
//#region src/crypto.d.ts
|
|
4
|
-
type PasswordsMap = Record<string, string>;
|
|
5
|
-
type Password = PasswordsMap | string;
|
|
6
|
-
declare const sealData: (data: unknown, {
|
|
7
|
-
password,
|
|
8
|
-
ttl
|
|
9
|
-
}: {
|
|
10
|
-
password: Password;
|
|
11
|
-
ttl?: number;
|
|
12
|
-
}) => Promise<string>;
|
|
13
|
-
declare const unsealData: <T>(seal: string, {
|
|
14
|
-
password,
|
|
15
|
-
ttl
|
|
16
|
-
}: {
|
|
17
|
-
password: Password;
|
|
18
|
-
ttl?: number;
|
|
19
|
-
}) => Promise<T>;
|
|
20
|
-
//#endregion
|
|
21
4
|
//#region src/session.d.ts
|
|
22
5
|
interface SessionOptions {
|
|
23
6
|
password: Password;
|
|
@@ -37,4 +20,4 @@ interface SessionAdapter {
|
|
|
37
20
|
}
|
|
38
21
|
declare function createSessionFromAdapter<T extends Record<string, unknown> = Record<string, unknown>>(adapter: SessionAdapter, options: SessionOptions): Promise<T & IronSession>;
|
|
39
22
|
//#endregion
|
|
40
|
-
export {
|
|
23
|
+
export { createSessionFromAdapter as i, SessionAdapter as n, SessionOptions as r, IronSession as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peta-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Encrypted cookie sessions for Bun — Hono, ElysiaJS & Nuxt adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
"types": "./dist/nuxt.d.mts",
|
|
23
23
|
"import": "./dist/nuxt.mjs"
|
|
24
24
|
},
|
|
25
|
+
"./jwt": {
|
|
26
|
+
"types": "./dist/jwt.d.mts",
|
|
27
|
+
"import": "./dist/jwt.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./csrf": {
|
|
30
|
+
"types": "./dist/csrf.d.mts",
|
|
31
|
+
"import": "./dist/csrf.mjs"
|
|
32
|
+
},
|
|
25
33
|
"./oauth/github": {
|
|
26
34
|
"types": "./dist/oauth/github.d.mts",
|
|
27
35
|
"import": "./dist/oauth/github.mjs"
|
|
@@ -39,18 +47,20 @@
|
|
|
39
47
|
"lint": "biome check --write .",
|
|
40
48
|
"lint:ci": "biome ci .",
|
|
41
49
|
"prepublish": "bun run build",
|
|
42
|
-
"test": "bun test"
|
|
50
|
+
"test": "bun test",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
43
52
|
},
|
|
44
53
|
"dependencies": {
|
|
45
54
|
"bcryptjs": "^3.0.3",
|
|
46
55
|
"cookie": "^1.0.2",
|
|
47
|
-
"iron-webcrypto": "^1.2.1"
|
|
56
|
+
"iron-webcrypto": "^1.2.1",
|
|
57
|
+
"jose": "^6.2.3"
|
|
48
58
|
},
|
|
49
59
|
"peerDependencies": {
|
|
50
60
|
"elysia": ">=1.0.0",
|
|
51
61
|
"h3": ">=1.15.0",
|
|
52
62
|
"hono": ">=4.0.0",
|
|
53
|
-
"typescript": "^
|
|
63
|
+
"typescript": "^6.0.0"
|
|
54
64
|
},
|
|
55
65
|
"peerDependenciesMeta": {
|
|
56
66
|
"elysia": {
|
|
@@ -71,6 +81,6 @@
|
|
|
71
81
|
"h3": "latest",
|
|
72
82
|
"hono": "latest",
|
|
73
83
|
"tsdown": "latest",
|
|
74
|
-
"typescript": "^
|
|
84
|
+
"typescript": "^6.0.3"
|
|
75
85
|
}
|
|
76
86
|
}
|