@davaux/session 0.8.0 → 0.8.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.
@@ -0,0 +1,69 @@
1
+ import type { MiddlewareFn } from 'davaux';
2
+ declare module 'davaux' {
3
+ interface State {
4
+ session: Session;
5
+ }
6
+ }
7
+ /**
8
+ * Per-request session object. Stores arbitrary key-value data serialised into
9
+ * an HMAC-signed cookie. Mutations (`set`, `delete`, `destroy`) are persisted
10
+ * automatically before the response headers are sent.
11
+ */
12
+ export declare class Session {
13
+ private readonly _data;
14
+ private _dirty;
15
+ private _destroyed;
16
+ constructor(data?: Record<string, unknown>);
17
+ /** Retrieve a session value by key. Returns `undefined` when absent. */
18
+ get<T = unknown>(key: string): T | undefined;
19
+ /** Store a value under `key`. Marks the session dirty so it is persisted. */
20
+ set(key: string, value: unknown): void;
21
+ /** Remove `key` from the session. Marks the session dirty so it is persisted. */
22
+ delete(key: string): void;
23
+ /** Clear all session data and delete the cookie on response. */
24
+ destroy(): void;
25
+ /** Snapshot of current session data. */
26
+ get data(): Record<string, unknown>;
27
+ /** @internal */
28
+ get dirty(): boolean;
29
+ /** @internal */
30
+ get destroyed(): boolean;
31
+ }
32
+ export interface SessionOptions {
33
+ /**
34
+ * Secret(s) used to sign the session cookie with HMAC-SHA256.
35
+ * Pass an array for rotation: the first secret signs new sessions,
36
+ * all are accepted for verification.
37
+ */
38
+ secret: string | string[];
39
+ /** Cookie name. Default: `'session'` */
40
+ cookieName?: string;
41
+ /** Max-Age in seconds. Omit for a session cookie (clears on browser close). */
42
+ maxAge?: number;
43
+ /** Default: `true` */
44
+ httpOnly?: boolean;
45
+ /** Default: `false`. Enable in production to require HTTPS. */
46
+ secure?: boolean;
47
+ /** Default: `'Lax'` */
48
+ sameSite?: 'Strict' | 'Lax' | 'None';
49
+ /** Default: `'/'` */
50
+ path?: string;
51
+ domain?: string;
52
+ }
53
+ /**
54
+ * HMAC-signed cookie session middleware. Parses the incoming session cookie,
55
+ * exposes the data as `ctx.state.session`, and automatically writes an updated
56
+ * cookie when the session is mutated or destroyed.
57
+ *
58
+ * Pass an array of secrets for key rotation: the first secret signs new
59
+ * sessions, all secrets are accepted for verification.
60
+ *
61
+ * @example
62
+ * import { sessionMiddleware } from '@davaux/session'
63
+ * export default sessionMiddleware({
64
+ * secret: process.env.SESSION_SECRET!,
65
+ * maxAge: 60 * 60 * 24 * 7, // 1 week
66
+ * })
67
+ */
68
+ export declare function sessionMiddleware(options: SessionOptions): MiddlewareFn;
69
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAI1C,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,KAAK;QACb,OAAO,EAAE,OAAO,CAAA;KACjB;CACF;AAID;;;;GAIG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,UAAU,CAAQ;gBAEd,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAI9C,wEAAwE;IACxE,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI5C,6EAA6E;IAC7E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAKtC,iFAAiF;IACjF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKzB,gEAAgE;IAChE,OAAO,IAAI,IAAI;IAKf,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAElC;IAED,gBAAgB;IAChB,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,gBAAgB;IAChB,IAAI,SAAS,IAAI,OAAO,CAEvB;CACF;AAID,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACzB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;IACpC,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAmCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,YAAY,CA8CvE"}
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ // ─── Session ──────────────────────────────────────────────────────────────────
3
+ /**
4
+ * Per-request session object. Stores arbitrary key-value data serialised into
5
+ * an HMAC-signed cookie. Mutations (`set`, `delete`, `destroy`) are persisted
6
+ * automatically before the response headers are sent.
7
+ */
8
+ export class Session {
9
+ _data;
10
+ _dirty = false;
11
+ _destroyed = false;
12
+ constructor(data = {}) {
13
+ this._data = data;
14
+ }
15
+ /** Retrieve a session value by key. Returns `undefined` when absent. */
16
+ get(key) {
17
+ return this._data[key];
18
+ }
19
+ /** Store a value under `key`. Marks the session dirty so it is persisted. */
20
+ set(key, value) {
21
+ this._data[key] = value;
22
+ this._dirty = true;
23
+ }
24
+ /** Remove `key` from the session. Marks the session dirty so it is persisted. */
25
+ delete(key) {
26
+ delete this._data[key];
27
+ this._dirty = true;
28
+ }
29
+ /** Clear all session data and delete the cookie on response. */
30
+ destroy() {
31
+ this._destroyed = true;
32
+ this._dirty = false;
33
+ }
34
+ /** Snapshot of current session data. */
35
+ get data() {
36
+ return { ...this._data };
37
+ }
38
+ /** @internal */
39
+ get dirty() {
40
+ return this._dirty;
41
+ }
42
+ /** @internal */
43
+ get destroyed() {
44
+ return this._destroyed;
45
+ }
46
+ }
47
+ // ─── Signing ──────────────────────────────────────────────────────────────────
48
+ function sign(payload, secret) {
49
+ const sig = createHmac('sha256', secret).update(payload).digest('base64url');
50
+ return `${payload}.${sig}`;
51
+ }
52
+ function verify(value, secrets) {
53
+ const dot = value.lastIndexOf('.');
54
+ if (dot === -1)
55
+ return null;
56
+ const payload = value.slice(0, dot);
57
+ const sigBuf = Buffer.from(value.slice(dot + 1), 'base64url');
58
+ for (const secret of secrets) {
59
+ const expectedBuf = createHmac('sha256', secret).update(payload).digest();
60
+ if (expectedBuf.length === sigBuf.length && timingSafeEqual(expectedBuf, sigBuf)) {
61
+ try {
62
+ return JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ // ─── Middleware ───────────────────────────────────────────────────────────────
72
+ /**
73
+ * HMAC-signed cookie session middleware. Parses the incoming session cookie,
74
+ * exposes the data as `ctx.state.session`, and automatically writes an updated
75
+ * cookie when the session is mutated or destroyed.
76
+ *
77
+ * Pass an array of secrets for key rotation: the first secret signs new
78
+ * sessions, all secrets are accepted for verification.
79
+ *
80
+ * @example
81
+ * import { sessionMiddleware } from '@davaux/session'
82
+ * export default sessionMiddleware({
83
+ * secret: process.env.SESSION_SECRET!,
84
+ * maxAge: 60 * 60 * 24 * 7, // 1 week
85
+ * })
86
+ */
87
+ export function sessionMiddleware(options) {
88
+ const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];
89
+ if (secrets.length === 0 || secrets.some((s) => !s)) {
90
+ throw new Error('@davaux/session: at least one non-empty secret is required');
91
+ }
92
+ if (secrets.some((s) => s.length < 32)) {
93
+ console.warn('@davaux/session: secret should be at least 32 characters — short secrets are weak and easy to brute-force');
94
+ }
95
+ const name = options.cookieName ?? 'session';
96
+ return async (ctx, next) => {
97
+ const raw = ctx.cookies.get(name);
98
+ const existing = raw ? verify(raw, secrets) : null;
99
+ const session = new Session(existing ?? {});
100
+ ctx.state.session = session;
101
+ // Inject the session cookie right before headers are committed.
102
+ // This approach handles the common case where the route handler calls
103
+ // res.writeHead() inside next(), after which res.setHeader() would fail.
104
+ const originalWriteHead = ctx.res.writeHead.bind(ctx.res);
105
+ // biome-ignore lint/suspicious/noExplicitAny: writeHead has multiple overloads
106
+ ctx.res.writeHead = ((...args) => {
107
+ ctx.res.writeHead = originalWriteHead;
108
+ if (session.destroyed) {
109
+ ctx.cookies.delete(name, { path: options.path ?? '/' });
110
+ }
111
+ else if (session.dirty) {
112
+ const payload = Buffer.from(JSON.stringify(session.data)).toString('base64url');
113
+ const signed = sign(payload, secrets[0]);
114
+ ctx.cookies.set(name, signed, {
115
+ httpOnly: options.httpOnly ?? true,
116
+ secure: options.secure ?? false,
117
+ sameSite: options.sameSite ?? 'Lax',
118
+ maxAge: options.maxAge,
119
+ path: options.path ?? '/',
120
+ domain: options.domain,
121
+ });
122
+ }
123
+ // biome-ignore lint/suspicious/noExplicitAny: forwarding overloaded args
124
+ return originalWriteHead(...args);
125
+ });
126
+ await next();
127
+ };
128
+ }
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davaux/session",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "HMAC-signed cookie sessions for Davaux",
5
5
  "type": "module",
6
6
  "author": "David L Dyess II",
@@ -13,6 +13,9 @@
13
13
  "url": "https://codeberg.org/davaux/davaux/issues"
14
14
  },
15
15
  "homepage": "https://codeberg.org/davaux/davaux#readme",
16
+ "files": [
17
+ "dist"
18
+ ],
16
19
  "exports": {
17
20
  ".": {
18
21
  "import": "./dist/index.js",
@@ -24,11 +27,11 @@
24
27
  "typecheck": "tsc --noEmit"
25
28
  },
26
29
  "peerDependencies": {
27
- "davaux": ">=0.8.0"
30
+ "davaux": ">=0.8.1"
28
31
  },
29
32
  "devDependencies": {
30
33
  "@types/node": "^25.0.0",
31
34
  "davaux": "*",
32
35
  "typescript": "^6.0.3"
33
36
  }
34
- }
37
+ }
package/CLAUDE.md DELETED
@@ -1,95 +0,0 @@
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/src/index.ts DELETED
@@ -1,186 +0,0 @@
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 DELETED
@@ -1,17 +0,0 @@
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
- }