@randajan/koa-io-session 2.2.0 → 3.0.1
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 +166 -104
- package/dist/cjs/index.cjs +433 -209
- 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-NKL2ZYZW.js +110 -0
- package/dist/esm/chunk-NKL2ZYZW.js.map +7 -0
- package/dist/esm/index.mjs +393 -251
- 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, onMissing?)`
|
|
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,131 +115,183 @@ 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
|
-
-
|
|
137
|
+
- socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler, onMissing?)`
|
|
138
|
+
|
|
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)
|
|
124
159
|
|
|
125
160
|
### `socket.withSession(handler, onMissing?)`
|
|
126
161
|
|
|
127
|
-
`handler` receives
|
|
162
|
+
`handler` receives:
|
|
128
163
|
- `sessionCtx.sessionId`
|
|
129
164
|
- `sessionCtx.session`
|
|
130
165
|
- `sessionCtx.socket`
|
|
131
166
|
|
|
132
167
|
Rules:
|
|
133
|
-
- default
|
|
134
|
-
- missing session means `socket.sessionId` is missing
|
|
135
|
-
- missing session means store does not have session for current sid
|
|
168
|
+
- default `onMissing` is error (`Session is missing for this socket`)
|
|
136
169
|
- if `sessionCtx.session = null`, session is destroyed
|
|
137
170
|
- if session changed, store `set` is called
|
|
138
|
-
- calls
|
|
171
|
+
- same-session calls are serialized by `sessionId`
|
|
139
172
|
|
|
140
173
|
`onMissing` behavior:
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
|
|
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 }))`
|
|
174
|
+
- `Error` -> throw
|
|
175
|
+
- `function` -> call and return its value
|
|
176
|
+
- any other value -> return as fallback
|
|
150
177
|
|
|
151
178
|
## Options
|
|
152
179
|
|
|
153
|
-
`opt` is mostly forwarded to `koa-session`,
|
|
180
|
+
`opt` is mostly forwarded to `koa-session`, with bridge-specific keys:
|
|
154
181
|
|
|
155
|
-
- `
|
|
156
|
-
- `
|
|
157
|
-
- `
|
|
158
|
-
- `
|
|
159
|
-
- `
|
|
160
|
-
- `
|
|
182
|
+
- `appKeys` (optional array used to initialize `app.keys`)
|
|
183
|
+
- `allowRndAppKeys` (default `false`; suppress runtime warning when keys are generated)
|
|
184
|
+
- `store` (backend store implementation)
|
|
185
|
+
- `maxAge` (session TTL used by StoreGateway and koa cookie)
|
|
186
|
+
- `autoCleanup` (default `false`)
|
|
187
|
+
- `autoCleanupMs` (used only when `autoCleanup === true`)
|
|
188
|
+
- `clientKey` (default `"cid"`)
|
|
189
|
+
- `clientMaxAge` (default `1 year`)
|
|
190
|
+
- `clientAlwaysRoll` (default `true`)
|
|
161
191
|
|
|
162
|
-
|
|
163
|
-
- `key`:
|
|
164
|
-
- `maxAge`: `30 days`
|
|
192
|
+
Default behavior:
|
|
193
|
+
- `key`: `"sid"` when missing
|
|
165
194
|
- `signed`: `true`
|
|
166
|
-
- `store`: `new
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
169
|
-
- `clientAlwaysRoll`: `true`
|
|
170
|
-
- `app.keys`: auto-generated if missing
|
|
195
|
+
- `store`: `new LiveStore()`
|
|
196
|
+
- `app.keys`: if missing, bridge generates 2 runtime keys (length 32) and logs warning
|
|
197
|
+
- `autoCleanupMs`: when omitted and `autoCleanup` is enabled, interval is computed as `maxAge / 4`, clamped to `<1 minute, 1 day>`
|
|
171
198
|
|
|
172
|
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
199
|
+
`app.keys` resolution:
|
|
200
|
+
- if `app.keys` already exists and `appKeys` is not provided: existing `app.keys` is used
|
|
201
|
+
- if `app.keys` already exists and `appKeys` is provided: throws error
|
|
202
|
+
- if `app.keys` is missing and `appKeys` is provided: `app.keys = appKeys`
|
|
203
|
+
- if both are missing: bridge generates runtime keys; warning is shown unless `allowRndAppKeys === true`
|
|
175
204
|
|
|
176
|
-
##
|
|
205
|
+
## Store Contract
|
|
177
206
|
|
|
178
|
-
|
|
207
|
+
Backend `store` must implement:
|
|
208
|
+
- `get(sid)` -> returns stored state or `undefined`
|
|
209
|
+
- `set(sid, state)` -> returns boolean (or truthy)
|
|
210
|
+
- `destroy(sid)` -> returns boolean
|
|
179
211
|
|
|
180
|
-
|
|
181
|
-
- `
|
|
212
|
+
Optional:
|
|
213
|
+
- `list()` -> required for cleanup features
|
|
214
|
+
- `optimize(clearedCount)` -> called after cleanup if present
|
|
182
215
|
|
|
183
|
-
|
|
184
|
-
- `
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
Stored state format expected by gateway:
|
|
217
|
+
- `{ session, expiresAt, ttl }` where `session` is JSON string (serialized session object)
|
|
218
|
+
|
|
219
|
+
Both sync and async store methods are supported.
|
|
220
|
+
|
|
221
|
+
## Consistency Rule (Important)
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
- `set`
|
|
192
|
-
- `destroy`
|
|
193
|
-
- `cleanup`
|
|
223
|
+
After bridge initialization, direct mutation of `opt.store` is unsupported by default.
|
|
194
224
|
|
|
195
|
-
|
|
225
|
+
Why:
|
|
226
|
+
- it bypasses gateway/bridge consistency flow
|
|
227
|
+
- it can break `clientId <-> sessionId` synchronization
|
|
228
|
+
- it can cause missing or misleading bridge events
|
|
196
229
|
|
|
197
|
-
|
|
198
|
-
- `get(sid)`
|
|
199
|
-
- `set(sid, session, maxAge?)`
|
|
200
|
-
- `destroy(sid)`
|
|
201
|
-
- `on(eventName, callback)`
|
|
230
|
+
Use `SessionBridge` methods (`setBy*`, `destroyBy*`, `cleanup`) for controlled mutations.
|
|
202
231
|
|
|
203
|
-
|
|
232
|
+
Advanced bypass (you take full responsibility):
|
|
233
|
+
- if you intentionally mutate backend store directly, call matching notify method right after each mutation:
|
|
234
|
+
- `notifyStoreSet(sessionId, isNew?)`
|
|
235
|
+
- `notifyStoreDestroy(sessionId)`
|
|
236
|
+
- `notifyStoreCleanup(clearedCount)`
|
|
204
237
|
|
|
205
|
-
|
|
206
|
-
- your store must emit `destroy` whenever a session is removed, otherwise bridge mapping can get stale
|
|
238
|
+
## Built-in Stores
|
|
207
239
|
|
|
208
|
-
|
|
240
|
+
### `LiveStore`
|
|
241
|
+
|
|
242
|
+
In-memory backend store.
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
import bridgeSession, { LiveStore } from "@randajan/koa-io-session";
|
|
246
|
+
|
|
247
|
+
const bridge = bridgeSession(app, io, {
|
|
248
|
+
store: new LiveStore()
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `FileStore` (persistent, `@randajan/file-db`)
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
import { FileStore } from "@randajan/koa-io-session/fdb";
|
|
256
|
+
|
|
257
|
+
const bridge = bridgeSession(app, io, {
|
|
258
|
+
store: new FileStore({ fileName: "sessions" })
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Behavior and Limitations
|
|
209
263
|
|
|
210
264
|
1. Session creation is HTTP-first.
|
|
211
|
-
- WebSocket
|
|
265
|
+
- WebSocket path does not create missing sessions by itself.
|
|
212
266
|
|
|
213
|
-
2.
|
|
214
|
-
- After process restart, mapping is rebuilt from incoming cookies
|
|
267
|
+
2. Mapping (`clientId <-> sessionId`) is in-memory.
|
|
268
|
+
- After process restart, mapping is rebuilt from incoming cookies and existing store state.
|
|
215
269
|
|
|
216
|
-
3. Signed cookies depend on stable keys
|
|
217
|
-
-
|
|
270
|
+
3. Signed cookies depend on stable `app.keys`.
|
|
271
|
+
- Changing keys invalidates previous signed cookies.
|
|
218
272
|
|
|
219
|
-
4.
|
|
220
|
-
- Non-serializable
|
|
273
|
+
4. WS change detection uses `JSON.stringify`.
|
|
274
|
+
- Non-serializable/cyclic payloads are not recommended in session data.
|
|
221
275
|
|
|
222
276
|
## Exports
|
|
223
277
|
|
|
278
|
+
Main entry:
|
|
279
|
+
|
|
224
280
|
```js
|
|
225
281
|
import bridgeSession, {
|
|
226
282
|
bridgeSession,
|
|
227
283
|
SessionBridge,
|
|
228
|
-
|
|
284
|
+
LiveStore,
|
|
229
285
|
generateUid
|
|
230
286
|
} from "@randajan/koa-io-session";
|
|
231
287
|
```
|
|
232
288
|
|
|
289
|
+
Persistent file store entry:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import { FileStore } from "@randajan/koa-io-session/fdb";
|
|
293
|
+
```
|
|
294
|
+
|
|
233
295
|
## License
|
|
234
296
|
|
|
235
297
|
MIT (c) [randajan](https://github.com/randajan)
|