@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 CHANGED
@@ -3,18 +3,24 @@
3
3
  [![NPM](https://img.shields.io/npm/v/@randajan/koa-io-session.svg)](https://www.npmjs.com/package/@randajan/koa-io-session)
4
4
  [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
5
5
 
6
- Bridge between `koa-session` and `socket.io` with one shared session store.
6
+ Bridge between `koa-session` and `socket.io` with one shared session flow.
7
7
 
8
- ## Why this library
8
+ ## Why
9
9
 
10
- It keeps HTTP and WebSocket session handling in sync while staying close to native `koa-session` behavior.
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 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
+ - 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
- ## Quick start
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: 24 * 60 * 60 * 1000,
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("sessionStart", ({ clientId, sessionId }) => {
49
- console.log("sessionStart", clientId, sessionId);
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("sessionEnd", ({ clientId, sessionId }) => {
53
- console.log("sessionEnd", clientId, sessionId);
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
- 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
- }
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
- `SessionBridge` extends Node.js `EventEmitter`.
113
-
114
- Properties:
115
- - `bridge.store` is the active store instance
120
+ Extends Node.js `EventEmitter`.
116
121
 
117
122
  Events:
118
- - `sessionStart` payload: `{ clientId, sessionId }`
119
- - `sessionEnd` payload: `{ clientId, sessionId }`
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
- - Socket: `socket.clientId`, `socket.sessionId`, `socket.withSession(handler, onMissing?)`
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 one object:
162
+ `handler` receives:
128
163
  - `sessionCtx.sessionId`
129
164
  - `sessionCtx.session`
130
165
  - `sessionCtx.socket`
131
166
 
132
167
  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
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 for same `sessionId` are serialized
171
+ - same-session calls are serialized by `sessionId`
139
172
 
140
173
  `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
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`, except internal bridge keys:
180
+ `opt` is mostly forwarded to `koa-session`, with bridge-specific keys:
154
181
 
155
- - `store`
156
- - `autoCleanup`
157
- - `autoCleanupMs`
158
- - `clientKey`
159
- - `clientMaxAge`
160
- - `clientAlwaysRoll`
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
- Defaults:
163
- - `key`: random `generateUid(12)`
164
- - `maxAge`: `30 days`
192
+ Default behavior:
193
+ - `key`: `"sid"` when missing
165
194
  - `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
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
- Notes:
173
- - set stable `app.keys` in production
174
- - keep cookie settings consistent with your deployment (`sameSite`, `secure`, domain/path)
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
- ## `SessionStore`
205
+ ## Store Contract
177
206
 
178
- Default in-memory store with TTL.
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
- Constructor:
181
- - `new SessionStore({ maxAge, autoCleanup, autoCleanupMs })`
212
+ Optional:
213
+ - `list()` -> required for cleanup features
214
+ - `optimize(clearedCount)` -> called after cleanup if present
182
215
 
183
- Methods:
184
- - `get(sid)`
185
- - `set(sid, session, maxAge?)`
186
- - `destroy(sid)` (also used by `delete`)
187
- - `cleanup()`
188
- - `on(eventName, callback)`
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
- Events emitted by store:
191
- - `set`
192
- - `destroy`
193
- - `cleanup`
223
+ After bridge initialization, direct mutation of `opt.store` is unsupported by default.
194
224
 
195
- ## Custom store contract
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
- Custom store is valid if it implements:
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
- Sync and async implementations are both supported.
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
- Important integration rule:
206
- - your store must emit `destroy` whenever a session is removed, otherwise bridge mapping can get stale
238
+ ## Built-in Stores
207
239
 
208
- ## Behavior and limitations
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 handler does not create missing sessions.
265
+ - WebSocket path does not create missing sessions by itself.
212
266
 
213
- 2. Bridge mapping is in-memory.
214
- - After process restart, mapping is rebuilt from incoming cookies plus store state.
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
- - If `app.keys` change, previously signed cookies become invalid.
270
+ 3. Signed cookies depend on stable `app.keys`.
271
+ - Changing keys invalidates previous signed cookies.
218
272
 
219
- 4. Change detection in WS uses `JSON.stringify`.
220
- - Non-serializable or cyclic data are not recommended in session payloads.
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
- SessionStore,
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)