@randajan/koa-io-session 1.0.0 → 2.2.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, onMissing?)` for strict or optional WS session handling
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,137 @@ 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
121
+ Runtime additions:
122
+ - HTTP context: `ctx.clientId`, `ctx.sessionId`
123
+ - Socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler, onMissing?)`
142
124
 
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.
125
+ ### `socket.withSession(handler, onMissing?)`
146
126
 
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.
127
+ `handler` receives one object:
128
+ - `sessionCtx.sessionId`
129
+ - `sessionCtx.session`
130
+ - `sessionCtx.socket`
150
131
 
151
- 3. Concurrency:
152
- - Operations for same `sessionId` are serialized to avoid race conditions.
132
+ Rules:
133
+ - default behavior (`onMissing` not provided): throws `Error("Session missing")`
134
+ - missing session means `socket.sessionId` is missing
135
+ - missing session means store does not have session for current sid
136
+ - if `sessionCtx.session = null`, session is destroyed
137
+ - if session changed, store `set` is called
138
+ - calls for same `sessionId` are serialized
153
139
 
154
- ---
140
+ `onMissing` behavior:
141
+ - if `onMissing` is an `Error`, it is thrown
142
+ - if `onMissing` is a function, its return value is used
143
+ - otherwise, `onMissing` value is returned as-is
155
144
 
156
- ## Destroy flow
145
+ Examples:
146
+ - strict (default): `await socket.withSession(handler)`
147
+ - silent undefined on missing: `await socket.withSession(handler, undefined)`
148
+ - custom fallback value: `await socket.withSession(handler, { ok: false })`
149
+ - custom fallback callback: `await socket.withSession(handler, () => ({ ok: false }))`
157
150
 
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
151
+ ## Options
165
152
 
166
- ---
153
+ `opt` is mostly forwarded to `koa-session`, except internal bridge keys:
167
154
 
168
- ## Reserved names and limitations
155
+ - `store`
156
+ - `autoCleanup`
157
+ - `autoCleanupMs`
158
+ - `clientKey`
159
+ - `clientMaxAge`
160
+ - `clientAlwaysRoll`
169
161
 
170
- 1. Room naming:
171
- - library joins sockets into room `sessionId:<sid>`
162
+ Defaults:
163
+ - `key`: random `generateUid(12)`
164
+ - `maxAge`: `30 days`
165
+ - `signed`: `true`
166
+ - `store`: `new SessionStore(opt)`
167
+ - `clientKey`: `${key}.cid`
168
+ - `clientMaxAge`: `1 year`
169
+ - `clientAlwaysRoll`: `true`
170
+ - `app.keys`: auto-generated if missing
172
171
 
173
- 2. Reserved socket event:
174
- - library emits `session:destroy`
172
+ Notes:
173
+ - set stable `app.keys` in production
174
+ - keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
175
175
 
176
- 3. Reserved socket properties:
177
- - library defines `socket.sessionId` and `socket.withSession`
176
+ ## `SessionStore`
178
177
 
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
178
+ Default in-memory store with TTL.
182
179
 
183
- 5. Session ID is fixed for current connection:
184
- - if cookie/session changes, existing socket must reconnect to use new SID
180
+ Constructor:
181
+ - `new SessionStore({ maxAge, autoCleanup, autoCleanupMs })`
185
182
 
186
- ---
183
+ Methods:
184
+ - `get(sid)`
185
+ - `set(sid, session, maxAge?)`
186
+ - `destroy(sid)` (also used by `delete`)
187
+ - `cleanup()`
188
+ - `on(eventName, callback)`
187
189
 
188
- ## Required integration details
190
+ Events emitted by store:
191
+ - `set`
192
+ - `destroy`
193
+ - `cleanup`
189
194
 
190
- 1. Cleanup is manual:
191
- - call `store.autoCleanup(interval)` yourself
195
+ ## Custom store contract
192
196
 
193
- 2. Custom store API is required:
197
+ Custom store is valid if it implements:
194
198
  - `get(sid)`
195
- - `set(sid, session, maxAge)`
199
+ - `set(sid, session, maxAge?)`
196
200
  - `destroy(sid)`
197
- - `touch(sid, maxAge)`
198
201
  - `on(eventName, callback)`
199
202
 
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
203
+ Sync and async implementations are both supported.
207
204
 
208
- ---
205
+ Important integration rule:
206
+ - your store must emit `destroy` whenever a session is removed, otherwise bridge mapping can get stale
209
207
 
210
- ## Options
208
+ ## Behavior and limitations
211
209
 
212
- `attachSession(app, io, opt)` forwards session options to `koa-session`.
210
+ 1. Session creation is HTTP-first.
211
+ - WebSocket handler does not create missing sessions.
213
212
 
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
213
+ 2. Bridge mapping is in-memory.
214
+ - After process restart, mapping is rebuilt from incoming cookies plus store state.
219
215
 
220
- Optional:
221
- - `opt.store` custom store implementing required API above
222
- - `opt.externalKey` works as in `koa-session`
216
+ 3. Signed cookies depend on stable keys.
217
+ - If `app.keys` change, previously signed cookies become invalid.
223
218
 
224
- ---
219
+ 4. Change detection in WS uses `JSON.stringify`.
220
+ - Non-serializable or cyclic data are not recommended in session payloads.
225
221
 
226
222
  ## Exports
227
223
 
228
224
  ```js
229
- import attachSession, { attachSession, SessionStore, generateUid } from "@randajan/koa-io-session";
225
+ import bridgeSession, {
226
+ bridgeSession,
227
+ SessionBridge,
228
+ SessionStore,
229
+ generateUid
230
+ } from "@randajan/koa-io-session";
230
231
  ```
231
232
 
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
233
  ## License
244
234
 
245
235
  MIT (c) [randajan](https://github.com/randajan)