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.
Files changed (2) hide show
  1. package/README.md +93 -185
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,186 +1,133 @@
1
1
  # peta-auth
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/peta-auth?style=flat-square)](https://www.npmjs.com/package/peta-auth)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
5
+ [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](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
- Inspired by [iron-session](https://github.com/vvo/iron-session) and [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils).
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 start
19
+ ## Quick Start
24
20
 
25
21
  ### Hono
26
22
 
27
23
  ```ts
28
- import { Hono } from 'hono'
29
- import { session, requireSession } from 'peta-auth/hono'
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('*', session({
34
- password: process.env.SESSION_SECRET!,
35
- cookieName: 'my-session',
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('/login', async (c) => {
39
- const { name } = await c.req.json()
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 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
-
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 'elysia'
62
- import { session, requireSession } from 'peta-auth/elysia'
48
+ import { Elysia } from "elysia"
49
+ import { session, requireSession } from "peta-auth/elysia"
63
50
 
64
51
  new Elysia()
65
- .use(session({
66
- password: process.env.SESSION_SECRET!,
67
- cookieName: 'my-session',
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 Response.json({ ok: true })
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('/profile', ({ session: s }) => Response.json(s.user))
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/profile.get.ts
85
- import { useSession, requireSession } from 'peta-auth/nuxt'
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: 'nuxt-session',
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` middleware (framework adapters)
83
+ ### `session(options)`
120
84
 
121
- Each adapter exports a `session(options)` function that reads the session from the incoming cookie and attaches it to the framework's context.
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!, // required, at least 32 chars
126
- cookieName: 'my-session', // required
127
- ttl: 60 * 60 * 24 * 14, // optional, default 14 days
128
- cookieOptions: { httpOnly: true, secure: true, sameSite: 'lax', path: '/' },
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 is supported via object syntax:
96
+ #### Password rotation
133
97
 
134
98
  ```ts
135
- session({ password: { 1: 'old-pw', 2: 'new-pw' }, cookieName: 'my-session' })
136
- // new cookies use key 2, old cookies still decrypt with key 1
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('*', session<{ user: { name: string }; views: number }>({ password, cookieName }))
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 user data. Optionally checks a specific session key:
116
+ Returns 401 if the session has no data. Optionally checks a specific key:
161
117
 
162
118
  ```ts
163
- // Guard on any session data
164
- app.use('/api/*', requireSession())
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> // seal & write the cookie
181
- destroy(): void // clear data & expire the cookie
182
- updateConfig(opts): void // change options for this request
183
- [key: string]: unknown // your data
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 'peta-auth/jwt'
141
+ import { signJWT, verifyJWT } from "peta-auth/jwt"
195
142
 
196
- // Sign
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, // optional, seconds from now
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('invalid or expired token')
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 protection
216
-
217
- Generate and validate CSRF tokens stored in the session.
159
+ ## CSRF Protection
218
160
 
219
161
  ```ts
220
- import { generateCsrf, validateCsrf } from 'peta-auth/csrf'
162
+ import { generateCsrf, validateCsrf } from "peta-auth/csrf"
221
163
 
222
- // On a form page — generate token and store in session
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
- // On form submission — validate
168
+ // Validate on form submission
228
169
  if (!validateCsrf(session, body._csrf)) {
229
- throw new Error('CSRF mismatch')
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 'peta-auth'
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, // optional, default 1 hour
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
- // Combined: verify token + hash new password
187
+ // Verify token + hash new password
259
188
  const result = await resetPassword(token, newPassword, process.env.SECRET!)
260
- if (!result) throw new Error('Invalid or expired token')
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
- ```ts
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 'peta-auth/oauth/github'
290
- import { session } from 'peta-auth/hono'
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('/auth/github', async (c) => githubHandler(c.req.raw))
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
- The `onSuccess` callback receives `request: Request` — create a session from the raw request inside the handler:
217
+ ---
218
+
219
+ ## Password Hashing
311
220
 
312
221
  ```ts
313
- import { createSessionFromAdapter } from 'peta-auth'
314
- import { parse } from 'cookie'
315
-
316
- async onSuccess({ user, tokens, request }) {
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
- ## Examples
232
+ ## Low-level API
233
+
234
+ ```ts
235
+ import { createSessionFromAdapter, sealData, unsealData } from "peta-auth"
331
236
 
332
- Full runnable examples in [`examples/`](./examples). All work with zero config (demo password fallback built in):
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, views counter
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 with bcrypt
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 + tamper detection
342
- bun run examples/csrf-basic.ts # Hono — CSRF token form example
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
- ## Scripts
275
+ ## Related packages
365
276
 
366
- ```bash
367
- bun test # 74 tests across 12 files
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peta-auth",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Encrypted cookie sessions for Bun — Hono, ElysiaJS & Nuxt adapters",
5
5
  "license": "MIT",
6
6
  "repository": {