@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 +109 -131
- package/dist/cjs/index.cjs +350 -199
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/index.mjs +348 -197
- package/dist/esm/index.mjs.map +4 -4
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# @randajan/koa-io-session
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@randajan/koa-io-session)
|
|
3
|
+
[](https://www.npmjs.com/package/@randajan/koa-io-session)
|
|
4
|
+
[](https://standardjs.com)
|
|
4
5
|
|
|
5
6
|
Bridge between `koa-session` and `socket.io` with one shared session store.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
49
|
-
key: "
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
ctx.session.httpCount += 1;
|
|
73
68
|
|
|
74
69
|
ctx.body = {
|
|
75
70
|
ok: true,
|
|
76
|
-
from:
|
|
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,
|
|
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
|
-
|
|
104
|
+
### `bridgeSession(app, io, opt)`
|
|
108
105
|
|
|
109
|
-
|
|
106
|
+
Creates and returns `SessionBridge`.
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
- Session ID resolved from cookie/external key during socket middleware.
|
|
108
|
+
Default export is `bridgeSession`.
|
|
113
109
|
|
|
114
|
-
|
|
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
|
-
|
|
112
|
+
`SessionBridge` extends Node.js `EventEmitter`.
|
|
123
113
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
117
|
+
Events:
|
|
118
|
+
- `sessionStart` payload: `{ clientId, sessionId }`
|
|
119
|
+
- `sessionEnd` payload: `{ clientId, sessionId }`
|
|
132
120
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
143
|
+
- `store`
|
|
144
|
+
- `autoCleanup`
|
|
145
|
+
- `autoCleanupMs`
|
|
146
|
+
- `clientKey`
|
|
147
|
+
- `clientMaxAge`
|
|
148
|
+
- `clientAlwaysRoll`
|
|
169
149
|
|
|
170
|
-
|
|
171
|
-
-
|
|
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
|
-
|
|
174
|
-
-
|
|
160
|
+
Notes:
|
|
161
|
+
- set stable `app.keys` in production
|
|
162
|
+
- keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
|
|
175
163
|
|
|
176
|
-
|
|
177
|
-
- library defines `socket.sessionId` and `socket.withSession`
|
|
164
|
+
## `SessionStore`
|
|
178
165
|
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
-
|
|
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
|
-
|
|
178
|
+
Events emitted by store:
|
|
179
|
+
- `set`
|
|
180
|
+
- `destroy`
|
|
181
|
+
- `cleanup`
|
|
189
182
|
|
|
190
|
-
|
|
191
|
-
- call `store.autoCleanup(interval)` yourself
|
|
183
|
+
## Custom store contract
|
|
192
184
|
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
196
|
+
## Behavior and limitations
|
|
211
197
|
|
|
212
|
-
|
|
198
|
+
1. Session creation is HTTP-first.
|
|
199
|
+
- WebSocket handler does not create missing sessions.
|
|
213
200
|
|
|
214
|
-
|
|
215
|
-
-
|
|
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
|
-
|
|
221
|
-
- `
|
|
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
|
|
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)
|