@randajan/koa-io-session 0.1.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,10 +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
- Simple bridge between `koa-session` and `socket.io`. Shares a unified session across HTTP and WebSocket using a session store.
6
+ Bridge between `koa-session` and `socket.io` with one shared session store.
6
7
 
7
- ---
8
+ ## Why this library
9
+
10
+ It keeps HTTP and WebSocket session handling in sync while staying close to native `koa-session` behavior.
11
+
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`
8
18
 
9
19
  ## Install
10
20
 
@@ -12,53 +22,202 @@ Simple bridge between `koa-session` and `socket.io`. Shares a unified session ac
12
22
  npm i @randajan/koa-io-session
13
23
  ```
14
24
 
15
- ---
16
-
17
- ## Quick use
25
+ ## Quick start
18
26
 
19
27
  ```js
20
28
  import Koa from "koa";
21
- import http from "http";
29
+ import { createServer } from "http";
22
30
  import { Server } from "socket.io";
23
- import { attachSession } from "@randajan/koa-io-session";
31
+ import bridgeSession from "@randajan/koa-io-session";
24
32
 
25
33
  const app = new Koa();
26
- const server = http.createServer(app.callback());
27
- const io = new Server(server);
34
+ const http = createServer(app.callback());
35
+ const io = new Server(http, {
36
+ cors: { origin: true, credentials: true }
37
+ });
28
38
 
29
- const store = attachSession(app, io, {
30
- key: "koa:sess",
39
+ const bridge = bridgeSession(app, io, {
40
+ key: "app.sid",
31
41
  signed: true,
32
- maxAge: 86400000
42
+ maxAge: 24 * 60 * 60 * 1000,
43
+ sameSite: "lax",
44
+ httpOnly: true,
45
+ secure: false
46
+ });
47
+
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
+ });
55
+
56
+ app.use(async (ctx, next) => {
57
+ if (ctx.path !== "/api/session") { return next(); }
58
+
59
+ if (ctx.query.reset === "1") {
60
+ ctx.session = null;
61
+ ctx.body = { ok: true, from: "http:reset" };
62
+ return;
63
+ }
64
+
65
+ if (!ctx.session.createdAt) { ctx.session.createdAt = Date.now(); }
66
+ if (!Number.isFinite(ctx.session.httpCount)) { ctx.session.httpCount = 0; }
67
+ ctx.session.httpCount += 1;
68
+
69
+ ctx.body = {
70
+ ok: true,
71
+ from: "http",
72
+ clientId: ctx.clientId,
73
+ sessionId: ctx.sessionId,
74
+ session: ctx.session
75
+ };
33
76
  });
34
77
 
35
- io.on("connection", socket => {
36
- console.log("session ID:", socket.sessionId);
37
- console.log("session data:", socket.session);
78
+ io.on("connection", (socket) => {
79
+ socket.on("session:get", async (ack) => {
80
+ try {
81
+ const payload = await socket.withSession(async (sessionCtx) => {
82
+ return {
83
+ ok: true,
84
+ from: "ws:get",
85
+ clientId: socket.clientId,
86
+ sessionId: sessionCtx.sessionId,
87
+ session: sessionCtx.session
88
+ };
89
+ });
90
+ if (typeof ack === "function") { ack(payload); }
91
+ } catch (error) {
92
+ if (typeof ack === "function") {
93
+ ack({ ok: false, error: error.message });
94
+ }
95
+ }
96
+ });
38
97
  });
98
+
99
+ http.listen(3000);
39
100
  ```
40
101
 
