@davaux/session 0.8.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/CLAUDE.md ADDED
@@ -0,0 +1,95 @@
1
+ <!-- pka-generated -->
2
+ # @davaux/session
3
+
4
+ > Generated by Project Knowledge Analyzer on 2026-06-06T21:53:10.444Z
5
+
6
+ ## Overview
7
+
8
+ HMAC-signed cookie sessions for Davaux
9
+
10
+ **Version**: 0.8.0
11
+ **Author**: David L Dyess II
12
+ **License**: MIT
13
+ **Repository**: https://codeberg.org/davaux/davaux#readme
14
+
15
+ ## Tech Stack
16
+
17
+ - **Language**: TypeScript
18
+ - **Module System**: ESM (`type: module`)
19
+
20
+ ## Commands
21
+
22
+ - `npm run build` — tsc
23
+ - `npm run typecheck` — tsc --noEmit
24
+
25
+ ## Project Structure
26
+
27
+ ```
28
+ ├── CLAUDE.md
29
+ ├── README.md
30
+ ├── package.json
31
+ ├── tsconfig.json
32
+ └── src/
33
+ └── index.ts
34
+
35
+ ```
36
+
37
+ ## Entry Points
38
+
39
+ - `src/index.ts`
40
+
41
+ ## Files by Type
42
+
43
+ ### Documentation (2)
44
+ - `CLAUDE.md`
45
+ - `README.md`
46
+
47
+ ### Config (2)
48
+ - `package.json`
49
+ - `tsconfig.json`
50
+
51
+ ### Module (1)
52
+ - `src/index.ts`
53
+
54
+
55
+ ## Git
56
+ - **Branch**: main
57
+ - **Last Commit**: chore: Add alpha status note to README
58
+ - **Author**: David Dyess II
59
+ - **Date**: 2026-06-06 15:52:27 -0600
60
+ - **Remote**: https://codeberg.org/davaux/davaux.git
61
+
62
+ ### Recent Commits
63
+ ```
64
+ f527031 chore: Add alpha status note to README
65
+ 90c819e chore: Add repo info to package.json files
66
+ b200d9d feat(davaux)!: Add OmlCacheConfig - opt-in with includes option or opt-out with excludes option; Fix OML implementation to follow OML spec - use output instead of return
67
+ 3bce0c2 chore: Update and add READMEs to packages; update ROADMAP
68
+ fc27c20 fix(davaux): Remove old dist folder on new builds; fix server port per DavauxConfig
69
+ c760659 feat(davaux): Add minify CSS in production builds
70
+ c899ab8 fix(davaux): Added method JS and JSX extenstion to scanner
71
+ 7d99f04 feat(davaux): Add support for declarative partial updates
72
+ 3b47f37 chore: Bump package versions to 0.8.0
73
+ aa0460f chore: Add CHANGELOG.md
74
+ ```
75
+
76
+ ## Dependencies
77
+
78
+
79
+ ### Development
80
+ @types/node, davaux, typescript
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+ ## Exported Symbols
91
+
92
+ **`src/index.ts`**
93
+ `Session`, `sessionMiddleware`, `SessionOptions`
94
+
95
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David L Dyess II
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @davaux/session
2
+
3
+ HMAC-signed cookie sessions for Davaux.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @davaux/session
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Add the middleware to your `davaux.config.ts`. The `secret` must be at least 32 characters — use an environment variable in production:
14
+
15
+ ```ts
16
+ // davaux.config.ts
17
+ import { defineConfig } from 'davaux/config'
18
+ import { sessionMiddleware } from '@davaux/session'
19
+
20
+ export default defineConfig({
21
+ middleware: [
22
+ sessionMiddleware({ secret: process.env.SESSION_SECRET! }),
23
+ ],
24
+ })
25
+ ```
26
+
27
+ ## Reading and writing session data
28
+
29
+ `ctx.state.session` is available in every handler, layout, and middleware after setup:
30
+
31
+ ```ts
32
+ // src/routes/login.page.tsx
33
+ import { definePage, redirect } from 'davaux'
34
+
35
+ export default definePage(async (ctx) => {
36
+ if (ctx.req.method === 'POST') {
37
+ const form = await ctx.formData()
38
+ ctx.state.session.set('userId', form.get('userId'))
39
+ redirect('/dashboard')
40
+ }
41
+
42
+ return <form method='post'>...</form>
43
+ })
44
+ ```
45
+
46
+ ```ts
47
+ // src/routes/dashboard.page.tsx
48
+ import { definePage, redirect } from 'davaux'
49
+
50
+ export default definePage((ctx) => {
51
+ const userId = ctx.state.session.get('userId')
52
+ if (!userId) redirect('/login')
53
+
54
+ return <h1>Welcome, {userId}</h1>
55
+ })
56
+ ```
57
+
58
+ ## Session API
59
+
60
+ `ctx.state.session` exposes:
61
+
62
+ | Method | Description |
63
+ |---|---|
64
+ | `get(key)` | Return the value for `key`, or `undefined` |
65
+ | `set(key, value)` | Store a value (must be JSON-serializable) |
66
+ | `delete(key)` | Remove a key |
67
+ | `clear()` | Wipe the entire session |
68
+ | `destroy()` | Clear the session and expire the cookie |
69
+
70
+ ## Options
71
+
72
+ | Option | Type | Default | Description |
73
+ |---|---|---|---|
74
+ | `secret` | `string` | — | HMAC signing secret (required, min 32 chars) |
75
+ | `cookieName` | `string` | `'session'` | Name of the session cookie |
76
+ | `maxAge` | `number` | `86400` | Cookie lifetime in seconds |
77
+ | `httpOnly` | `boolean` | `true` | Set the `HttpOnly` flag |
78
+ | `secure` | `boolean` | `false` | Set the `Secure` flag (enable in production) |
79
+ | `sameSite` | `'strict' \| 'lax' \| 'none'` | `'lax'` | `SameSite` attribute |
80
+ | `path` | `string` | `'/'` | Cookie path |
81
+
82
+ ## TypeScript
83
+
84
+ `ctx.state.session` is typed automatically when you import `@davaux/session`. If you need the `Session` type directly:
85
+
86
+ ```ts
87
+ import type { Session } from '@davaux/session'
88
+ ```
89
+
90
+ ## Notes
91
+
92
+ - Sessions are stored entirely in a signed cookie — no server-side store required
93
+ - The cookie payload is Base64-encoded JSON; it is signed but not encrypted — do not store secrets in the session
94
+ - Browsers enforce a ~4 KB per-cookie limit; keep session data minimal (user IDs, flags) and load larger data from your database in the handler
95
+
96
+ ## Documentation
97
+
98
+ [davaux.codeberg.page/docs/session](https://davaux.codeberg.page/docs/session)
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@davaux/session",
3
+ "version": "0.8.0",
4
+ "description": "HMAC-signed cookie sessions for Davaux",
5
+ "type": "module",
6
+ "author": "David L Dyess II",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://codeberg.org/davaux/davaux"
11
+ },
12
+ "bugs": {
13
+ "url": "https://codeberg.org/davaux/davaux/issues"
14
+ },
15
+ "homepage": "https://codeberg.org/davaux/davaux#readme",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "peerDependencies": {
27
+ "davaux": ">=0.8.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^25.0.0",
31
+ "davaux": "*",
32
+ "typescript": "^6.0.3"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto'
2
+ import type { ServerResponse } from 'node:http'
3
+ import type { MiddlewareFn } from 'davaux'
4
+
5
+ // ─── State augmentation ───────────────────────────────────────────────────────
6
+
7
+ declare module 'davaux' {
8
+ interface State {
9
+ session: Session
10
+ }
11
+ }
12
+
13
+ // ─── Session ──────────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Per-request session object. Stores arbitrary key-value data serialised into
17
+ * an HMAC-signed cookie. Mutations (`set`, `delete`, `destroy`) are persisted
18
+ * automatically before the response headers are sent.
19
+ */
20
+ export class Session {
21
+ private readonly _data: Record<string, unknown>
22
+ private _dirty = false
23
+ private _destroyed = false
24
+
25
+ constructor(data: Record<string, unknown> = {}) {
26
+ this._data = data
27
+ }
28
+
29
+ /** Retrieve a session value by key. Returns `undefined` when absent. */
30
+ get<T = unknown>(key: string): T | undefined {
31
+ return this._data[key] as T | undefined
32
+ }
33
+
34
+ /** Store a value under `key`. Marks the session dirty so it is persisted. */
35
+ set(key: string, value: unknown): void {
36
+ this._data[key] = value
37
+ this._dirty = true
38
+ }
39
+
40
+ /** Remove `key` from the session. Marks the session dirty so it is persisted. */
41
+ delete(key: string): void {
42
+ delete this._data[key]
43
+ this._dirty = true
44
+ }
45
+
46
+ /** Clear all session data and delete the cookie on response. */
47
+ destroy(): void {
48
+ this._destroyed = true
49
+ this._dirty = false
50
+ }
51
+
52
+ /** Snapshot of current session data. */
53
+ get data(): Record<string, unknown> {
54
+ return { ...this._data }
55
+ }
56
+
57
+ /** @internal */
58
+ get dirty(): boolean {
59
+ return this._dirty
60
+ }
61
+
62
+ /** @internal */
63
+ get destroyed(): boolean {
64
+ return this._destroyed
65
+ }
66
+ }
67
+
68
+ // ─── Options ──────────────────────────────────────────────────────────────────
69
+
70
+ export interface SessionOptions {
71
+ /**
72
+ * Secret(s) used to sign the session cookie with HMAC-SHA256.
73
+ * Pass an array for rotation: the first secret signs new sessions,
74
+ * all are accepted for verification.
75
+ */
76
+ secret: string | string[]
77
+ /** Cookie name. Default: `'session'` */
78
+ cookieName?: string
79
+ /** Max-Age in seconds. Omit for a session cookie (clears on browser close). */
80
+ maxAge?: number
81
+ /** Default: `true` */
82
+ httpOnly?: boolean
83
+ /** Default: `false`. Enable in production to require HTTPS. */
84
+ secure?: boolean
85
+ /** Default: `'Lax'` */
86
+ sameSite?: 'Strict' | 'Lax' | 'None'
87
+ /** Default: `'/'` */
88
+ path?: string
89
+ domain?: string
90
+ }
91
+
92
+ // ─── Signing ──────────────────────────────────────────────────────────────────
93
+
94
+ function sign(payload: string, secret: string): string {
95
+ const sig = createHmac('sha256', secret).update(payload).digest('base64url')
96
+ return `${payload}.${sig}`
97
+ }
98
+
99
+ function verify(value: string, secrets: string[]): Record<string, unknown> | null {
100
+ const dot = value.lastIndexOf('.')
101
+ if (dot === -1) return null
102
+
103
+ const payload = value.slice(0, dot)
104
+ const sigBuf = Buffer.from(value.slice(dot + 1), 'base64url')
105
+
106
+ for (const secret of secrets) {
107
+ const expectedBuf = createHmac('sha256', secret).update(payload).digest()
108
+ if (expectedBuf.length === sigBuf.length && timingSafeEqual(expectedBuf, sigBuf)) {
109
+ try {
110
+ return JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')) as Record<
111
+ string,
112
+ unknown
113
+ >
114
+ } catch {
115
+ return null
116
+ }
117
+ }
118
+ }
119
+
120
+ return null
121
+ }
122
+
123
+ // ─── Middleware ───────────────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * HMAC-signed cookie session middleware. Parses the incoming session cookie,
127
+ * exposes the data as `ctx.state.session`, and automatically writes an updated
128
+ * cookie when the session is mutated or destroyed.
129
+ *
130
+ * Pass an array of secrets for key rotation: the first secret signs new
131
+ * sessions, all secrets are accepted for verification.
132
+ *
133
+ * @example
134
+ * import { sessionMiddleware } from '@davaux/session'
135
+ * export default sessionMiddleware({
136
+ * secret: process.env.SESSION_SECRET!,
137
+ * maxAge: 60 * 60 * 24 * 7, // 1 week
138
+ * })
139
+ */
140
+ export function sessionMiddleware(options: SessionOptions): MiddlewareFn {
141
+ const secrets = Array.isArray(options.secret) ? options.secret : [options.secret]
142
+ if (secrets.length === 0 || secrets.some((s) => !s)) {
143
+ throw new Error('@davaux/session: at least one non-empty secret is required')
144
+ }
145
+ if (secrets.some((s) => s.length < 32)) {
146
+ console.warn(
147
+ '@davaux/session: secret should be at least 32 characters — short secrets are weak and easy to brute-force',
148
+ )
149
+ }
150
+
151
+ const name = options.cookieName ?? 'session'
152
+
153
+ return async (ctx, next) => {
154
+ const raw = ctx.cookies.get(name)
155
+ const existing = raw ? verify(raw, secrets) : null
156
+ const session = new Session(existing ?? {})
157
+ ctx.state.session = session
158
+
159
+ // Inject the session cookie right before headers are committed.
160
+ // This approach handles the common case where the route handler calls
161
+ // res.writeHead() inside next(), after which res.setHeader() would fail.
162
+ const originalWriteHead = ctx.res.writeHead.bind(ctx.res) as typeof ctx.res.writeHead
163
+ // biome-ignore lint/suspicious/noExplicitAny: writeHead has multiple overloads
164
+ ctx.res.writeHead = ((...args: any[]) => {
165
+ ctx.res.writeHead = originalWriteHead
166
+ if (session.destroyed) {
167
+ ctx.cookies.delete(name, { path: options.path ?? '/' })
168
+ } else if (session.dirty) {
169
+ const payload = Buffer.from(JSON.stringify(session.data)).toString('base64url')
170
+ const signed = sign(payload, secrets[0])
171
+ ctx.cookies.set(name, signed, {
172
+ httpOnly: options.httpOnly ?? true,
173
+ secure: options.secure ?? false,
174
+ sameSite: options.sameSite ?? 'Lax',
175
+ maxAge: options.maxAge,
176
+ path: options.path ?? '/',
177
+ domain: options.domain,
178
+ })
179
+ }
180
+ // biome-ignore lint/suspicious/noExplicitAny: forwarding overloaded args
181
+ return (originalWriteHead as (...a: any[]) => ServerResponse)(...args)
182
+ }) as typeof ctx.res.writeHead
183
+
184
+ await next()
185
+ }
186
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "skipLibCheck": true,
13
+ "lib": ["ESNext"],
14
+ "types": ["node"]
15
+ },
16
+ "include": ["src/**/*"]
17
+ }