@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 +119 -129
- package/dist/cjs/index.cjs +360 -199
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/index.mjs +358 -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, 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
|
|
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,137 @@ 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
|
|
121
|
+
Runtime additions:
|
|
122
|
+
- HTTP context: `ctx.clientId`, `ctx.sessionId`
|
|
123
|
+
- Socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler, onMissing?)`
|
|
142
124
|
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
127
|
+
`handler` receives one object:
|
|
128
|
+
- `sessionCtx.sessionId`
|
|
129
|
+
- `sessionCtx.session`
|
|
130
|
+
- `sessionCtx.socket`
|
|
150
131
|
|
|
151
|
-
|
|
152
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
- `store`
|
|
156
|
+
- `autoCleanup`
|
|
157
|
+
- `autoCleanupMs`
|
|
158
|
+
- `clientKey`
|
|
159
|
+
- `clientMaxAge`
|
|
160
|
+
- `clientAlwaysRoll`
|
|
169
161
|
|
|
170
|
-
|
|
171
|
-
-
|
|
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
|
-
|
|
174
|
-
-
|
|
172
|
+
Notes:
|
|
173
|
+
- set stable `app.keys` in production
|
|
174
|
+
- keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
- library defines `socket.sessionId` and `socket.withSession`
|
|
176
|
+
## `SessionStore`
|
|
178
177
|
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
-
|
|
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
|
-
|
|
190
|
+
Events emitted by store:
|
|
191
|
+
- `set`
|
|
192
|
+
- `destroy`
|
|
193
|
+
- `cleanup`
|
|
189
194
|
|
|
190
|
-
|
|
191
|
-
- call `store.autoCleanup(interval)` yourself
|
|
195
|
+
## Custom store contract
|
|
192
196
|
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
208
|
+
## Behavior and limitations
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
1. Session creation is HTTP-first.
|
|
211
|
+
- WebSocket handler does not create missing sessions.
|
|
213
212
|
|
|
214
|
-
|
|
215
|
-
-
|
|
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
|
-
|
|
221
|
-
- `
|
|
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
|
|
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)
|