@randajan/koa-io-session 2.1.0 → 3.0.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 +162 -96
- package/dist/cjs/index.cjs +399 -199
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/stores/FileStore.cjs +117 -0
- package/dist/cjs/stores/FileStore.cjs.map +7 -0
- package/dist/esm/chunk-27XQ42ES.js +98 -0
- package/dist/esm/chunk-27XQ42ES.js.map +7 -0
- package/dist/esm/index.mjs +369 -241
- package/dist/esm/index.mjs.map +4 -4
- package/dist/esm/stores/FileStore.mjs +66 -0
- package/dist/esm/stores/FileStore.mjs.map +7 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@randajan/koa-io-session)
|
|
4
4
|
[](https://standardjs.com)
|
|
5
5
|
|
|
6
|
-
Bridge between `koa-session` and `socket.io` with one shared session
|
|
6
|
+
Bridge between `koa-session` and `socket.io` with one shared session flow.
|
|
7
7
|
|
|
8
|
-
## Why
|
|
8
|
+
## Why
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
This library keeps HTTP and WebSocket session work synchronized while preserving native `koa-session` behavior.
|
|
11
11
|
|
|
12
12
|
You get:
|
|
13
|
-
- standard `ctx.session` in HTTP
|
|
14
|
-
- `ctx.clientId` and `socket.clientId`
|
|
15
|
-
- `ctx.sessionId` and `socket.sessionId` resolved
|
|
16
|
-
- `socket.withSession(handler)`
|
|
17
|
-
- bridge
|
|
13
|
+
- standard `ctx.session` in HTTP
|
|
14
|
+
- `ctx.clientId` and `socket.clientId`
|
|
15
|
+
- `ctx.sessionId` and `socket.sessionId` resolved through bridge mapping
|
|
16
|
+
- `socket.withSession(handler, onMissing?)` helper
|
|
17
|
+
- bridge events: `sessionSet`, `sessionDestroy`, `cleanup`
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
Public API is `SessionBridge`.
|
|
22
|
+
|
|
23
|
+
Internally, bridge uses a private store layer for TTL/event consistency over your backend `store` (`LiveStore` / `FileStore` / custom).
|
|
18
24
|
|
|
19
25
|
## Install
|
|
20
26
|
|
|
@@ -22,7 +28,13 @@ You get:
|
|
|
22
28
|
npm i @randajan/koa-io-session
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
For persistent file store:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i @randajan/file-db
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
26
38
|
|
|
27
39
|
```js
|
|
28
40
|
import Koa from "koa";
|
|
@@ -31,6 +43,10 @@ import { Server } from "socket.io";
|
|
|
31
43
|
import bridgeSession from "@randajan/koa-io-session";
|
|
32
44
|
|
|
33
45
|
const app = new Koa();
|
|
46
|
+
|
|
47
|
+
// Keep keys stable in production (important for signed cookies after restart)
|
|
48
|
+
app.keys = ["your-stable-key-1", "your-stable-key-2"];
|
|
49
|
+
|
|
34
50
|
const http = createServer(app.callback());
|
|
35
51
|
const io = new Server(http, {
|
|
36
52
|
cors: { origin: true, credentials: true }
|
|
@@ -39,18 +55,22 @@ const io = new Server(http, {
|
|
|
39
55
|
const bridge = bridgeSession(app, io, {
|
|
40
56
|
key: "app.sid",
|
|
41
57
|
signed: true,
|
|
42
|
-
maxAge:
|
|
58
|
+
maxAge: 1000 * 60 * 60 * 24,
|
|
43
59
|
sameSite: "lax",
|
|
44
60
|
httpOnly: true,
|
|
45
61
|
secure: false
|
|
46
62
|
});
|
|
47
63
|
|
|
48
|
-
bridge.on("
|
|
49
|
-
console.log("
|
|
64
|
+
bridge.on("sessionSet", ({ clientId, sessionId, isNew, isInit }) => {
|
|
65
|
+
console.log("sessionSet", { clientId, sessionId, isNew, isInit });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
bridge.on("sessionDestroy", ({ clientId, sessionId }) => {
|
|
69
|
+
console.log("sessionDestroy", { clientId, sessionId });
|
|
50
70
|
});
|
|
51
71
|
|
|
52
|
-
bridge.on("
|
|
53
|
-
console.log("
|
|
72
|
+
bridge.on("cleanup", (cleared) => {
|
|
73
|
+
console.log("cleanup", cleared);
|
|
54
74
|
});
|
|
55
75
|
|
|
56
76
|
app.use(async (ctx, next) => {
|
|
@@ -68,7 +88,6 @@ app.use(async (ctx, next) => {
|
|
|
68
88
|
|
|
69
89
|
ctx.body = {
|
|
70
90
|
ok: true,
|
|
71
|
-
from: "http",
|
|
72
91
|
clientId: ctx.clientId,
|
|
73
92
|
sessionId: ctx.sessionId,
|
|
74
93
|
session: ctx.session
|
|
@@ -77,22 +96,13 @@ app.use(async (ctx, next) => {
|
|
|
77
96
|
|
|
78
97
|
io.on("connection", (socket) => {
|
|
79
98
|
socket.on("session:get", async (ack) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
}
|
|
99
|
+
const payload = await socket.withSession((sessionCtx) => ({
|
|
100
|
+
ok: true,
|
|
101
|
+
sessionId: sessionCtx.sessionId,
|
|
102
|
+
session: sessionCtx.session
|
|
103
|
+
}), { ok: false, error: "missing-session" });
|
|
104
|
+
|
|
105
|
+
if (typeof ack === "function") { ack(payload); }
|
|
96
106
|
});
|
|
97
107
|
});
|
|
98
108
|
|
|
@@ -105,119 +115,175 @@ http.listen(3000);
|
|
|
105
115
|
|
|
106
116
|
Creates and returns `SessionBridge`.
|
|
107
117
|
|
|
108
|
-
Default export is `bridgeSession`.
|
|
109
|
-
|
|
110
118
|
### `SessionBridge`
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Properties:
|
|
115
|
-
- `bridge.store` is the active store instance
|
|
120
|
+
Extends Node.js `EventEmitter`.
|
|
116
121
|
|
|
117
122
|
Events:
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
123
|
+
- `sessionSet`: `{ clientId, sessionId, isNew, isInit }`
|
|
124
|
+
- `sessionDestroy`: `{ clientId, sessionId }`
|
|
125
|
+
- `cleanup`: `clearedCount` (number of expired sessions removed)
|
|
126
|
+
|
|
127
|
+
`sessionSet` flags:
|
|
128
|
+
- `isNew`: backend store reported creation of a new persisted session record (`sid` had no previous state)
|
|
129
|
+
- `isInit`: bridge just initialized/attached mapping in current process lifecycle (`clientId <-> sessionId`)
|
|
130
|
+
- typical combinations:
|
|
131
|
+
- `isNew: true`, `isInit: true` -> newly created session was attached now
|
|
132
|
+
- `isNew: false`, `isInit: true` -> existing session was attached now (for example after process restart)
|
|
133
|
+
- `isNew: false`, `isInit: false` -> already attached session was updated
|
|
120
134
|
|
|
121
135
|
Runtime additions:
|
|
122
136
|
- HTTP context: `ctx.clientId`, `ctx.sessionId`
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
### `socket.withSession(handler)`
|
|
137
|
+
- socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler, onMissing?)`
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
Methods:
|
|
140
|
+
- `getSessionId(clientId): string | undefined`
|
|
141
|
+
- `getClientId(sessionId): string | undefined`
|
|
142
|
+
- `getById(sessionId): Promise<object | undefined>`
|
|
143
|
+
- `getByClientId(clientId): Promise<object | undefined>`
|
|
144
|
+
- `destroyById(sessionId): Promise<boolean>`
|
|
145
|
+
- `destroyByClientId(clientId): Promise<boolean>`
|
|
146
|
+
- `setById(sessionId, session, maxAge?): Promise<boolean>` (cannot create missing session)
|
|
147
|
+
- `setByClientId(clientId, session, maxAge?): Promise<boolean>` (cannot create missing session)
|
|
148
|
+
- `cleanup(): Promise<number>`
|
|
149
|
+
- `startAutoCleanup(interval?): boolean`
|
|
150
|
+
- `stopAutoCleanup(): boolean`
|
|
151
|
+
- `notifyStoreSet(sessionId, isNew?): void`
|
|
152
|
+
- `notifyStoreDestroy(sessionId): void`
|
|
153
|
+
- `notifyStoreCleanup(clearedCount): void`
|
|
154
|
+
|
|
155
|
+
Missing policy:
|
|
156
|
+
- `getBy*` on missing mapping: returns `undefined`
|
|
157
|
+
- `destroyBy*` on missing mapping: returns `false`
|
|
158
|
+
- `setBy*` on missing mapping: throws `Error` (creating via this path is prohibited)
|
|
159
|
+
|
|
160
|
+
### `socket.withSession(handler, onMissing?)`
|
|
161
|
+
|
|
162
|
+
`handler` receives:
|
|
128
163
|
- `sessionCtx.sessionId`
|
|
129
164
|
- `sessionCtx.session`
|
|
130
165
|
- `sessionCtx.socket`
|
|
131
166
|
|
|
132
167
|
Rules:
|
|
133
|
-
-
|
|
134
|
-
- throws `"Session not found"` if store does not have the session
|
|
168
|
+
- default `onMissing` is error (`Session is missing for this socket`)
|
|
135
169
|
- if `sessionCtx.session = null`, session is destroyed
|
|
136
170
|
- if session changed, store `set` is called
|
|
137
|
-
- calls
|
|
171
|
+
- same-session calls are serialized by `sessionId`
|
|
172
|
+
|
|
173
|
+
`onMissing` behavior:
|
|
174
|
+
- `Error` -> throw
|
|
175
|
+
- `function` -> call and return its value
|
|
176
|
+
- any other value -> return as fallback
|
|
138
177
|
|
|
139
178
|
## Options
|
|
140
179
|
|
|
141
|
-
`opt` is mostly forwarded to `koa-session`,
|
|
180
|
+
`opt` is mostly forwarded to `koa-session`, with bridge-specific keys:
|
|
142
181
|
|
|
143
|
-
- `store`
|
|
144
|
-
- `
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
182
|
+
- `store` (backend store implementation)
|
|
183
|
+
- `maxAge` (session TTL used by StoreGateway and koa cookie)
|
|
184
|
+
- `autoCleanup` (default `false`)
|
|
185
|
+
- `autoCleanupMs` (used only when `autoCleanup === true`)
|
|
186
|
+
- `clientKey` (default `${key}.cid`)
|
|
187
|
+
- `clientMaxAge` (default `1 year`)
|
|
188
|
+
- `clientAlwaysRoll` (default `true`)
|
|
149
189
|
|
|
150
|
-
|
|
151
|
-
- `key`: random
|
|
152
|
-
- `maxAge`: `30 days`
|
|
190
|
+
Default behavior:
|
|
191
|
+
- `key`: random generated when missing
|
|
153
192
|
- `signed`: `true`
|
|
154
|
-
- `store`: `new
|
|
155
|
-
- `
|
|
156
|
-
- `
|
|
157
|
-
- `clientAlwaysRoll`: `true`
|
|
158
|
-
- `app.keys`: auto-generated if missing
|
|
193
|
+
- `store`: `new LiveStore()`
|
|
194
|
+
- `app.keys`: auto-generated if missing (recommended to set manually in production)
|
|
195
|
+
- `autoCleanupMs`: when omitted and `autoCleanup` is enabled, interval is computed as `maxAge / 4`, clamped to `<1 minute, 1 day>`
|
|
159
196
|
|
|
160
|
-
|
|
161
|
-
- set stable `app.keys` in production
|
|
162
|
-
- keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
|
|
197
|
+
## Store Contract
|
|
163
198
|
|
|
164
|
-
|
|
199
|
+
Backend `store` must implement:
|
|
200
|
+
- `get(sid)` -> returns stored state or `undefined`
|
|
201
|
+
- `set(sid, state)` -> returns boolean (or truthy)
|
|
202
|
+
- `destroy(sid)` -> returns boolean
|
|
165
203
|
|
|
166
|
-
|
|
204
|
+
Optional:
|
|
205
|
+
- `list()` -> required for cleanup features
|
|
206
|
+
- `optimize(clearedCount)` -> called after cleanup if present
|
|
167
207
|
|
|
168
|
-
|
|
169
|
-
- `
|
|
208
|
+
Stored state format expected by gateway:
|
|
209
|
+
- `{ session, expiresAt, ttl }` where `session` is JSON string (serialized session object)
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- `on(eventName, callback)`
|
|
211
|
+
Both sync and async store methods are supported.
|
|
212
|
+
|
|
213
|
+
## Consistency Rule (Important)
|
|
214
|
+
|
|
215
|
+
After bridge initialization, direct mutation of `opt.store` is unsupported by default.
|
|
177
216
|
|
|
178
|
-
|
|
179
|
-
-
|
|
180
|
-
- `
|
|
181
|
-
-
|
|
217
|
+
Why:
|
|
218
|
+
- it bypasses gateway/bridge consistency flow
|
|
219
|
+
- it can break `clientId <-> sessionId` synchronization
|
|
220
|
+
- it can cause missing or misleading bridge events
|
|
182
221
|
|
|
183
|
-
|
|
222
|
+
Use `SessionBridge` methods (`setBy*`, `destroyBy*`, `cleanup`) for controlled mutations.
|
|
184
223
|
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
- `
|
|
188
|
-
- `
|
|
189
|
-
- `
|
|
224
|
+
Advanced bypass (you take full responsibility):
|
|
225
|
+
- if you intentionally mutate backend store directly, call matching notify method right after each mutation:
|
|
226
|
+
- `notifyStoreSet(sessionId, isNew?)`
|
|
227
|
+
- `notifyStoreDestroy(sessionId)`
|
|
228
|
+
- `notifyStoreCleanup(clearedCount)`
|
|
190
229
|
|
|
191
|
-
|
|
230
|
+
## Built-in Stores
|
|
192
231
|
|
|
193
|
-
|
|
194
|
-
- your store must emit `destroy` whenever a session is removed, otherwise bridge mapping can get stale
|
|
232
|
+
### `LiveStore`
|
|
195
233
|
|
|
196
|
-
|
|
234
|
+
In-memory backend store.
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
import bridgeSession, { LiveStore } from "@randajan/koa-io-session";
|
|
238
|
+
|
|
239
|
+
const bridge = bridgeSession(app, io, {
|
|
240
|
+
store: new LiveStore()
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `FileStore` (persistent, `@randajan/file-db`)
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import { FileStore } from "@randajan/koa-io-session/fdb";
|
|
248
|
+
|
|
249
|
+
const bridge = bridgeSession(app, io, {
|
|
250
|
+
store: new FileStore({ fileName: "sessions" })
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Behavior and Limitations
|
|
197
255
|
|
|
198
256
|
1. Session creation is HTTP-first.
|
|
199
|
-
- WebSocket
|
|
257
|
+
- WebSocket path does not create missing sessions by itself.
|
|
200
258
|
|
|
201
|
-
2.
|
|
202
|
-
- After process restart, mapping is rebuilt from incoming cookies
|
|
259
|
+
2. Mapping (`clientId <-> sessionId`) is in-memory.
|
|
260
|
+
- After process restart, mapping is rebuilt from incoming cookies and existing store state.
|
|
203
261
|
|
|
204
|
-
3. Signed cookies depend on stable keys
|
|
205
|
-
-
|
|
262
|
+
3. Signed cookies depend on stable `app.keys`.
|
|
263
|
+
- Changing keys invalidates previous signed cookies.
|
|
206
264
|
|
|
207
|
-
4.
|
|
208
|
-
- Non-serializable
|
|
265
|
+
4. WS change detection uses `JSON.stringify`.
|
|
266
|
+
- Non-serializable/cyclic payloads are not recommended in session data.
|
|
209
267
|
|
|
210
268
|
## Exports
|
|
211
269
|
|
|
270
|
+
Main entry:
|
|
271
|
+
|
|
212
272
|
```js
|
|
213
273
|
import bridgeSession, {
|
|
214
274
|
bridgeSession,
|
|
215
275
|
SessionBridge,
|
|
216
|
-
|
|
276
|
+
LiveStore,
|
|
217
277
|
generateUid
|
|
218
278
|
} from "@randajan/koa-io-session";
|
|
219
279
|
```
|
|
220
280
|
|
|
281
|
+
Persistent file store entry:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import { FileStore } from "@randajan/koa-io-session/fdb";
|
|
285
|
+
```
|
|
286
|
+
|
|
221
287
|
## License
|
|
222
288
|
|
|
223
289
|
MIT (c) [randajan](https://github.com/randajan)
|