41
- ---
102
+ ## API
103
+
104
+ ### `bridgeSession(app, io, opt)`
105
+
106
+ Creates and returns `SessionBridge`.
107
+
108
+ Default export is `bridgeSession`.
109
+
110
+ ### `SessionBridge`
111
+
112
+ `SessionBridge` extends Node.js `EventEmitter`.
113
+
114
+ Properties:
115
+ - `bridge.store` is the active store instance
116
+
117
+ Events:
118
+ - `sessionStart` payload: `{ clientId, sessionId }`
119
+ - `sessionEnd` payload: `{ clientId, sessionId }`
120
+
121
+ Runtime additions:
122
+ - HTTP context: `ctx.clientId`, `ctx.sessionId`
123
+ - Socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler)`
42
124
 
43
- ## Socket helpers
125
+ ### `socket.withSession(handler)`
44
126
 
45
- - `socket.sessionId` session ID from cookies
46
- - `socket.session` → session object from store
127
+ `handler` receives one object:
128
+ - `sessionCtx.sessionId`
129
+ - `sessionCtx.session`
130
+ - `sessionCtx.socket`
47
131
 
48
- ---
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
49
138
 
50
- ## Production notes
139
+ ## Options
51
140
 
52
- - **Stable signing keys**: Provide your own `app.keys` and a fixed `opt.key` (cookie name).
53
- Using randomly generated values on every server restart will invalidate existing signed cookies and force users to log in again.
54
- - **Persistent stores for production**: The bundled in‑memory store works only for local development because all sessions disappear when the process restarts.
55
- Configure a persistent store such as Redis, DynamoDB, or SQL for real deployments.
56
- - **Proxy deep‑mutation limitation**: The session proxy tracks changes only on top‑level properties.
57
- If you mutate nested objects you must either replace the whole object or use immutable updates so that changes are picked up and persisted.
58
- - **Middleware order matters**: Call `attachSession` *before* any middleware (Router, authentication, etc.) that expects `ctx.session` to exist.
141
+ `opt` is mostly forwarded to `koa-session`, except internal bridge keys:
59
142
 
143
+ - `store`
144
+ - `autoCleanup`
145
+ - `autoCleanupMs`
146
+ - `clientKey`
147
+ - `clientMaxAge`
148
+ - `clientAlwaysRoll`
60
149
 
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
159
+
160
+ Notes:
161
+ - set stable `app.keys` in production
162
+ - keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
163
+
164
+ ## `SessionStore`
165
+
166
+ Default in-memory store with TTL.
167
+
168
+ Constructor:
169
+ - `new SessionStore({ maxAge, autoCleanup, autoCleanupMs })`
170
+
171
+ Methods:
172
+ - `get(sid)`
173
+ - `set(sid, session, maxAge?)`
174
+ - `destroy(sid)` (also used by `delete`)
175
+ - `cleanup()`
176
+ - `on(eventName, callback)`
177
+
178
+ Events emitted by store:
179
+ - `set`
180
+ - `destroy`
181
+ - `cleanup`
182
+
183
+ ## Custom store contract
184
+
185
+ Custom store is valid if it implements:
186
+ - `get(sid)`
187
+ - `set(sid, session, maxAge?)`
188
+ - `destroy(sid)`
189
+ - `on(eventName, callback)`
190
+
191
+ Sync and async implementations are both supported.
192
+
193
+ Important integration rule:
194
+ - your store must emit `destroy` whenever a session is removed, otherwise bridge mapping can get stale
195
+
196
+ ## Behavior and limitations
197
+
198
+ 1. Session creation is HTTP-first.
199
+ - WebSocket handler does not create missing sessions.
200
+
201
+ 2. Bridge mapping is in-memory.
202
+ - After process restart, mapping is rebuilt from incoming cookies plus store state.
203
+
204
+ 3. Signed cookies depend on stable keys.
205
+ - If `app.keys` change, previously signed cookies become invalid.
206
+
207
+ 4. Change detection in WS uses `JSON.stringify`.
208
+ - Non-serializable or cyclic data are not recommended in session payloads.
209
+
210
+ ## Exports
211
+
212
+ ```js
213
+ import bridgeSession, {
214
+ bridgeSession,
215
+ SessionBridge,
216
+ SessionStore,
217
+ generateUid
218
+ } from "@randajan/koa-io-session";
219
+ ```
61
220
 
62
221
  ## License
63
222
 
64
- MIT © [randajan](https://github.com/randajan)
223
+ MIT (c) [randajan](https://github.com/randajan)