@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 +189 -30
- package/dist/cjs/index.cjs +409 -65
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/index.mjs +408 -64
- package/dist/esm/index.mjs.map +4 -4
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,10 +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
|
+
## 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
|
|
29
|
+
import { createServer } from "http";
|
|
22
30
|
import { Server } from "socket.io";
|
|
23
|
-
import
|
|
31
|
+
import bridgeSession from "@randajan/koa-io-session";
|
|
24
32
|
|
|
25
33
|
const app = new Koa();
|
|
26
|
-
const
|
|
27
|
-
const io = new Server(
|
|
34
|
+
const http = createServer(app.callback());
|
|
35
|
+
const io = new Server(http, {
|
|
36
|
+
cors: { origin: true, credentials: true }
|
|
37
|
+
});
|
|
28
38
|
|
|
29
|
-
const
|
|
30
|
-
key: "
|
|
39
|
+
const bridge = bridgeSession(app, io, {
|
|
40
|
+
key: "app.sid",
|
|
31
41
|
signed: true,
|
|
32
|
-
maxAge:
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
125
|
+
### `socket.withSession(handler)`
|
|
44
126
|
|
|
45
|
-
|
|
46
|
-
- `
|
|
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
|
-
##
|
|
139
|
+
## Options
|
|
51
140
|
|
|
52
|
-
|
|
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
|
|
223
|
+
MIT (c) [randajan](https://github.com/randajan)
|