peta-auth 0.2.0 → 0.2.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 +93 -185
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,186 +1,133 @@
|
|
|
1
1
|
# peta-auth
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/peta-auth)
|
|
4
|
+
[](https://www.typescriptlang.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Encrypted cookie sessions for Bun — with first-class adapters for **Hono**, **ElysiaJS**, and **Nuxt**.
|
|
4
8
|
|
|
5
9
|
Uses `iron-webcrypto` (AES-256-CBC + HMAC-SHA256) to seal session data into stateless, signed-and-encrypted cookies. No server-side storage needed.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Also provides JWT signing/verification, CSRF protection, password hashing (argon2), password reset flows, and OAuth (GitHub, Google).
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
14
|
bun add peta-auth
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
Install only the framework adapters you need:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
bun add hono # for peta-auth/hono
|
|
17
|
-
bun add elysia # for peta-auth/elysia
|
|
18
|
-
# Nuxt already includes h3 — just add peta-auth
|
|
19
|
-
```
|
|
20
|
-
|
|
21
17
|
---
|
|
22
18
|
|
|
23
|
-
## Quick
|
|
19
|
+
## Quick Start
|
|
24
20
|
|
|
25
21
|
### Hono
|
|
26
22
|
|
|
27
23
|
```ts
|
|
28
|
-
import { Hono } from
|
|
29
|
-
import { session, requireSession } from
|
|
24
|
+
import { Hono } from "hono"
|
|
25
|
+
import { session, requireSession } from "peta-auth/hono"
|
|
30
26
|
|
|
31
27
|
const app = new Hono()
|
|
32
28
|
|
|
33
|
-
app.use(
|
|
34
|
-
password: process.env.SESSION_SECRET!,
|
|
35
|
-
cookieName:
|
|
29
|
+
app.use("*", session({
|
|
30
|
+
password: process.env.SESSION_SECRET!, // at least 32 characters
|
|
31
|
+
cookieName: "my-session",
|
|
36
32
|
}))
|
|
37
33
|
|
|
38
|
-
app.post(
|
|
39
|
-
|
|
40
|
-
c.var.session.user = { name }
|
|
34
|
+
app.post("/login", async (c) => {
|
|
35
|
+
c.var.session.user = await c.req.json()
|
|
41
36
|
await c.var.session.save()
|
|
42
37
|
return c.json({ ok: true })
|
|
43
38
|
})
|
|
44
39
|
|
|
45
|
-
// Everything
|
|
46
|
-
app.use(
|
|
47
|
-
|
|
48
|
-
app.get('/api/profile', (c) => c.json(c.var.session.user))
|
|
49
|
-
|
|
50
|
-
app.post('/logout', (c) => {
|
|
51
|
-
c.var.session.destroy()
|
|
52
|
-
return c.json({ ok: true })
|
|
53
|
-
})
|
|
40
|
+
// Everything after requireSession returns 401 if not logged in
|
|
41
|
+
app.use("/api/*", requireSession())
|
|
42
|
+
app.get("/api/profile", (c) => c.json(c.var.session.user))
|
|
54
43
|
```
|
|
55
44
|
|
|
56
|
-
Run with `bun run file.ts` — Bun auto-starts the server.
|
|
57
|
-
|
|
58
45
|
### ElysiaJS
|
|
59
46
|
|
|
60
47
|
```ts
|
|
61
|
-
import { Elysia } from
|
|
62
|
-
import { session, requireSession } from
|
|
48
|
+
import { Elysia } from "elysia"
|
|
49
|
+
import { session, requireSession } from "peta-auth/elysia"
|
|
63
50
|
|
|
64
51
|
new Elysia()
|
|
65
|
-
.use(session({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}))
|
|
69
|
-
.post('/login', async ({ session: s, body }: any) => {
|
|
70
|
-
s.user = { name: body.name }
|
|
52
|
+
.use(session({ password: process.env.SESSION_SECRET!, cookieName: "my-session" }))
|
|
53
|
+
.post("/login", async ({ session: s, body }: any) => {
|
|
54
|
+
s.user = body
|
|
71
55
|
await s.save()
|
|
72
|
-
return
|
|
56
|
+
return { ok: true }
|
|
73
57
|
})
|
|
74
|
-
.get('/public', () => Response.json({ message: 'public' }))
|
|
75
|
-
// Everything after requireSession is guarded
|
|
76
58
|
.use(requireSession())
|
|
77
|
-
.get(
|
|
59
|
+
.get("/profile", ({ session: s }) => s.user)
|
|
78
60
|
.listen(3000)
|
|
79
61
|
```
|
|
80
62
|
|
|
81
63
|
### Nuxt
|
|
82
64
|
|
|
83
65
|
```ts
|
|
84
|
-
// server/api/
|
|
85
|
-
import { useSession, requireSession } from
|
|
66
|
+
// server/api/login.post.ts
|
|
67
|
+
import { useSession, requireSession } from "peta-auth/nuxt"
|
|
86
68
|
|
|
87
69
|
export default defineEventHandler(async (event) => {
|
|
88
70
|
const session = await useSession(event, {
|
|
89
71
|
password: process.env.NUXT_SESSION_PASSWORD!,
|
|
90
|
-
cookieName:
|
|
72
|
+
cookieName: "nuxt-session",
|
|
91
73
|
})
|
|
92
74
|
requireSession(event, session)
|
|
93
75
|
return session.user
|
|
94
76
|
})
|
|
95
77
|
```
|
|
96
78
|
|
|
97
|
-
```ts
|
|
98
|
-
// server/api/login.post.ts
|
|
99
|
-
import { useSession } from 'peta-auth/nuxt'
|
|
100
|
-
|
|
101
|
-
export default defineEventHandler(async (event) => {
|
|
102
|
-
const session = await useSession(event, {
|
|
103
|
-
password: process.env.NUXT_SESSION_PASSWORD!,
|
|
104
|
-
cookieName: 'nuxt-session',
|
|
105
|
-
})
|
|
106
|
-
const body = await readBody(event)
|
|
107
|
-
Object.assign(session, { user: body, loggedInAt: Date.now() })
|
|
108
|
-
await session.save()
|
|
109
|
-
return { ok: true }
|
|
110
|
-
})
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Set `NUXT_SESSION_PASSWORD` in your `.env`.
|
|
114
|
-
|
|
115
79
|
---
|
|
116
80
|
|
|
117
|
-
## API
|
|
81
|
+
## Session API
|
|
118
82
|
|
|
119
|
-
### `session
|
|
83
|
+
### `session(options)`
|
|
120
84
|
|
|
121
|
-
|
|
85
|
+
Reads the session from the incoming cookie and attaches it to the framework's context.
|
|
122
86
|
|
|
123
87
|
```ts
|
|
124
88
|
session({
|
|
125
|
-
password: process.env.SESSION_SECRET!,
|
|
126
|
-
cookieName:
|
|
127
|
-
ttl: 60 * 60 * 24 * 14,
|
|
128
|
-
cookieOptions: { httpOnly: true, secure: true, sameSite:
|
|
89
|
+
password: process.env.SESSION_SECRET!, // required, >= 32 chars
|
|
90
|
+
cookieName: "my-session", // required
|
|
91
|
+
ttl: 60 * 60 * 24 * 14, // optional, default 14 days
|
|
92
|
+
cookieOptions: { httpOnly: true, secure: true, sameSite: "lax", path: "/" },
|
|
129
93
|
})
|
|
130
94
|
```
|
|
131
95
|
|
|
132
|
-
Password rotation
|
|
96
|
+
#### Password rotation
|
|
133
97
|
|
|
134
98
|
```ts
|
|
135
|
-
session({ password: { 1:
|
|
136
|
-
//
|
|
99
|
+
session({ password: { 1: "old-pw", 2: "new-pw" }, cookieName: "my-session" })
|
|
100
|
+
// New cookies use key 2, old cookies still decrypt with key 1
|
|
137
101
|
```
|
|
138
102
|
|
|
139
103
|
### Typed sessions
|
|
140
104
|
|
|
141
|
-
Add a generic type parameter to get full IntelliSense on your session data:
|
|
142
|
-
|
|
143
105
|
```ts
|
|
144
106
|
// Hono
|
|
145
|
-
app.use(
|
|
107
|
+
app.use("*", session<{ user: { name: string }; views: number }>({ password, cookieName }))
|
|
146
108
|
// c.var.session.user.name → string
|
|
147
|
-
// c.var.session.views → number
|
|
148
109
|
|
|
149
110
|
// Elysia
|
|
150
111
|
app.use(session<{ user: { name: string } }>({ password, cookieName }))
|
|
151
|
-
|
|
152
|
-
// Nuxt
|
|
153
|
-
const session = await useSession<{ user: { name: string } }>(event, { password, cookieName })
|
|
154
112
|
```
|
|
155
113
|
|
|
156
|
-
Without a generic parameter, session data defaults to `Record<string, unknown>`.
|
|
157
|
-
|
|
158
114
|
### `requireSession()` guard
|
|
159
115
|
|
|
160
|
-
Returns 401 if the session has no
|
|
116
|
+
Returns 401 if the session has no data. Optionally checks a specific key:
|
|
161
117
|
|
|
162
118
|
```ts
|
|
163
|
-
//
|
|
164
|
-
app.use(
|
|
165
|
-
|
|
166
|
-
// Guard on a specific key (e.g. session.userId must be truthy)
|
|
167
|
-
app.use('/admin/*', requireSession('userId'))
|
|
119
|
+
app.use("/api/*", requireSession()) // any session data
|
|
120
|
+
app.use("/admin/*", requireSession("role")) // session.role must be truthy
|
|
168
121
|
```
|
|
169
122
|
|
|
170
|
-
Works per-framework:
|
|
171
|
-
|
|
172
|
-
- **Hono**: `app.use('/protected/*', requireSession())` — path-patterned middleware
|
|
173
|
-
- **Elysia**: `app.use(requireSession())` — guards all routes defined after it
|
|
174
|
-
- **Nuxt**: `requireSession(event, session)` or `requireSession(event, session, 'userId')` — throws `createError({ statusCode: 401 })`
|
|
175
|
-
|
|
176
123
|
### Session object
|
|
177
124
|
|
|
178
125
|
```ts
|
|
179
126
|
interface IronSession {
|
|
180
|
-
save(): Promise<void>
|
|
181
|
-
destroy(): void
|
|
182
|
-
updateConfig(opts): void
|
|
183
|
-
[key: string]: unknown
|
|
127
|
+
save(): Promise<void> // seal & write cookie
|
|
128
|
+
destroy(): void // clear data & expire cookie
|
|
129
|
+
updateConfig(opts): void // change options for this request
|
|
130
|
+
[key: string]: unknown // your data
|
|
184
131
|
}
|
|
185
132
|
```
|
|
186
133
|
|
|
@@ -191,106 +138,66 @@ interface IronSession {
|
|
|
191
138
|
Sign and verify HS256 JWTs using the same password infrastructure.
|
|
192
139
|
|
|
193
140
|
```ts
|
|
194
|
-
import { signJWT, verifyJWT } from
|
|
141
|
+
import { signJWT, verifyJWT } from "peta-auth/jwt"
|
|
195
142
|
|
|
196
|
-
|
|
197
|
-
const token = await signJWT({ userId: 42, role: 'admin' }, {
|
|
143
|
+
const token = await signJWT({ userId: 42, role: "admin" }, {
|
|
198
144
|
password: process.env.JWT_SECRET!,
|
|
199
|
-
exp: 3600,
|
|
145
|
+
exp: 3600, // optional, seconds from now
|
|
200
146
|
})
|
|
201
147
|
|
|
202
|
-
// Verify
|
|
203
148
|
const payload = await verifyJWT<{ userId: number; role: string }>(token, {
|
|
204
149
|
password: process.env.JWT_SECRET!,
|
|
205
150
|
})
|
|
206
|
-
if (!payload) throw new Error(
|
|
151
|
+
if (!payload) throw new Error("invalid or expired token")
|
|
207
152
|
```
|
|
208
153
|
|
|
209
|
-
- `exp` defaults to no expiry if omitted
|
|
210
154
|
- Supports password rotation (tries all keys on verify, signs with highest)
|
|
211
155
|
- Requires password at least 32 characters
|
|
212
156
|
|
|
213
157
|
---
|
|
214
158
|
|
|
215
|
-
## CSRF
|
|
216
|
-
|
|
217
|
-
Generate and validate CSRF tokens stored in the session.
|
|
159
|
+
## CSRF Protection
|
|
218
160
|
|
|
219
161
|
```ts
|
|
220
|
-
import { generateCsrf, validateCsrf } from
|
|
162
|
+
import { generateCsrf, validateCsrf } from "peta-auth/csrf"
|
|
221
163
|
|
|
222
|
-
//
|
|
164
|
+
// Generate token on form page
|
|
223
165
|
const token = await generateCsrf(session)
|
|
224
166
|
await session.save()
|
|
225
|
-
// → render form with hidden field: <input name="_csrf" value="${token}" />
|
|
226
167
|
|
|
227
|
-
//
|
|
168
|
+
// Validate on form submission
|
|
228
169
|
if (!validateCsrf(session, body._csrf)) {
|
|
229
|
-
throw new Error(
|
|
170
|
+
throw new Error("CSRF mismatch")
|
|
230
171
|
}
|
|
231
172
|
```
|
|
232
173
|
|
|
233
|
-
- Uses `crypto.randomUUID()` for token generation
|
|
234
|
-
- Stores token in session under `_csrfToken` (configurable via `{ key: 'myKey' }`)
|
|
235
|
-
- You must call `session.save()` after `generateCsrf()`
|
|
236
|
-
|
|
237
174
|
---
|
|
238
175
|
|
|
239
176
|
## Password Reset
|
|
240
177
|
|
|
241
|
-
Helpers for forgot/reset password flows using short-lived JWTs.
|
|
242
|
-
|
|
243
178
|
```ts
|
|
244
|
-
import { createPasswordResetToken, verifyPasswordResetToken, resetPassword } from
|
|
179
|
+
import { createPasswordResetToken, verifyPasswordResetToken, resetPassword } from "peta-auth"
|
|
245
180
|
|
|
246
181
|
// Generate a token (e.g., in a "forgot password" endpoint)
|
|
247
182
|
const token = await createPasswordResetToken(user.email, {
|
|
248
183
|
password: process.env.SECRET!,
|
|
249
|
-
exp: 3600,
|
|
184
|
+
exp: 3600,
|
|
250
185
|
})
|
|
251
|
-
// → email token as a link: https://example.com/reset?token=${token}
|
|
252
|
-
|
|
253
|
-
// Verify a token (e.g., in a "reset password" endpoint)
|
|
254
|
-
const payload = await verifyPasswordResetToken(token, process.env.SECRET!)
|
|
255
|
-
if (!payload) throw new Error('Invalid or expired token')
|
|
256
|
-
// payload.userId → the email passed to createPasswordResetToken
|
|
257
186
|
|
|
258
|
-
//
|
|
187
|
+
// Verify token + hash new password
|
|
259
188
|
const result = await resetPassword(token, newPassword, process.env.SECRET!)
|
|
260
|
-
if (!result) throw new Error(
|
|
261
|
-
users.set(result.userId, { ...user, hash: result.hash })
|
|
189
|
+
if (!result) throw new Error("Invalid or expired token")
|
|
262
190
|
```
|
|
263
191
|
|
|
264
192
|
---
|
|
265
193
|
|
|
266
|
-
## Low-level
|
|
267
|
-
|
|
268
|
-
```ts
|
|
269
|
-
import { createSessionFromAdapter, sealData, unsealData } from 'peta-auth'
|
|
270
|
-
import { hashPassword, verifyPassword } from 'peta-auth'
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
- **`createSessionFromAdapter<T>(adapter, options)`** — takes a `SessionAdapter` (`{ getCookie, setCookie }`). Used internally by all framework adapters.
|
|
274
|
-
- **`sealData(data, { password, ttl? })` / `unsealData<T>(seal, { password, ttl? })`** — encrypt/decrypt arbitrary data.
|
|
275
|
-
- **`hashPassword(password, { cost? })` / `verifyPassword(hash, password)`** — bcrypt hashing via `bcryptjs`. Default cost: 10.
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
194
|
## OAuth
|
|
280
195
|
|
|
281
|
-
|
|
282
|
-
import { defineOAuthGitHubEventHandler } from 'peta-auth/oauth/github'
|
|
283
|
-
import { defineOAuthGoogleEventHandler } from 'peta-auth/oauth/google'
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Each returns a `(request: Request) => Promise<Response>` handler — framework-agnostic.
|
|
196
|
+
Framework-agnostic handlers for GitHub and Google:
|
|
287
197
|
|
|
288
198
|
```ts
|
|
289
|
-
import { defineOAuthGitHubEventHandler } from
|
|
290
|
-
import {
|
|
291
|
-
|
|
292
|
-
const app = new Hono()
|
|
293
|
-
app.use('*', session({ password: process.env.SESSION_SECRET!, cookieName: 'my-session' }))
|
|
199
|
+
import { defineOAuthGitHubEventHandler } from "peta-auth/oauth/github"
|
|
200
|
+
import { defineOAuthGoogleEventHandler } from "peta-auth/oauth/google"
|
|
294
201
|
|
|
295
202
|
const githubHandler = defineOAuthGitHubEventHandler({
|
|
296
203
|
config: {
|
|
@@ -298,54 +205,58 @@ const githubHandler = defineOAuthGitHubEventHandler({
|
|
|
298
205
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
299
206
|
},
|
|
300
207
|
async onSuccess({ user, tokens }) {
|
|
301
|
-
return new Response(null, { status: 302, headers: { Location:
|
|
208
|
+
return new Response(null, { status: 302, headers: { Location: "/" } })
|
|
302
209
|
},
|
|
303
210
|
})
|
|
304
211
|
|
|
305
|
-
app.get(
|
|
212
|
+
app.get("/auth/github", async (c) => githubHandler(c.req.raw))
|
|
306
213
|
```
|
|
307
214
|
|
|
308
215
|
Requires env vars: `PETA_OAUTH_GITHUB_CLIENT_ID`, `PETA_OAUTH_GITHUB_CLIENT_SECRET` (or `PETA_OAUTH_GOOGLE_*`).
|
|
309
216
|
|
|
310
|
-
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Password Hashing
|
|
311
220
|
|
|
312
221
|
```ts
|
|
313
|
-
import {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const res = new Response(null, { status: 302, headers: { Location: '/' } })
|
|
318
|
-
const session = await createSessionFromAdapter({
|
|
319
|
-
getCookie: (name) => parse(request.headers.get('cookie') ?? '')[name],
|
|
320
|
-
setCookie: (v) => res.headers.append('Set-Cookie', v),
|
|
321
|
-
}, { password: process.env.SESSION_SECRET!, cookieName: 'my-session' })
|
|
322
|
-
session.user = { id: user.id, login: user.login }
|
|
323
|
-
await session.save()
|
|
324
|
-
return res
|
|
325
|
-
}
|
|
222
|
+
import { hashPassword, verifyPassword } from "peta-auth"
|
|
223
|
+
|
|
224
|
+
const hash = await hashPassword("my-password", { memoryCost: 19456 })
|
|
225
|
+
const match = await verifyPassword(hash, "my-password")
|
|
326
226
|
```
|
|
327
227
|
|
|
228
|
+
Uses argon2id via `@node-rs/argon2`.
|
|
229
|
+
|
|
328
230
|
---
|
|
329
231
|
|
|
330
|
-
##
|
|
232
|
+
## Low-level API
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { createSessionFromAdapter, sealData, unsealData } from "peta-auth"
|
|
331
236
|
|
|
332
|
-
|
|
237
|
+
// Create a session from any cookie adapter
|
|
238
|
+
const session = await createSessionFromAdapter({ getCookie, setCookie }, options)
|
|
239
|
+
|
|
240
|
+
// Encrypt/decrypt arbitrary data
|
|
241
|
+
const sealed = await sealData({ foo: "bar" }, { password })
|
|
242
|
+
const data = await unsealData(sealed, { password })
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Examples
|
|
333
248
|
|
|
334
249
|
```bash
|
|
335
250
|
bun run examples/hono-basic.ts # Hono — session CRUD, views counter
|
|
336
251
|
bun run examples/hono-guard.ts # Hono — requireSession guard
|
|
337
|
-
bun run examples/elysia-basic.ts # Elysia — session CRUD
|
|
252
|
+
bun run examples/elysia-basic.ts # Elysia — session CRUD
|
|
338
253
|
bun run examples/elysia-guard.ts # Elysia — requireSession guard
|
|
339
|
-
bun run examples/password-auth.ts # Hono — signup + login
|
|
254
|
+
bun run examples/password-auth.ts # Hono — signup + login
|
|
340
255
|
bun run examples/password-reset.ts # Hono — forgot/reset password flow
|
|
341
|
-
bun run examples/jwt-basic.ts # JWT sign + verify
|
|
342
|
-
bun run examples/csrf-basic.ts #
|
|
256
|
+
bun run examples/jwt-basic.ts # JWT sign + verify
|
|
257
|
+
bun run examples/csrf-basic.ts # CSRF token example
|
|
343
258
|
bun run examples/oauth-github.ts # Hono — GitHub OAuth
|
|
344
259
|
bun run examples/oauth-google.ts # Hono — Google OAuth (PKCE)
|
|
345
|
-
bun run examples/elysia-password.ts # Elysia — signup + login with bcrypt
|
|
346
|
-
bun run examples/elysia-oauth-github.ts # Elysia — GitHub OAuth
|
|
347
|
-
bun run examples/elysia-oauth-google.ts # Elysia — Google OAuth (PKCE)
|
|
348
|
-
cd examples/nuxt # Nuxt — server routes with useSession
|
|
349
260
|
```
|
|
350
261
|
|
|
351
262
|
---
|
|
@@ -361,10 +272,7 @@ Session data is serialized, encrypted with AES-256-CBC, integrity-protected with
|
|
|
361
272
|
|
|
362
273
|
---
|
|
363
274
|
|
|
364
|
-
##
|
|
275
|
+
## Related packages
|
|
365
276
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
bun run build # tsdown → dist/ (21 files, 30 kB)
|
|
369
|
-
bun run prepublish # build + publish
|
|
370
|
-
```
|
|
277
|
+
- [peta-orm](../orm) — ORM with models, relations, hooks, soft deletes
|
|
278
|
+
- [peta-docs](../docs) — OpenAPI 3.1 spec generation + Scalar UI
|