hono-sessions 0.2.4 → 0.3.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 CHANGED
@@ -1,128 +1,104 @@
1
1
  # Hono Sessions Middleware
2
- Use cookie-based sessions with the [Hono](https://hono.dev/) framework. Currently tested to work with Cloudflare Workers and Deno.
2
+ Use cookie-based sessions with the [Hono](https://hono.dev/) framework. Currently tested to work with Deno, Cloudflare Workers, and Bun.
3
3
 
4
4
  ### 🛠️ Features
5
- - Runs in Deno and Cloudflare Workers (possibly others, currently untested)
5
+ - Runs in Deno, Cloudflare Workers, and Bun (possibly others, currently untested)
6
6
  - Flash messages — data that is deleted once it's read (one-off error messages, etc.)
7
7
  - Built-in Memory and Cookie storage drivers (more coming soon)
8
8
  - Encrypted cookies thanks to [iron-webcrypto](https://github.com/brc-dd/iron-webcrypto)
9
9
  - Session expiration after inactivity
10
- - Session key rotation, for mitigating session fixation attacks
10
+ - Session key rotation*
11
11
 
12
- ## Usage
12
+ > *CookieStore is not able to rotate session keys by nature of how a pure cookie session works (no server-side state).
13
13
 
14
- ### Cloudflare Workers
14
+ ## Installation and Usage
15
15
 
16
- Install from NPM
17
- ```
18
- npm install hono-sessions
19
- ```
16
+ ### Deno
20
17
 
21
- Here is a full-fledged example that shows what a login form might look like:
18
+ Simply include the package from `deno.land/x`
22
19
 
23
20
  ```ts
24
- import { Hono } from 'hono'
25
- import { sessionMiddleware, CookieStore, Session } from 'hono-sessions'
21
+ import { sessionMiddleware } from 'https://deno.land/x/hono_sessions/mod.ts'
22
+ ```
26
23
 
27
- const store = new CookieStore()
24
+ ### Bun, Cloudflare Workers
25
+
26
+ Install the NPM package
27
+ ```
28
+ npm install hono-sessions
29
+ ```
28
30
 
29
- const app = new Hono()
31
+ ## Examples
30
32
 
31
- const sessionRoutes = new Hono<{
33
+ ### Deno
34
+ ```ts
35
+ import { Hono } from 'https://deno.land/x/hono@v3.5.8/mod.ts'
36
+ import {
37
+ Session,
38
+ sessionMiddleware,
39
+ CookieStore
40
+ } from 'https://deno.land/x/hono_sessions/mod.ts'
41
+
42
+ const app = new Hono<{
32
43
  Variables: {
33
44
  session: Session,
34
45
  session_key_rotation: boolean
35
46
  }
36
47
  }>()
37
48
 
38
- sessionRoutes.use('*', sessionMiddleware({
49
+ const store = new CookieStore()
50
+
51
+ app.use('*', sessionMiddleware({
39
52
  store,
40
- expireAfterSeconds: 900, // delete session after 15 minutes of inactivity
41
- encryptionKey: 'password_that_is_at_least_32_characters_long' // Required while using CookieStore. Please use a secure, un-guessable password!
53
+ encryptionKey: 'password_at_least_32_characters_long', // Required for CookieStore, recommended for others
54
+ expireAfterSeconds: 900, // Expire session after 15 minutes
55
+ cookieOptions: {
56
+ sameSite: 'Lax',
57
+ },
42
58
  }))
43
59
 
44
- sessionRoutes.post('/login', async (c) => {
60
+ app.get('/', async (c, next) => {
45
61
  const session = c.get('session')
46
62
 
47
- const { email, password } = await c.req.parseBody()
48
-
49
- if (password === 'correct') {
50
- c.set('session_key_rotation', true)
51
- session.set('email', email)
52
- session.set('failed-login-attempts', null)
53
- session.flash('message', 'Login Successful')
63
+ if (session.get('counter')) {
64
+ session.set('counter', session.get('counter') as number + 1)
54
65
  } else {
55
- const failedLoginAttempts = (session.get('failed-login-attempts') || 0) as number
56
- session.set('failed-login-attempts', failedLoginAttempts + 1)
57
- session.flash('error', 'Incorrect username or password')
66
+ session.set('counter', 1)
58
67
  }
59
68
 
60
- return c.redirect('/')
69
+ return c.html(`<h1>You have visited this page ${ session.get('counter') } times</h1>`)
61
70
  })
62
71
 
63
- sessionRoutes.post('/logout', (c) => {
64
- c.get('session').deleteSession()
65
- return c.redirect('/')
66
- })
72
+ Deno.serve(app.fetch)
73
+ ```
67
74
 
68
- sessionRoutes.get('/', (c) => {
69
- const session = c.get('session')
75
+ ### Bun
70
76
 
71
- const message = session.get('message') || ''
72
- const error = session.get('error') || ''
73
- const failedLoginAttempts = session.get('failed-login-attempts')
74
- const email = session.get('email')
75
-
76
- return c.html(`<!DOCTYPE html>
77
- <html lang="en">
78
- <head>
79
- <meta charset="UTF-8">
80
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
81
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
82
- <title>Hono Sessions</title>
83
- </head>
84
- <body>
85
- <p>${message}</p>
86
- <p>${error}</p>
87
- <p>${failedLoginAttempts ? `Failed login attempts: ${failedLoginAttempts}` : ''}</p>
88
-
89
- ${email ?
90
- `<form id="logout" action="/logout" method="post">
91
- <button name="logout" type="submit">Log out ${email}</button>
92
- </form>`
93
- :
94
- `<form id="login" action="/login" method="post">
95
- <p>
96
- <input id="email" name="email" type="text" placeholder="you@email.com">
97
- </p>
98
- <p>
99
- <input id="password" name="password" type="password" placeholder="password">
100
- </p>
101
- <button name="login" type="submit">Log in</button>
102
- </form>`
103
- }
104
- </body>
105
- </html>`)
106
- })
77
+ ```ts
78
+ import { Hono } from 'hono'
79
+ import { sessionMiddleware, CookieStore, Session } from 'hono-sessions'
107
80
 
108
- app.route('/', sessionRoutes)
81
+ // Same as Deno, however instead of:
82
+ // Deno.serve(app.fetch)
83
+ // use:
109
84
 
110
- export default app
85
+ export default {
86
+ port: 3000,
87
+ fetch: app.fetch
88
+ }
111
89
  ```
112
90
 
113
- ### Deno
114
-
115
- There is a Deno package available on `deno.land/x`.
91
+ ### Cloudflare Workers
116
92
 
117
93
  ```ts
118
- import { Hono } from 'https://deno.land/x/hono/mod.ts'
119
- import { sessionMiddleware, CookieStore, Session } from 'https://deno.land/x/hono_sessions/mod.ts'
94
+ import { Hono } from 'hono'
95
+ import { sessionMiddleware, CookieStore, Session } from 'hono-sessions'
120
96
 
121
- // Same as CF Workers, however instead of:
122
- // export default app
97
+ // Same as Deno, however instead of:
98
+ // Deno.serve(app.fetch)
123
99
  // use:
124
100
 
125
- Deno.serve(app.fetch)
101
+ export default app
126
102
  ```
127
103
 
128
104
  ## Contributing
@@ -132,5 +108,13 @@ This package is built Deno-first, so you'll need to have Deno installed in your
132
108
  Once Deno is installed, there is a test server you can run a basic web server to check your changes:
133
109
 
134
110
  ```
135
- deno run --allow-net --watch test/server_deno.ts
111
+ deno run --allow-net --watch test/deno/server_deno.ts
112
+ ```
113
+
114
+ There's also a [Playwright](https://playwright.dev/) test suite. By default, it is set up to run a Deno server with the MemoryStore driver. In Github actions, it runs through a series of runtimes and storage drivers when a pull request is made.
115
+
116
+ ```
117
+ cd playwright
118
+ npm install
119
+ npx playwright test
136
120
  ```
@@ -31,8 +31,13 @@ export function sessionMiddleware(options) {
31
31
  session_data = await store.getSession(c);
32
32
  }
33
33
  else {
34
- sid = (encryptionKey ? await decrypt(encryptionKey, sessionCookie) : sessionCookie);
35
- session_data = await store.getSessionById(sid);
34
+ try {
35
+ sid = (encryptionKey ? await decrypt(encryptionKey, sessionCookie) : sessionCookie);
36
+ session_data = await store.getSessionById(sid);
37
+ }
38
+ catch {
39
+ createNewSession = true;
40
+ }
36
41
  }
37
42
  if (session_data) {
38
43
  session.setCache(session_data);
@@ -10,7 +10,7 @@ declare class CookieStore {
10
10
  cookieOptions: CookieOptions | undefined;
11
11
  sessionCookieName: string;
12
12
  constructor(options?: CookieStoreOptions);
13
- getSession(c: Context): Promise<any>;
13
+ getSession(c: Context): Promise<SessionData | null>;
14
14
  createSession(c: Context, initial_data: SessionData): Promise<void>;
15
15
  deleteSession(c: Context): Promise<void>;
16
16
  persistSessionData(c: Context, session_data: SessionData): Promise<void>;
@@ -25,12 +25,23 @@ class CookieStore {
25
25
  this.sessionCookieName = options?.sessionCookieName || 'session';
26
26
  }
27
27
  async getSession(c) {
28
- let session_data;
28
+ let session_data_raw;
29
29
  const sessionCookie = getCookie(c, this.sessionCookieName);
30
30
  if (this.encryptionKey && sessionCookie) {
31
- session_data = (await decrypt(this.encryptionKey, sessionCookie));
32
- if (session_data) {
33
- return JSON.parse(session_data);
31
+ // Decrypt cookie string. If decryption fails, return null
32
+ try {
33
+ session_data_raw = (await decrypt(this.encryptionKey, sessionCookie));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ // Parse session object from cookie string and return result. If fails, return null
39
+ try {
40
+ const session_data = JSON.parse(session_data_raw);
41
+ return session_data;
42
+ }
43
+ catch {
44
+ return null;
34
45
  }
35
46
  }
36
47
  else {
@@ -0,0 +1,11 @@
1
+ import Store from '../Store.js';
2
+ import { SessionData } from '../../Session.js';
3
+ export declare class BunSqliteStore implements Store {
4
+ db: any;
5
+ tableName: string;
6
+ constructor(db: any, tableName?: string);
7
+ getSessionById(sessionId: string): any;
8
+ createSession(sessionId: string, initialData: SessionData): void;
9
+ deleteSession(sessionId: string): void;
10
+ persistSessionData(sessionId: string, sessionData: SessionData): void;
11
+ }
@@ -0,0 +1,42 @@
1
+ export class BunSqliteStore {
2
+ constructor(db, tableName = 'sessions') {
3
+ Object.defineProperty(this, "db", {
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true,
7
+ value: void 0
8
+ });
9
+ Object.defineProperty(this, "tableName", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: void 0
14
+ });
15
+ this.db = db;
16
+ this.tableName = tableName;
17
+ const query = db.query(`CREATE TABLE IF NOT EXISTS ${tableName} (id TEXT PRIMARY KEY, data TEXT)`);
18
+ query.run();
19
+ }
20
+ getSessionById(sessionId) {
21
+ const query = this.db.query(`SELECT data FROM ${this.tableName} WHERE id = $id`);
22
+ const result = query.get({ $id: sessionId });
23
+ if (result) {
24
+ return JSON.parse(result.data);
25
+ }
26
+ else {
27
+ return null;
28
+ }
29
+ }
30
+ createSession(sessionId, initialData) {
31
+ const query = this.db.query(`INSERT INTO ${this.tableName} (id, data) VALUES ($id, $data)`);
32
+ query.run({ $id: sessionId, $data: JSON.stringify(initialData) });
33
+ }
34
+ deleteSession(sessionId) {
35
+ const query = this.db.query(`DELETE FROM ${this.tableName} WHERE id = $id`);
36
+ query.run({ $id: sessionId });
37
+ }
38
+ persistSessionData(sessionId, sessionData) {
39
+ const query = this.db.query(`UPDATE ${this.tableName} SET data = $data WHERE id = $id`);
40
+ query.run({ $id: sessionId, $data: JSON.stringify(sessionData) });
41
+ }
42
+ }
@@ -0,0 +1,11 @@
1
+ import Store from '../Store.js';
2
+ import { SessionData } from '../../Session.js';
3
+ export declare class CloudflareD1Store implements Store {
4
+ db: any;
5
+ tableName: string;
6
+ constructor(tableName?: string);
7
+ getSessionById(sessionId?: string | undefined): Promise<any>;
8
+ createSession(sessionId: string, initialData: SessionData): Promise<void>;
9
+ deleteSession(sessionId: string): Promise<void>;
10
+ persistSessionData(sessionId: string, sessionData: SessionData): Promise<void>;
11
+ }
@@ -0,0 +1,37 @@
1
+ export class CloudflareD1Store {
2
+ constructor(tableName = 'sessions') {
3
+ Object.defineProperty(this, "db", {
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true,
7
+ value: void 0
8
+ });
9
+ Object.defineProperty(this, "tableName", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: void 0
14
+ });
15
+ this.tableName = tableName;
16
+ }
17
+ async getSessionById(sessionId) {
18
+ const session = await this.db.prepare(`SELECT data FROM ${this.tableName} WHERE id = ?`)
19
+ .bind(sessionId)
20
+ .first('data');
21
+ if (session) {
22
+ return JSON.parse(session);
23
+ }
24
+ else {
25
+ return null;
26
+ }
27
+ }
28
+ async createSession(sessionId, initialData) {
29
+ await this.db.prepare(`INSERT INTO ${this.tableName} (id, data) VALUES (?, ?)`).bind(sessionId, JSON.stringify(initialData)).run();
30
+ }
31
+ async deleteSession(sessionId) {
32
+ await this.db.prepare(`DELETE FROM ${this.tableName} WHERE id = ?`).bind(sessionId).run();
33
+ }
34
+ async persistSessionData(sessionId, sessionData) {
35
+ await this.db.prepare(`UPDATE ${this.tableName} SET data = ? WHERE id = ?`).bind(JSON.stringify(sessionData), sessionId).run();
36
+ }
37
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "module": "./esm/mod.js",
3
3
  "main": "./script/mod.js",
4
4
  "name": "hono-sessions",
5
- "version": "0.2.4",
5
+ "version": "0.3.1",
6
6
  "description": "Cookie-based sessions for Hono web framework",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -16,6 +16,14 @@
16
16
  ".": {
17
17
  "import": "./esm/mod.js",
18
18
  "require": "./script/mod.js"
19
+ },
20
+ "./bun-sqlite-store": {
21
+ "import": "./esm/src/store/bun/BunSqliteStore.js",
22
+ "require": "./script/src/store/bun/BunSqliteStore.js"
23
+ },
24
+ "./cloudflare-d1-store": {
25
+ "import": "./esm/src/store/cloudflare/CloudflareD1Store.js",
26
+ "require": "./script/src/store/cloudflare/CloudflareD1Store.js"
19
27
  }
20
28
  },
21
29
  "dependencies": {
@@ -37,8 +37,13 @@ function sessionMiddleware(options) {
37
37
  session_data = await store.getSession(c);
38
38
  }
39
39
  else {
40
- sid = (encryptionKey ? await (0, mod_js_1.decrypt)(encryptionKey, sessionCookie) : sessionCookie);
41
- session_data = await store.getSessionById(sid);
40
+ try {
41
+ sid = (encryptionKey ? await (0, mod_js_1.decrypt)(encryptionKey, sessionCookie) : sessionCookie);
42
+ session_data = await store.getSessionById(sid);
43
+ }
44
+ catch {
45
+ createNewSession = true;
46
+ }
42
47
  }
43
48
  if (session_data) {
44
49
  session.setCache(session_data);
@@ -10,7 +10,7 @@ declare class CookieStore {
10
10
  cookieOptions: CookieOptions | undefined;
11
11
  sessionCookieName: string;
12
12
  constructor(options?: CookieStoreOptions);
13
- getSession(c: Context): Promise<any>;
13
+ getSession(c: Context): Promise<SessionData | null>;
14
14
  createSession(c: Context, initial_data: SessionData): Promise<void>;
15
15
  deleteSession(c: Context): Promise<void>;
16
16
  persistSessionData(c: Context, session_data: SessionData): Promise<void>;
@@ -27,12 +27,23 @@ class CookieStore {
27
27
  this.sessionCookieName = options?.sessionCookieName || 'session';
28
28
  }
29
29
  async getSession(c) {
30
- let session_data;
30
+ let session_data_raw;
31
31
  const sessionCookie = (0, deps_js_1.getCookie)(c, this.sessionCookieName);
32
32
  if (this.encryptionKey && sessionCookie) {
33
- session_data = (await (0, mod_js_1.decrypt)(this.encryptionKey, sessionCookie));
34
- if (session_data) {
35
- return JSON.parse(session_data);
33
+ // Decrypt cookie string. If decryption fails, return null
34
+ try {
35
+ session_data_raw = (await (0, mod_js_1.decrypt)(this.encryptionKey, sessionCookie));
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ // Parse session object from cookie string and return result. If fails, return null
41
+ try {
42
+ const session_data = JSON.parse(session_data_raw);
43
+ return session_data;
44
+ }
45
+ catch {
46
+ return null;
36
47
  }
37
48
  }
38
49
  else {
@@ -0,0 +1,11 @@
1
+ import Store from '../Store.js';
2
+ import { SessionData } from '../../Session.js';
3
+ export declare class BunSqliteStore implements Store {
4
+ db: any;
5
+ tableName: string;
6
+ constructor(db: any, tableName?: string);
7
+ getSessionById(sessionId: string): any;
8
+ createSession(sessionId: string, initialData: SessionData): void;
9
+ deleteSession(sessionId: string): void;
10
+ persistSessionData(sessionId: string, sessionData: SessionData): void;
11
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BunSqliteStore = void 0;
4
+ class BunSqliteStore {
5
+ constructor(db, tableName = 'sessions') {
6
+ Object.defineProperty(this, "db", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: void 0
11
+ });
12
+ Object.defineProperty(this, "tableName", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: void 0
17
+ });
18
+ this.db = db;
19
+ this.tableName = tableName;
20
+ const query = db.query(`CREATE TABLE IF NOT EXISTS ${tableName} (id TEXT PRIMARY KEY, data TEXT)`);
21
+ query.run();
22
+ }
23
+ getSessionById(sessionId) {
24
+ const query = this.db.query(`SELECT data FROM ${this.tableName} WHERE id = $id`);
25
+ const result = query.get({ $id: sessionId });
26
+ if (result) {
27
+ return JSON.parse(result.data);
28
+ }
29
+ else {
30
+ return null;
31
+ }
32
+ }
33
+ createSession(sessionId, initialData) {
34
+ const query = this.db.query(`INSERT INTO ${this.tableName} (id, data) VALUES ($id, $data)`);
35
+ query.run({ $id: sessionId, $data: JSON.stringify(initialData) });
36
+ }
37
+ deleteSession(sessionId) {
38
+ const query = this.db.query(`DELETE FROM ${this.tableName} WHERE id = $id`);
39
+ query.run({ $id: sessionId });
40
+ }
41
+ persistSessionData(sessionId, sessionData) {
42
+ const query = this.db.query(`UPDATE ${this.tableName} SET data = $data WHERE id = $id`);
43
+ query.run({ $id: sessionId, $data: JSON.stringify(sessionData) });
44
+ }
45
+ }
46
+ exports.BunSqliteStore = BunSqliteStore;
@@ -0,0 +1,11 @@
1
+ import Store from '../Store.js';
2
+ import { SessionData } from '../../Session.js';
3
+ export declare class CloudflareD1Store implements Store {
4
+ db: any;
5
+ tableName: string;
6
+ constructor(tableName?: string);
7
+ getSessionById(sessionId?: string | undefined): Promise<any>;
8
+ createSession(sessionId: string, initialData: SessionData): Promise<void>;
9
+ deleteSession(sessionId: string): Promise<void>;
10
+ persistSessionData(sessionId: string, sessionData: SessionData): Promise<void>;
11
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CloudflareD1Store = void 0;
4
+ class CloudflareD1Store {
5
+ constructor(tableName = 'sessions') {
6
+ Object.defineProperty(this, "db", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: void 0
11
+ });
12
+ Object.defineProperty(this, "tableName", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: void 0
17
+ });
18
+ this.tableName = tableName;
19
+ }
20
+ async getSessionById(sessionId) {
21
+ const session = await this.db.prepare(`SELECT data FROM ${this.tableName} WHERE id = ?`)
22
+ .bind(sessionId)
23
+ .first('data');
24
+ if (session) {
25
+ return JSON.parse(session);
26
+ }
27
+ else {
28
+ return null;
29
+ }
30
+ }
31
+ async createSession(sessionId, initialData) {
32
+ await this.db.prepare(`INSERT INTO ${this.tableName} (id, data) VALUES (?, ?)`).bind(sessionId, JSON.stringify(initialData)).run();
33
+ }
34
+ async deleteSession(sessionId) {
35
+ await this.db.prepare(`DELETE FROM ${this.tableName} WHERE id = ?`).bind(sessionId).run();
36
+ }
37
+ async persistSessionData(sessionId, sessionData) {
38
+ await this.db.prepare(`UPDATE ${this.tableName} SET data = ? WHERE id = ?`).bind(JSON.stringify(sessionData), sessionId).run();
39
+ }
40
+ }
41
+ exports.CloudflareD1Store = CloudflareD1Store;