@essentialai/cogent-server 2.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.
Files changed (166) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +16 -0
  3. package/Caddyfile +8 -0
  4. package/Dockerfile +46 -0
  5. package/LICENSE +190 -0
  6. package/README.md +89 -0
  7. package/config.json.example +16 -0
  8. package/dist/__tests__/helpers.d.ts +56 -0
  9. package/dist/__tests__/helpers.d.ts.map +1 -0
  10. package/dist/__tests__/helpers.js +138 -0
  11. package/dist/__tests__/helpers.js.map +1 -0
  12. package/dist/app.d.ts +38 -0
  13. package/dist/app.d.ts.map +1 -0
  14. package/dist/app.js +60 -0
  15. package/dist/app.js.map +1 -0
  16. package/dist/config.d.ts +88 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +148 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +102 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware/auth.d.ts +15 -0
  25. package/dist/middleware/auth.d.ts.map +1 -0
  26. package/dist/middleware/auth.js +47 -0
  27. package/dist/middleware/auth.js.map +1 -0
  28. package/dist/middleware/error-handler.d.ts +14 -0
  29. package/dist/middleware/error-handler.d.ts.map +1 -0
  30. package/dist/middleware/error-handler.js +26 -0
  31. package/dist/middleware/error-handler.js.map +1 -0
  32. package/dist/middleware/not-found.d.ts +8 -0
  33. package/dist/middleware/not-found.d.ts.map +1 -0
  34. package/dist/middleware/not-found.js +12 -0
  35. package/dist/middleware/not-found.js.map +1 -0
  36. package/dist/middleware/request-logger.d.ts +17 -0
  37. package/dist/middleware/request-logger.d.ts.map +1 -0
  38. package/dist/middleware/request-logger.js +65 -0
  39. package/dist/middleware/request-logger.js.map +1 -0
  40. package/dist/middleware/ws-auth.d.ts +21 -0
  41. package/dist/middleware/ws-auth.d.ts.map +1 -0
  42. package/dist/middleware/ws-auth.js +59 -0
  43. package/dist/middleware/ws-auth.js.map +1 -0
  44. package/dist/routes/health.d.ts +11 -0
  45. package/dist/routes/health.d.ts.map +1 -0
  46. package/dist/routes/health.js +34 -0
  47. package/dist/routes/health.js.map +1 -0
  48. package/dist/routes/messages.d.ts +19 -0
  49. package/dist/routes/messages.d.ts.map +1 -0
  50. package/dist/routes/messages.js +154 -0
  51. package/dist/routes/messages.js.map +1 -0
  52. package/dist/routes/peers.d.ts +17 -0
  53. package/dist/routes/peers.d.ts.map +1 -0
  54. package/dist/routes/peers.js +169 -0
  55. package/dist/routes/peers.js.map +1 -0
  56. package/dist/routes/poll.d.ts +15 -0
  57. package/dist/routes/poll.d.ts.map +1 -0
  58. package/dist/routes/poll.js +97 -0
  59. package/dist/routes/poll.js.map +1 -0
  60. package/dist/routes/sessions.d.ts +14 -0
  61. package/dist/routes/sessions.d.ts.map +1 -0
  62. package/dist/routes/sessions.js +113 -0
  63. package/dist/routes/sessions.js.map +1 -0
  64. package/dist/routes/ui.d.ts +21 -0
  65. package/dist/routes/ui.d.ts.map +1 -0
  66. package/dist/routes/ui.js +173 -0
  67. package/dist/routes/ui.js.map +1 -0
  68. package/dist/routes/validation-hook.d.ts +18 -0
  69. package/dist/routes/validation-hook.d.ts.map +1 -0
  70. package/dist/routes/validation-hook.js +24 -0
  71. package/dist/routes/validation-hook.js.map +1 -0
  72. package/dist/services/auth-service.d.ts +48 -0
  73. package/dist/services/auth-service.d.ts.map +1 -0
  74. package/dist/services/auth-service.js +63 -0
  75. package/dist/services/auth-service.js.map +1 -0
  76. package/dist/services/connection-manager.d.ts +108 -0
  77. package/dist/services/connection-manager.d.ts.map +1 -0
  78. package/dist/services/connection-manager.js +216 -0
  79. package/dist/services/connection-manager.js.map +1 -0
  80. package/dist/services/message-queue.d.ts +56 -0
  81. package/dist/services/message-queue.d.ts.map +1 -0
  82. package/dist/services/message-queue.js +164 -0
  83. package/dist/services/message-queue.js.map +1 -0
  84. package/dist/services/peer-cleanup.d.ts +39 -0
  85. package/dist/services/peer-cleanup.d.ts.map +1 -0
  86. package/dist/services/peer-cleanup.js +96 -0
  87. package/dist/services/peer-cleanup.js.map +1 -0
  88. package/dist/services/session-cleanup.d.ts +44 -0
  89. package/dist/services/session-cleanup.d.ts.map +1 -0
  90. package/dist/services/session-cleanup.js +100 -0
  91. package/dist/services/session-cleanup.js.map +1 -0
  92. package/dist/services/session-store.d.ts +103 -0
  93. package/dist/services/session-store.d.ts.map +1 -0
  94. package/dist/services/session-store.js +292 -0
  95. package/dist/services/session-store.js.map +1 -0
  96. package/dist/services/stats-service.d.ts +48 -0
  97. package/dist/services/stats-service.d.ts.map +1 -0
  98. package/dist/services/stats-service.js +77 -0
  99. package/dist/services/stats-service.js.map +1 -0
  100. package/dist/types.d.ts +60 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/types.js +2 -0
  103. package/dist/types.js.map +1 -0
  104. package/dist/ui/components/Footer.d.ts +7 -0
  105. package/dist/ui/components/Footer.d.ts.map +1 -0
  106. package/dist/ui/components/Footer.js +17 -0
  107. package/dist/ui/components/Footer.js.map +1 -0
  108. package/dist/ui/components/Layout.d.ts +13 -0
  109. package/dist/ui/components/Layout.d.ts.map +1 -0
  110. package/dist/ui/components/Layout.js +11 -0
  111. package/dist/ui/components/Layout.js.map +1 -0
  112. package/dist/ui/components/NavBar.d.ts +12 -0
  113. package/dist/ui/components/NavBar.d.ts.map +1 -0
  114. package/dist/ui/components/NavBar.js +60 -0
  115. package/dist/ui/components/NavBar.js.map +1 -0
  116. package/dist/ui/components/StatCard.d.ts +14 -0
  117. package/dist/ui/components/StatCard.d.ts.map +1 -0
  118. package/dist/ui/components/StatCard.js +32 -0
  119. package/dist/ui/components/StatCard.js.map +1 -0
  120. package/dist/ui/components/Terminal.d.ts +13 -0
  121. package/dist/ui/components/Terminal.d.ts.map +1 -0
  122. package/dist/ui/components/Terminal.js +37 -0
  123. package/dist/ui/components/Terminal.js.map +1 -0
  124. package/dist/ui/pages/AdminDashboard.d.ts +13 -0
  125. package/dist/ui/pages/AdminDashboard.d.ts.map +1 -0
  126. package/dist/ui/pages/AdminDashboard.js +59 -0
  127. package/dist/ui/pages/AdminDashboard.js.map +1 -0
  128. package/dist/ui/pages/HowToPage.d.ts +8 -0
  129. package/dist/ui/pages/HowToPage.d.ts.map +1 -0
  130. package/dist/ui/pages/HowToPage.js +312 -0
  131. package/dist/ui/pages/HowToPage.js.map +1 -0
  132. package/dist/ui/pages/LandingPage.d.ts +13 -0
  133. package/dist/ui/pages/LandingPage.d.ts.map +1 -0
  134. package/dist/ui/pages/LandingPage.js +160 -0
  135. package/dist/ui/pages/LandingPage.js.map +1 -0
  136. package/dist/ui/pages/MessageLog.d.ts +25 -0
  137. package/dist/ui/pages/MessageLog.d.ts.map +1 -0
  138. package/dist/ui/pages/MessageLog.js +146 -0
  139. package/dist/ui/pages/MessageLog.js.map +1 -0
  140. package/dist/ui/pages/SessionDetail.d.ts +14 -0
  141. package/dist/ui/pages/SessionDetail.d.ts.map +1 -0
  142. package/dist/ui/pages/SessionDetail.js +165 -0
  143. package/dist/ui/pages/SessionDetail.js.map +1 -0
  144. package/dist/ui/pages/SessionList.d.ts +22 -0
  145. package/dist/ui/pages/SessionList.d.ts.map +1 -0
  146. package/dist/ui/pages/SessionList.js +88 -0
  147. package/dist/ui/pages/SessionList.js.map +1 -0
  148. package/dist/ui/styles/theme.d.ts +35 -0
  149. package/dist/ui/styles/theme.d.ts.map +1 -0
  150. package/dist/ui/styles/theme.js +65 -0
  151. package/dist/ui/styles/theme.js.map +1 -0
  152. package/dist/ws/frames.d.ts +82 -0
  153. package/dist/ws/frames.d.ts.map +1 -0
  154. package/dist/ws/frames.js +68 -0
  155. package/dist/ws/frames.js.map +1 -0
  156. package/dist/ws/handler.d.ts +26 -0
  157. package/dist/ws/handler.d.ts.map +1 -0
  158. package/dist/ws/handler.js +72 -0
  159. package/dist/ws/handler.js.map +1 -0
  160. package/dist/ws/heartbeat.d.ts +18 -0
  161. package/dist/ws/heartbeat.d.ts.map +1 -0
  162. package/dist/ws/heartbeat.js +39 -0
  163. package/dist/ws/heartbeat.js.map +1 -0
  164. package/docker-compose.yml +38 -0
  165. package/nginx.conf.example +63 -0
  166. package/package.json +61 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Background service that periodically scans all sessions and removes
