@randajan/koa-io-session 1.0.0 → 2.1.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/README.md CHANGED
@@ -1,15 +1,20 @@
1
1
  # @randajan/koa-io-session
2
2
 
3
- [![NPM](https://img.shields.io/npm/v/@randajan/koa-io-session.svg)](https://www.npmjs.com/package/@randajan/koa-io-session) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
3
+ [![NPM](https://img.shields.io/npm/v/@randajan/koa-io-session.svg)](https://www.npmjs.com/package/@randajan/koa-io-session)
4
+ [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
4
5
 
5
6
  Bridge between `koa-session` and `socket.io` with one shared session store.
6
7
 
7
- You get:
8
- - standard `ctx.session` for HTTP
9
- - `socket.sessionId` and `socket.withSession(...)` for WebSocket
10
- - a clear destroy flow for stale sockets
8
+ ## Why this library
9
+
10
+ It keeps HTTP and WebSocket session handling in sync while staying close to native `koa-session` behavior.
11
11
 
12
- ---
12
+ You get:
13
+ - standard `ctx.session` in HTTP handlers
14
+ - `ctx.clientId` and `socket.clientId` for early client tracking
15
+ - `ctx.sessionId` and `socket.sessionId` resolved by bridge mapping
16
+ - `socket.withSession(handler)` for safe session read/write from WS handlers
17
+ - bridge-level events `sessionStart` and `sessionEnd`
13
18
 
14
19
  ## Install
15
20
 
@@ -17,27 +22,13 @@ You get:
17
22
  npm i @randajan/koa-io-session
18
23
  ```
19
24
 
20
- ---
21
-
22
- ## Use case
23
-
24
- Typical scenario:
25
- 1. User signs in via HTTP (`ctx.session.userId = ...`).
26
- 2. Client opens Socket.IO connection.
27
- 3. Socket handlers read/update the same session as HTTP handlers.
28
- 4. Session reset/logout invalidates related sockets.
29
-
30
- This is useful for realtime apps where HTTP and WS must stay consistent without duplicate auth/session logic.
31
-
32
- ---
33
-
34
25
  ## Quick start
35
26
 
36
27
  ```js
37
28
  import Koa from "koa";
38
29
  import { createServer } from "http";
39
30
  import { Server } from "socket.io";
40
- import { attachSession } from "@randajan/koa-io-session";
31
+ import bridgeSession from "@randajan/koa-io-session";
41
32
 
42
33
  const app = new Koa();
43
34
  const http = createServer(app.callback());
@@ -45,23 +36,27 @@ const io = new Server(http, {
45
36
  cors: { origin: true, credentials: true }
46
37
  });
47
38
 
48
- const store = attachSession(app, io, {
49
- key: "koa.io.sid",
39
+ const bridge = bridgeSession(app, io, {
40
+ key: "app.sid",
50
41
  signed: true,
51
42
  maxAge: 24 * 60 * 60 * 1000,
52
43
  sameSite: "lax",
53
- httpOnly: true
44
+ httpOnly: true,
45
+ secure: false
54
46
  });
55
47
 
56
- // Required: cleanup is manual
57
- store.autoCleanup();
48
+ bridge.on("sessionStart", ({ clientId, sessionId }) => {
49
+ console.log("sessionStart", clientId, sessionId);
50
+ });
51
+
52
+ bridge.on("sessionEnd", ({ clientId, sessionId }) => {
53
+ console.log("sessionEnd", clientId, sessionId);
54
+ });
58
55
 
59
- // HTTP route example
60
56
  app.use(async (ctx, next) => {
61
57
  if (ctx.path !== "/api/session") { return next(); }
62
58
 
63
- const action = String(ctx.query.action || "read");
64
- if (action === "reset") {
59
+ if (ctx.query.reset === "1") {
65
60
  ctx.session = null;
66
61
  ctx.body = { ok: true, from: "http:reset" };
67
62
  return;
@@ -69,11 +64,12 @@ app.use(async (ctx, next) => {
69
64
 
70
65
  if (!ctx.session.createdAt) { ctx.session.createdAt = Date.now(); }
71
66
  if (!Number.isFinite(ctx.session.httpCount)) { ctx.session.httpCount = 0; }
72
- if (action === "inc") { ctx.session.httpCount += 1; }
67
+ ctx.session.httpCount += 1;
73
68
 
74
69
  ctx.body = {
75
70
  ok: true,
76
- from: `http:${action}`,
71
+ from: "http",
72
+ clientId: ctx.clientId,
77
73
  sessionId: ctx.sessionId,
78
74
  session: ctx.session
79
75
  };
@@ -86,6 +82,7 @@ io.on("connection", (socket) => {
86
82
  return {
87
83
  ok: true,
88
84
  from: "ws:get",
85
+ clientId: socket.clientId,
89
86
  sessionId: sessionCtx.sessionId,
90
87
  session: sessionCtx.session
91
88
  };
@@ -93,7 +90,7 @@ io.on("connection", (socket) => {
93
90
  if (typeof ack === "function") { ack(payload); }
94
91
  } catch (error) {
95
92
  if (typeof ack === "function") {
96
- ack({ ok: false, from: "ws:get", error: error.message });
93
+ ack({ ok: false, error: error.message });
97
94
  }
98
95
  }
99
96
  });
@@ -102,144 +99,125 @@ io.on("connection", (socket) => {
102
99
  http.listen(3000);
103
100
  ```
104
101
 
105
- ---
102
+ ## API
106
103
 
107
- ## Socket API
104
+ ### `bridgeSession(app, io, opt)`
108
105
 
109
- After `attachSession(app, io, opt)`:
106
+ Creates and returns `SessionBridge`.
110
107
 
111
- 1. `socket.sessionId`
112
- - Session ID resolved from cookie/external key during socket middleware.
108
+ Default export is `bridgeSession`.
113
109
 
114
- 2. `socket.withSession(handler)`
115
- - Safe wrapper for session operations with per-session lock.
116
- - `handler` receives `sessionCtx`:
117
- - `sessionCtx.sessionId` -> session ID
118
- - `sessionCtx.session` -> mutable session object
119
- - `sessionCtx.socket` -> socket instance
120
- - Return value of `handler` is returned by `withSession`.
110
+ ### `SessionBridge`
121
111
 
122
- Example:
112
+ `SessionBridge` extends Node.js `EventEmitter`.
123
113
 
124
- ```js
125
- const result = await socket.withSession(async (sessionCtx) => {
126
- sessionCtx.session.wsCount = (sessionCtx.session.wsCount || 0) + 1;
127
- return { ok: true, sid: sessionCtx.sessionId };
128
- });
129
- ```
114
+ Properties:
115
+ - `bridge.store` is the active store instance
130
116
 
131
- Destroy session from WS:
117
+ Events:
118
+ - `sessionStart` payload: `{ clientId, sessionId }`
119
+ - `sessionEnd` payload: `{ clientId, sessionId }`
132
120
 
133
- ```js
134
- await socket.withSession(async (sessionCtx) => {
135
- sessionCtx.session = null;
136
- });
137
- ```
138
-
139
- ---
140
-
141
- ## Session model behavior
142
-
143
- 1. WS does not create missing sessions.
144
- - If session is missing in store, `withSession` throws `Session not found`.
145
- - New session must be created via HTTP/Koa flow.
121
+ Runtime additions:
122
+ - HTTP context: `ctx.clientId`, `ctx.sessionId`
123
+ - Socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler)`
146
124
 
147
- 2. Change detection:
148
- - If session changed, store `set` is called.
149
- - If session did not change, throttled `touch` is used to refresh TTL.
125
+ ### `socket.withSession(handler)`
150
126
 
151
- 3. Concurrency:
152
- - Operations for same `sessionId` are serialized to avoid race conditions.
127
+ `handler` receives one object:
128
+ - `sessionCtx.sessionId`
129
+ - `sessionCtx.session`
130
+ - `sessionCtx.socket`
153
131
 
154
- ---
132
+ Rules:
133
+ - throws if `socket.sessionId` is missing
134
+ - throws `"Session not found"` if store does not have the session
135
+ - if `sessionCtx.session = null`, session is destroyed
136
+ - if session changed, store `set` is called
137
+ - calls for same `sessionId` are serialized
155
138
 
156
- ## Destroy flow
157
-
158
- When store emits `destroy` for a SID:
159
- 1. all sockets in room `sessionId:<sid>` receive `session:destroy`
160
- 2. after ~200 ms, these sockets are forcibly disconnected
161
-
162
- Client should react to `session:destroy` by:
163
- 1. performing HTTP bootstrap request (to get new cookie/session)
164
- 2. reconnecting socket
139
+ ## Options
165
140
 
166
- ---
141
+ `opt` is mostly forwarded to `koa-session`, except internal bridge keys:
167
142
 
168
- ## Reserved names and limitations
143
+ - `store`
144
+ - `autoCleanup`
145
+ - `autoCleanupMs`
146
+ - `clientKey`
147
+ - `clientMaxAge`
148
+ - `clientAlwaysRoll`
169
149
 
170
- 1. Room naming:
171
- - library joins sockets into room `sessionId:<sid>`
150
+ Defaults:
151
+ - `key`: random `generateUid(12)`
152
+ - `maxAge`: `30 days`
153
+ - `signed`: `true`
154
+ - `store`: `new SessionStore(opt)`
155
+ - `clientKey`: `${key}.cid`
156
+ - `clientMaxAge`: `1 year`
157
+ - `clientAlwaysRoll`: `true`
158
+ - `app.keys`: auto-generated if missing
172
159
 
173
- 2. Reserved socket event:
174
- - library emits `session:destroy`
160
+ Notes:
161
+ - set stable `app.keys` in production
162
+ - keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
175
163
 
176
- 3. Reserved socket properties:
177
- - library defines `socket.sessionId` and `socket.withSession`
164
+ ## `SessionStore`
178
165
 
179
- 4. WS cannot bootstrap missing session:
180
- - when session is missing in store, `withSession` throws `Session not found`
181
- - new session must be created via HTTP/Koa flow
166
+ Default in-memory store with TTL.
182
167
 
183
- 5. Session ID is fixed for current connection:
184
- - if cookie/session changes, existing socket must reconnect to use new SID
168
+ Constructor:
169
+ - `new SessionStore({ maxAge, autoCleanup, autoCleanupMs })`
185
170
 
186
- ---
171
+ Methods:
172
+ - `get(sid)`
173
+ - `set(sid, session, maxAge?)`
174
+ - `destroy(sid)` (also used by `delete`)
175
+ - `cleanup()`
176
+ - `on(eventName, callback)`
187
177
 
188
- ## Required integration details
178
+ Events emitted by store:
179
+ - `set`
180
+ - `destroy`
181
+ - `cleanup`
189
182
 
190
- 1. Cleanup is manual:
191
- - call `store.autoCleanup(interval)` yourself
183
+ ## Custom store contract
192
184
 
193
- 2. Custom store API is required:
185
+ Custom store is valid if it implements:
194
186
  - `get(sid)`
195
- - `set(sid, session, maxAge)`
187
+ - `set(sid, session, maxAge?)`
196
188
  - `destroy(sid)`
197
- - `touch(sid, maxAge)`
198
189
  - `on(eventName, callback)`
199
190
 
200
- 3. Store must emit destroy event:
201
- - event name: `destroy`
202
- - callback signature expected by this library: `(_store, sid) => {}`
203
-
204
- 4. Destroy socket behavior:
205
- - library emits `session:destroy`
206
- - library disconnects socket room ~200 ms later
191
+ Sync and async implementations are both supported.
207
192
 
208
- ---
193
+ Important integration rule:
194
+ - your store must emit `destroy` whenever a session is removed, otherwise bridge mapping can get stale
209
195
 
210
- ## Options
196
+ ## Behavior and limitations
211
197
 
212
- `attachSession(app, io, opt)` forwards session options to `koa-session`.
198
+ 1. Session creation is HTTP-first.
199
+ - WebSocket handler does not create missing sessions.
213
200
 
214
- Defaults set by this library when missing:
215
- - `opt.signed = true`
216
- - `opt.key = generateUid(12)`
217
- - `opt.maxAge = 86_400_000`
218
- - `app.keys` auto-generated if missing
201
+ 2. Bridge mapping is in-memory.
202
+ - After process restart, mapping is rebuilt from incoming cookies plus store state.
219
203
 
220
- Optional:
221
- - `opt.store` custom store implementing required API above
222
- - `opt.externalKey` works as in `koa-session`
204
+ 3. Signed cookies depend on stable keys.
205
+ - If `app.keys` change, previously signed cookies become invalid.
223
206
 
224
- ---
207
+ 4. Change detection in WS uses `JSON.stringify`.
208
+ - Non-serializable or cyclic data are not recommended in session payloads.
225
209
 
226
210
  ## Exports
227
211
 
228
212
  ```js
229
- import attachSession, { attachSession, SessionStore, generateUid } from "@randajan/koa-io-session";
213
+ import bridgeSession, {
214
+ bridgeSession,
215
+ SessionBridge,
216
+ SessionStore,
217
+ generateUid
218
+ } from "@randajan/koa-io-session";
230
219
  ```
231
220
 
232
- ---
233
-
234
- ## Production checklist
235
-
236
- 1. Set stable `app.keys` and fixed cookie `opt.key`.
237
- 2. Use persistent store (Redis/DB), not in-memory store.
238
- 3. Call `store.autoCleanup(...)`.
239
- 4. Handle `session:destroy` on client and reconnect via HTTP bootstrap.
240
-
241
- ---
242
-
243
221
  ## License
244
222
 
245
223
  MIT (c) [randajan](https://github.com/randajan)