3
+ * stale peers -- peers whose lastSeenAt timestamp exceeds the configured
4
+ * timeout. Removal adds peer_disconnected events to the session state
5
+ * so poll consumers are notified.
6
+ *
7
+ * Uses setInterval with unref() so the timer does not prevent Node.js
8
+ * from exiting naturally during graceful shutdown.
9
+ */
10
+ export class PeerCleanup {
11
+ sessionStore;
12
+ staleTimeoutMs;
13
+ intervalHandle = null;
14
+ constructor(sessionStore, staleTimeoutMs) {
15
+ this.sessionStore = sessionStore;
16
+ this.staleTimeoutMs = staleTimeoutMs;
17
+ }
18
+ /**
19
+ * Start the periodic cleanup timer.
20
+ *
21
+ * @param intervalMs - How often to check for stale peers (default: 60 seconds)
22
+ */
23
+ start(intervalMs = 60_000) {
24
+ if (this.intervalHandle !== null) {
25
+ return; // Already running
26
+ }
27
+ this.intervalHandle = setInterval(() => {
28
+ this.cleanup().catch((err) => {
29
+ console.error("Peer cleanup error:", err);
30
+ });
31
+ }, intervalMs);
32
+ // Allow Node.js to exit even if the timer is still running
33
+ this.intervalHandle.unref();
34
+ }
35
+ /**
36
+ * Stop the periodic cleanup timer.
37
+ */
38
+ stop() {
39
+ if (this.intervalHandle !== null) {
40
+ clearInterval(this.intervalHandle);
41
+ this.intervalHandle = null;
42
+ }
43
+ }
44
+ /**
45
+ * Scan all sessions and remove stale peers.
46
+ *
47
+ * A peer is stale if `Date.now() - Date.parse(peer.lastSeenAt)` exceeds
48
+ * the configured staleTimeoutMs.
49
+ *
50
+ * For each removed peer, a peer_disconnected event is recorded in the
51
+ * session state so poll consumers are notified.
52
+ *
53
+ * @returns Total number of stale peers removed across all sessions.
54
+ */
55
+ async cleanup() {
56
+ const sessionIds = await this.sessionStore.listSessions();
57
+ let totalRemoved = 0;
58
+ for (const sessionId of sessionIds) {
59
+ const session = await this.sessionStore.getSession(sessionId);
60
+ if (!session)
61
+ continue;
62
+ const now = Date.now();
63
+ const stalePeerIds = [];
64
+ for (const [peerId, peer] of Object.entries(session.peers)) {
65
+ const lastSeen = Date.parse(peer.lastSeenAt);
66
+ if (now - lastSeen > this.staleTimeoutMs) {
67
+ stalePeerIds.push(peerId);
68
+ }
69
+ }
70
+ if (stalePeerIds.length === 0)
71
+ continue;
72
+ await this.sessionStore.updateSession(sessionId, (state) => {
73
+ const timestamp = new Date().toISOString();
74
+ for (const peerId of stalePeerIds) {
75
+ // Only remove if still present (could have been deregistered concurrently)
76
+ if (state.peers[peerId]) {
77
+ delete state.peers[peerId];
78
+ const event = {
79
+ type: "peer_disconnected",
80
+ peerId,
81
+ timestamp,
82
+ };
83
+ state.peerEvents.push(event);
84
+ }
85
+ }
86
+ return state;
87
+ });
88
+ totalRemoved += stalePeerIds.length;
89
+ }
90
+ if (totalRemoved > 0) {
91
+ console.error(`Peer cleanup: removed ${totalRemoved} stale peer(s)`);
92
+ }
93
+ return totalRemoved;
94
+ }
95
+ }
96
+ //# sourceMappingURL=peer-cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-cleanup.js","sourceRoot":"","sources":["../../src/services/peer-cleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { SessionStore } from "./session-store.js";
2
+ import type { ConnectionManager } from "./connection-manager.js";
3
+ /**
4
+ * Background service that periodically scans all sessions and deletes
5
+ * those with no activity (messages, peer registrations, or peer events)
6
+ * beyond the configured inactivity threshold.
7
+ *
8
+ * Sessions with active WebSocket connections are never deleted,
9
+ * regardless of their timestamp age.
10
+ *
11
+ * Uses setInterval with unref() so the timer does not prevent Node.js
12
+ * from exiting naturally during graceful shutdown.
13
+ */
14
+ export declare class SessionCleanup {
15
+ private readonly sessionStore;
16
+ private readonly connectionManager;
17
+ private readonly inactivityThresholdMs;
18
+ private intervalHandle;
19
+ constructor(sessionStore: SessionStore, connectionManager: ConnectionManager, inactivityThresholdMs: number);
20
+ /**
21
+ * Start the periodic cleanup timer.
22
+ *
23
+ * @param intervalMs - How often to scan for inactive sessions (default: 1 hour)
24
+ */
25
+ start(intervalMs?: number): void;
26
+ /**
27
+ * Stop the periodic cleanup timer.
28
+ */
29
+ stop(): void;
30
+ /**
31
+ * Scan all sessions and delete those inactive beyond the threshold.
32
+ *
33
+ * A session is inactive if the most recent activity timestamp
34
+ * (from createdAt, last message, last peer registration, or last peer event)
35
+ * is older than `inactivityThresholdMs` milliseconds ago.
36
+ *
37
+ * Sessions with active WebSocket connections are always preserved,
38
+ * regardless of their timestamps.
39
+ *
40
+ * @returns Number of sessions deleted.
41
+ */
42
+ cleanup(): Promise<number>;
43
+ }
44
+ //# sourceMappingURL=session-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cleanup.d.ts","sourceRoot":"","sources":["../../src/services/session-cleanup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAoB;IACtD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,cAAc,CAA+C;gBAGnE,YAAY,EAAE,YAAY,EAC1B,iBAAiB,EAAE,iBAAiB,EACpC,qBAAqB,EAAE,MAAM;IAO/B;;;;OAIG;IACH,KAAK,CAAC,UAAU,GAAE,MAAkB,GAAG,IAAI;IAe3C;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;;;;;;;;;;OAWG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CAiDjC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Background service that periodically scans all sessions and deletes
3
+ * those with no activity (messages, peer registrations, or peer events)
4
+ * beyond the configured inactivity threshold.
5
+ *
6
+ * Sessions with active WebSocket connections are never deleted,
7
+ * regardless of their timestamp age.
8
+ *
9
+ * Uses setInterval with unref() so the timer does not prevent Node.js
10
+ * from exiting naturally during graceful shutdown.
11
+ */
12
+ export class SessionCleanup {
13
+ sessionStore;
14
+ connectionManager;
15
+ inactivityThresholdMs;
16
+ intervalHandle = null;
17
+ constructor(sessionStore, connectionManager, inactivityThresholdMs) {
18
+ this.sessionStore = sessionStore;
19
+ this.connectionManager = connectionManager;
20
+ this.inactivityThresholdMs = inactivityThresholdMs;
21
+ }
22
+ /**
23
+ * Start the periodic cleanup timer.
24
+ *
25
+ * @param intervalMs - How often to scan for inactive sessions (default: 1 hour)
26
+ */
27
+ start(intervalMs = 3_600_000) {
28
+ if (this.intervalHandle !== null) {
29
+ return; // Already running
30
+ }
31
+ this.intervalHandle = setInterval(() => {
32
+ this.cleanup().catch((err) => {
33
+ console.error("Session cleanup error:", err);
34
+ });
35
+ }, intervalMs);
36
+ // Allow Node.js to exit even if the timer is still running
37
+ this.intervalHandle.unref();
38
+ }
39
+ /**
40
+ * Stop the periodic cleanup timer.
41
+ */
42
+ stop() {
43
+ if (this.intervalHandle !== null) {
44
+ clearInterval(this.intervalHandle);
45
+ this.intervalHandle = null;
46
+ }
47
+ }
48
+ /**
49
+ * Scan all sessions and delete those inactive beyond the threshold.
50
+ *
51
+ * A session is inactive if the most recent activity timestamp
52
+ * (from createdAt, last message, last peer registration, or last peer event)
53
+ * is older than `inactivityThresholdMs` milliseconds ago.
54
+ *
55
+ * Sessions with active WebSocket connections are always preserved,
56
+ * regardless of their timestamps.
57
+ *
58
+ * @returns Number of sessions deleted.
59
+ */
60
+ async cleanup() {
61
+ const sessionIds = await this.sessionStore.listSessions();
62
+ let count = 0;
63
+ for (const sessionId of sessionIds) {
64
+ const session = await this.sessionStore.getSession(sessionId);
65
+ if (!session)
66
+ continue;
67
+ // Never delete a session with active WebSocket connections
68
+ if (this.connectionManager.getConnectedPeerIds(sessionId).length > 0) {
69
+ continue;
70
+ }
71
+ // Determine last activity timestamp from all sources
72
+ let lastActivityMs = Date.parse(session.createdAt);
73
+ // Last message timestamp (messages are in chronological order)
74
+ const lastMessage = session.messages.at(-1);
75
+ if (lastMessage) {
76
+ lastActivityMs = Math.max(lastActivityMs, Date.parse(lastMessage.timestamp));
77
+ }
78
+ // Last peer registration timestamp
79
+ const peerTimestamps = Object.values(session.peers).map((p) => Date.parse(p.registeredAt));
80
+ if (peerTimestamps.length > 0) {
81
+ lastActivityMs = Math.max(lastActivityMs, ...peerTimestamps);
82
+ }
83
+ // Last peer event timestamp
84
+ const lastEvent = session.peerEvents.at(-1);
85
+ if (lastEvent) {
86
+ lastActivityMs = Math.max(lastActivityMs, Date.parse(lastEvent.timestamp));
87
+ }
88
+ // Delete if inactive beyond threshold
89
+ if (Date.now() - lastActivityMs > this.inactivityThresholdMs) {
90
+ await this.sessionStore.deleteSession(sessionId);
91
+ count++;
92
+ }
93
+ }
94
+ if (count > 0) {
95
+ console.error(`Session cleanup: deleted ${count} inactive session(s)`);
96
+ }
97
+ return count;
98
+ }
99
+ }
100
+ //# sourceMappingURL=session-cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cleanup.js","sourceRoot":"","sources":["../../src/services/session-cleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ import type { TokenEntry, SessionFileState } from "../types.js";
2
+ /**
3
+ * Per-session JSON file store with in-memory locking and atomic writes.
4
+ *
5
+ * Each session is stored as `{sessionId}.json` in the state directory.
6
+ * A per-session promise-chain mutex prevents lost updates from concurrent
7
+ * requests targeting the same session. An in-memory Map provides O(1)
8
+ * token-to-session lookup, rebuilt from disk on startup.
9
+ */
10
+ export declare class SessionStore {
11
+ private readonly stateDir;
12
+ private readonly maxMessagesPerSession;
13
+ /** Per-session promise-chain mutex. */
14
+ private readonly locks;
15
+ /** In-memory index: SHA-256 tokenHash -> sessionId. Rebuilt on init(). */
16
+ private readonly tokenIndex;
17
+ /** In-memory index: label -> sessionId. Rebuilt on init(). */
18
+ private readonly labelIndex;
19
+ constructor(stateDir: string, maxMessagesPerSession: number);
20
+ /**
21
+ * Initialize the store: create state directory and rebuild token index
22
+ * from all existing session files on disk.
23
+ */
24
+ init(): Promise<void>;
25
+ /**
26
+ * Create a new session with the given ID, label, secret hash, and initial token.
27
+ * Throws INVALID_INPUT if a session file already exists for this ID.
28
+ */
29
+ createSession(sessionId: string, label: string | undefined, secretHash: string, tokenEntry: TokenEntry, creatorIp?: string): Promise<SessionFileState>;
30
+ /**
31
+ * Read a session by ID. Returns null if the session file does not exist.
32
+ */
33
+ getSession(sessionId: string): Promise<SessionFileState | null>;
34
+ /**
35
+ * Look up a session by token hash. Returns the sessionId and state,
36
+ * or null if no matching token is found.
37
+ */
38
+ getSessionByTokenHash(tokenHash: string): Promise<{
39
+ sessionId: string;
40
+ state: SessionFileState;
41
+ } | null>;
42
+ /**
43
+ * Read-modify-write a session within a per-session lock.
44
+ * The updater function receives the current state and must return the new state.
45
+ * Throws SESSION_NOT_FOUND if the session does not exist.
46
+ */
47
+ updateSession(sessionId: string, updater: (state: SessionFileState) => SessionFileState): Promise<SessionFileState>;
48
+ /**
49
+ * Add a bearer token to a session and update the in-memory index.
50
+ */
51
+ addToken(sessionId: string, tokenEntry: TokenEntry): Promise<void>;
52
+ /**
53
+ * Delete a session by ID: remove token index entries, delete the JSON file,
54
+ * and clean up the lock entry. Silently succeeds if the session file is
55
+ * already deleted (ENOENT). Needed for admin CRUD operations.
56
+ */
57
+ deleteSession(sessionId: string): Promise<void>;
58
+ /**
59
+ * List all session IDs by scanning .json files in the state directory.
60
+ */
61
+ listSessions(): Promise<string[]>;
62
+ /**
63
+ * Return the token-to-session index (read-only) for auth middleware.
64
+ */
65
+ getTokenIndex(): ReadonlyMap<string, string>;
66
+ /**
67
+ * Look up a session ID by its human-readable label.
68
+ * Returns null if no session has the given label.
69
+ */
70
+ getSessionIdByLabel(label: string): string | null;
71
+ /**
72
+ * Check whether a label is already in use by an existing session.
73
+ */
74
+ isLabelTaken(label: string): boolean;
75
+ /**
76
+ * Read and parse a session JSON file.
77
+ * Throws SESSION_NOT_FOUND if the file does not exist.
78
+ * Throws STATE_CORRUPT if the file cannot be parsed.
79
+ */
80
+ private _readState;
81
+ /**
82
+ * Write session state to disk atomically.
83
+ * Writes to a temp file first, then renames (atomic on same filesystem).
84
+ */
85
+ private _writeState;
86
+ /**
87
+ * Per-session promise-chain mutex.
88
+ * Serializes all operations on a given session to prevent lost updates
89
+ * from concurrent requests.
90
+ */
91
+ private _withLock;
92
+ /**
93
+ * Rebuild the token index entries for a single session's tokens.
94
+ */
95
+ private _rebuildTokenIndex;
96
+ /**
97
+ * Rebuild the label index entry for a single session.
98
+ */
99
+ private _rebuildLabelIndex;
100
+ /** Compute the file path for a session ID. */
101
+ private _filePath;
102
+ }
103
+ //# sourceMappingURL=session-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/services/session-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAE/C,uCAAuC;IACvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAE1D,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAExD,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;gBAE5C,QAAQ,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM;IAK3D;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B;;;OAGG;IACG,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,UAAU,EACtB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,gBAAgB,CAAC;IA8C5B;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAcrE;;;OAGG;IACG,qBAAqB,CACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,GAAG,IAAI,CAAC;IAcjE;;;;OAIG;IACG,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,gBAAgB,GACrD,OAAO,CAAC,gBAAgB,CAAC;IAe5B;;OAEG;IACG,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IASxE;;;;OAIG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CrD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAOvC;;OAEG;IACH,aAAa,IAAI,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;IAI5C;;;OAGG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIjD;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAMpC;;;;OAIG;YACW,UAAU;IAgCxB;;;OAGG;YACW,WAAW;IAWzB;;;;OAIG;YACW,SAAS;IAmBvB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B,8CAA8C;IAC9C,OAAO,CAAC,SAAS;CAGlB"}
@@ -0,0 +1,292 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { BridgeError, ErrorCode } from "@essentialai/cogent";
4
+ /**
5
+ * Per-session JSON file store with in-memory locking and atomic writes.
6
+ *
7
+ * Each session is stored as `{sessionId}.json` in the state directory.
8
+ * A per-session promise-chain mutex prevents lost updates from concurrent
9
+ * requests targeting the same session. An in-memory Map provides O(1)
10
+ * token-to-session lookup, rebuilt from disk on startup.
11
+ */
12
+ export class SessionStore {
13
+ stateDir;
14
+ maxMessagesPerSession;
15
+ /** Per-session promise-chain mutex. */
16
+ locks = new Map();
17
+ /** In-memory index: SHA-256 tokenHash -> sessionId. Rebuilt on init(). */
18
+ tokenIndex = new Map();
19
+ /** In-memory index: label -> sessionId. Rebuilt on init(). */
20
+ labelIndex = new Map();
21
+ constructor(stateDir, maxMessagesPerSession) {
22
+ this.stateDir = stateDir;
23
+ this.maxMessagesPerSession = maxMessagesPerSession;
24
+ }
25
+ /**
26
+ * Initialize the store: create state directory and rebuild token index
27
+ * from all existing session files on disk.
28
+ */
29
+ async init() {
30
+ await fs.mkdir(this.stateDir, { recursive: true });
31
+ const files = await fs.readdir(this.stateDir);
32
+ for (const file of files) {
33
+ if (!file.endsWith(".json"))
34
+ continue;
35
+ const filePath = path.join(this.stateDir, file);
36
+ try {
37
+ const raw = await fs.readFile(filePath, "utf-8");
38
+ const state = JSON.parse(raw);
39
+ this._rebuildTokenIndex(state);
40
+ this._rebuildLabelIndex(state);
41
+ }
42
+ catch {
43
+ // Skip corrupt files during init -- they'll error on access
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Create a new session with the given ID, label, secret hash, and initial token.
49
+ * Throws INVALID_INPUT if a session file already exists for this ID.
50
+ */
51
+ async createSession(sessionId, label, secretHash, tokenEntry, creatorIp) {
52
+ return this._withLock(sessionId, async () => {
53
+ const filePath = this._filePath(sessionId);
54
+ // Check if session file already exists
55
+ try {
56
+ await fs.access(filePath);
57
+ throw new BridgeError(ErrorCode.INVALID_INPUT, `Session ${sessionId} already exists`, "Use a different session ID or join the existing session");
58
+ }
59
+ catch (err) {
60
+ // If the error is our BridgeError, re-throw it
61
+ if (err instanceof BridgeError)
62
+ throw err;
63
+ // Otherwise, file doesn't exist -- proceed
64
+ }
65
+ // Enforce label uniqueness before creating
66
+ if (label !== undefined && this.isLabelTaken(label)) {
67
+ throw new BridgeError(ErrorCode.INVALID_INPUT, `Label "${label}" already in use`, "Choose a different label");
68
+ }
69
+ const state = {
70
+ sessionId,
71
+ ...(label !== undefined ? { label } : {}),
72
+ secretHash,
73
+ tokens: [tokenEntry],
74
+ createdAt: new Date().toISOString(),
75
+ ...(creatorIp ? { creatorIp } : {}),
76
+ peers: {},
77
+ messages: [],
78
+ peerEvents: [],
79
+ };
80
+ await this._writeState(sessionId, state);
81
+ this._rebuildTokenIndex(state);
82
+ this._rebuildLabelIndex(state);
83
+ return state;
84
+ });
85
+ }
86
+ /**
87
+ * Read a session by ID. Returns null if the session file does not exist.
88
+ */
89
+ async getSession(sessionId) {
90
+ try {
91
+ return await this._readState(sessionId);
92
+ }
93
+ catch (err) {
94
+ if (err instanceof BridgeError &&
95
+ err.code === ErrorCode.SESSION_NOT_FOUND) {
96
+ return null;
97
+ }
98
+ throw err;
99
+ }
100
+ }
101
+ /**
102
+ * Look up a session by token hash. Returns the sessionId and state,
103
+ * or null if no matching token is found.
104
+ */
105
+ async getSessionByTokenHash(tokenHash) {
106
+ const sessionId = this.tokenIndex.get(tokenHash);
107
+ if (!sessionId)
108
+ return null;
109
+ const state = await this.getSession(sessionId);
110
+ if (!state) {
111
+ // Token index is stale -- remove the mapping
112
+ this.tokenIndex.delete(tokenHash);
113
+ return null;
114
+ }
115
+ return { sessionId, state };
116
+ }
117
+ /**
118
+ * Read-modify-write a session within a per-session lock.
119
+ * The updater function receives the current state and must return the new state.
120
+ * Throws SESSION_NOT_FOUND if the session does not exist.
121
+ */
122
+ async updateSession(sessionId, updater) {
123
+ return this._withLock(sessionId, async () => {
124
+ const state = await this._readState(sessionId);
125
+ const updated = updater(state);
126
+ // Enforce message cap
127
+ if (updated.messages.length > this.maxMessagesPerSession) {
128
+ updated.messages = updated.messages.slice(-this.maxMessagesPerSession);
129
+ }
130
+ await this._writeState(sessionId, updated);
131
+ return updated;
132
+ });
133
+ }
134
+ /**
135
+ * Add a bearer token to a session and update the in-memory index.
136
+ */
137
+ async addToken(sessionId, tokenEntry) {
138
+ await this._withLock(sessionId, async () => {
139
+ const state = await this._readState(sessionId);
140
+ state.tokens.push(tokenEntry);
141
+ await this._writeState(sessionId, state);
142
+ this.tokenIndex.set(tokenEntry.tokenHash, sessionId);
143
+ });
144
+ }
145
+ /**
146
+ * Delete a session by ID: remove token index entries, delete the JSON file,
147
+ * and clean up the lock entry. Silently succeeds if the session file is
148
+ * already deleted (ENOENT). Needed for admin CRUD operations.
149
+ */
150
+ async deleteSession(sessionId) {
151
+ await this._withLock(sessionId, async () => {
152
+ // Read the session to get all token hashes for index cleanup
153
+ const filePath = this._filePath(sessionId);
154
+ try {
155
+ const raw = await fs.readFile(filePath, "utf-8");
156
+ const state = JSON.parse(raw);
157
+ // Remove each token hash from the in-memory index
158
+ for (const token of state.tokens) {
159
+ this.tokenIndex.delete(token.tokenHash);
160
+ }
161
+ // Remove label from the label index
162
+ if (state.label) {
163
+ this.labelIndex.delete(state.label);
164
+ }
165
+ }
166
+ catch {
167
+ // If we can't read the file, it may already be deleted or corrupt.
168
+ // In either case, proceed with file deletion attempt.
169
+ }
170
+ // Delete the session JSON file from disk
171
+ try {
172
+ await fs.unlink(filePath);
173
+ }
174
+ catch (err) {
175
+ // ENOENT is fine -- file already deleted
176
+ if (!(err instanceof Error &&
177
+ "code" in err &&
178
+ err.code === "ENOENT")) {
179
+ throw err;
180
+ }
181
+ }
182
+ });
183
+ // Clean up the lock entry after completion
184
+ this.locks.delete(sessionId);
185
+ }
186
+ /**
187
+ * List all session IDs by scanning .json files in the state directory.
188
+ */
189
+ async listSessions() {
190
+ const files = await fs.readdir(this.stateDir);
191
+ return files
192
+ .filter((f) => f.endsWith(".json"))
193
+ .map((f) => f.slice(0, -5));
194
+ }
195
+ /**
196
+ * Return the token-to-session index (read-only) for auth middleware.
197
+ */
198
+ getTokenIndex() {
199
+ return this.tokenIndex;
200
+ }
201
+ /**
202
+ * Look up a session ID by its human-readable label.
203
+ * Returns null if no session has the given label.
204
+ */
205
+ getSessionIdByLabel(label) {
206
+ return this.labelIndex.get(label) ?? null;
207
+ }
208
+ /**
209
+ * Check whether a label is already in use by an existing session.
210
+ */
211
+ isLabelTaken(label) {
212
+ return this.labelIndex.has(label);
213
+ }
214
+ // --- Private methods ---
215
+ /**
216
+ * Read and parse a session JSON file.
217
+ * Throws SESSION_NOT_FOUND if the file does not exist.
218
+ * Throws STATE_CORRUPT if the file cannot be parsed.
219
+ */
220
+ async _readState(sessionId) {
221
+ const filePath = this._filePath(sessionId);
222
+ let raw;
223
+ try {
224
+ raw = await fs.readFile(filePath, "utf-8");
225
+ }
226
+ catch (err) {
227
+ if (err instanceof Error &&
228
+ "code" in err &&
229
+ err.code === "ENOENT") {
230
+ throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
231
+ }
232
+ throw err;
233
+ }
234
+ try {
235
+ return JSON.parse(raw);
236
+ }
237
+ catch {
238
+ throw new BridgeError(ErrorCode.STATE_CORRUPT, `Session file for ${sessionId} is corrupt`, "The session data file could not be parsed as JSON");
239
+ }
240
+ }
241
+ /**
242
+ * Write session state to disk atomically.
243
+ * Writes to a temp file first, then renames (atomic on same filesystem).
244
+ */
245
+ async _writeState(sessionId, state) {
246
+ const filePath = this._filePath(sessionId);
247
+ const tmpPath = `${filePath}.${process.pid}.tmp`;
248
+ await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
249
+ await fs.rename(tmpPath, filePath);
250
+ }
251
+ /**
252
+ * Per-session promise-chain mutex.
253
+ * Serializes all operations on a given session to prevent lost updates
254
+ * from concurrent requests.
255
+ */
256
+ async _withLock(sessionId, fn) {
257
+ const prev = this.locks.get(sessionId) ?? Promise.resolve();
258
+ let release;
259
+ const next = new Promise((resolve) => {
260
+ release = resolve;
261
+ });
262
+ this.locks.set(sessionId, next);
263
+ await prev;
264
+ try {
265
+ return await fn();
266
+ }
267
+ finally {
268
+ release();
269
+ }
270
+ }
271
+ /**
272
+ * Rebuild the token index entries for a single session's tokens.
273
+ */
274
+ _rebuildTokenIndex(state) {
275
+ for (const token of state.tokens) {
276
+ this.tokenIndex.set(token.tokenHash, state.sessionId);
277
+ }
278
+ }
279
+ /**
280
+ * Rebuild the label index entry for a single session.
281
+ */
282
+ _rebuildLabelIndex(state) {
283
+ if (state.label) {
284
+ this.labelIndex.set(state.label, state.sessionId);
285
+ }
286
+ }
287
+ /** Compute the file path for a session ID. */
288
+ _filePath(sessionId) {
289
+ return path.join(this.stateDir, `${sessionId}.json`);
290
+ }
291
+ }
292
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/services/session-store.ts"],"names":[],"mappings":"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"}