@essentialai/cogent-server 3.4.2 → 3.4.3
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/dist/__tests__/helpers.d.ts +5 -2
- package/dist/__tests__/helpers.d.ts.map +1 -1
- package/dist/__tests__/helpers.js +11 -4
- package/dist/__tests__/helpers.js.map +1 -1
- package/dist/__tests__/services/session-store-contract.d.ts +17 -0
- package/dist/__tests__/services/session-store-contract.d.ts.map +1 -0
- package/dist/__tests__/services/session-store-contract.js +186 -0
- package/dist/__tests__/services/session-store-contract.js.map +1 -0
- package/dist/app.d.ts +9 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +18 -2
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -1
- package/dist/contract/control-plane-contract.d.ts +93 -0
- package/dist/contract/control-plane-contract.d.ts.map +1 -0
- package/dist/contract/control-plane-contract.js +72 -0
- package/dist/contract/control-plane-contract.js.map +1 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate-cli.d.ts +2 -0
- package/dist/db/migrate-cli.d.ts.map +1 -0
- package/dist/db/migrate-cli.js +54 -0
- package/dist/db/migrate-cli.js.map +1 -0
- package/dist/db/migrate.d.ts +31 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +98 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0001_init_sessions.down.sql +4 -0
- package/dist/db/migrations/0001_init_sessions.up.sql +46 -0
- package/dist/db/migrations/0002_org_quotas.down.sql +2 -0
- package/dist/db/migrations/0002_org_quotas.up.sql +13 -0
- package/dist/db/pool.d.ts +39 -0
- package/dist/db/pool.d.ts.map +1 -0
- package/dist/db/pool.js +72 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +32 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/control-plane-auth.d.ts +17 -0
- package/dist/middleware/control-plane-auth.d.ts.map +1 -0
- package/dist/middleware/control-plane-auth.js +35 -0
- package/dist/middleware/control-plane-auth.js.map +1 -0
- package/dist/routes/control-plane.d.ts +20 -0
- package/dist/routes/control-plane.d.ts.map +1 -0
- package/dist/routes/control-plane.js +122 -0
- package/dist/routes/control-plane.js.map +1 -0
- package/dist/routes/messages.d.ts.map +1 -1
- package/dist/routes/messages.js +18 -0
- package/dist/routes/messages.js.map +1 -1
- package/dist/routes/poll.js +2 -2
- package/dist/routes/poll.js.map +1 -1
- package/dist/routes/sessions.d.ts +22 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +99 -13
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/validation-hook.d.ts +31 -0
- package/dist/routes/validation-hook.d.ts.map +1 -1
- package/dist/routes/validation-hook.js +3 -0
- package/dist/routes/validation-hook.js.map +1 -1
- package/dist/services/auth-service.d.ts +5 -45
- package/dist/services/auth-service.d.ts.map +1 -1
- package/dist/services/auth-service.js +5 -60
- package/dist/services/auth-service.js.map +1 -1
- package/dist/services/connection-manager.d.ts +15 -0
- package/dist/services/connection-manager.d.ts.map +1 -1
- package/dist/services/connection-manager.js +29 -0
- package/dist/services/connection-manager.js.map +1 -1
- package/dist/services/join-rate-limiter.d.ts +50 -0
- package/dist/services/join-rate-limiter.d.ts.map +1 -0
- package/dist/services/join-rate-limiter.js +89 -0
- package/dist/services/join-rate-limiter.js.map +1 -0
- package/dist/services/obs-log.d.ts +51 -0
- package/dist/services/obs-log.d.ts.map +1 -0
- package/dist/services/obs-log.js +93 -0
- package/dist/services/obs-log.js.map +1 -0
- package/dist/services/session-store-memory.d.ts +60 -0
- package/dist/services/session-store-memory.d.ts.map +1 -0
- package/dist/services/session-store-memory.js +189 -0
- package/dist/services/session-store-memory.js.map +1 -0
- package/dist/services/session-store-postgres.d.ts +60 -0
- package/dist/services/session-store-postgres.d.ts.map +1 -0
- package/dist/services/session-store-postgres.js +393 -0
- package/dist/services/session-store-postgres.js.map +1 -0
- package/dist/services/session-store.d.ts +73 -5
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +62 -16
- package/dist/services/session-store.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -6
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { BridgeError, ErrorCode, isValidSessionId } from "@essentialai/cogent";
|
|
2
|
+
import { withTransaction } from "../db/pool.js";
|
|
3
|
+
import { isUpToDate } from "../db/migrate.js";
|
|
4
|
+
import { labelIndexKey } from "./session-store.js";
|
|
5
|
+
import { logOrgEvent } from "./obs-log.js";
|
|
6
|
+
/**
|
|
7
|
+
* Postgres implementation of {@link SessionStore} (Business Edition, FR35),
|
|
8
|
+
* JSONB-hybrid: queryable/unique/auth scalars are columns; the rest of the
|
|
9
|
+
* session document lives in a `state jsonb` column — a 1:1 mirror of the file
|
|
10
|
+
* store's per-session JSON, so `updateSession` is a whole-document UPDATE.
|
|
11
|
+
*
|
|
12
|
+
* The synchronous index methods (`getSessionIdByLabel`, `isLabelTaken`,
|
|
13
|
+
* `getTokenIndex`) are served from in-memory caches rebuilt on `init()` and kept
|
|
14
|
+
* in sync on writes — exactly as `FileSessionStore` does. This is correct for
|
|
15
|
+
* the single-node MVP (all writes go through this instance); multi-node (vNext)
|
|
16
|
+
* will need cache invalidation or a shared lookup. `getSessionByTokenHash`
|
|
17
|
+
* queries the DB directly so bearer-token auth is always authoritative.
|
|
18
|
+
*
|
|
19
|
+
* The pool is injected (DI) and owned by the caller (index.ts closes it).
|
|
20
|
+
*/
|
|
21
|
+
export class PostgresSessionStore {
|
|
22
|
+
pool;
|
|
23
|
+
maxMessagesPerSession;
|
|
24
|
+
/** label-scope key -> sessionId (mirrors FileSessionStore.labelIndex). */
|
|
25
|
+
labelIndex = new Map();
|
|
26
|
+
/** SHA-256 tokenHash -> sessionId. */
|
|
27
|
+
tokenIndex = new Map();
|
|
28
|
+
constructor(pool, maxMessagesPerSession) {
|
|
29
|
+
this.pool = pool;
|
|
30
|
+
this.maxMessagesPerSession = maxMessagesPerSession;
|
|
31
|
+
}
|
|
32
|
+
async init() {
|
|
33
|
+
if (!(await isUpToDate(this.pool))) {
|
|
34
|
+
throw new BridgeError(ErrorCode.STARTUP_FAILED, "Database schema is not migrated", "Run `npm run db:migrate` against COGENT_SERVER_DATABASE_URL before starting");
|
|
35
|
+
}
|
|
36
|
+
// Rebuild the sync-lookup caches from the DB.
|
|
37
|
+
const tokens = await this.pool.query("SELECT token_hash, session_id FROM tokens");
|
|
38
|
+
for (const r of tokens.rows)
|
|
39
|
+
this.tokenIndex.set(r.token_hash, r.session_id);
|
|
40
|
+
const labels = await this.pool.query("SELECT session_id, label, org_id_hash, org_scope FROM sessions WHERE label IS NOT NULL");
|
|
41
|
+
for (const r of labels.rows) {
|
|
42
|
+
// Fail-closed: never index a legacy business session lacking org_scope
|
|
43
|
+
// under the public bare key (mirrors FileSessionStore._rebuildLabelIndex).
|
|
44
|
+
if (r.org_id_hash !== null && r.org_scope === null)
|
|
45
|
+
continue;
|
|
46
|
+
this.labelIndex.set(labelIndexKey(r.label, r.org_scope ?? undefined), r.session_id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async createSession(sessionId, label, secretHash, tokenEntry, creatorIp, org) {
|
|
50
|
+
const createdAt = new Date().toISOString();
|
|
51
|
+
const initialDoc = { peers: {}, messages: [], peerEvents: [] };
|
|
52
|
+
// Returned from the tx, logged AFTER commit so an "admitted" quota_check is
|
|
53
|
+
// never emitted for a create that later rolls back. (Rejections are logged
|
|
54
|
+
// inside the tx — the rejection IS the committed outcome.)
|
|
55
|
+
const channelAdmit = await withTransaction(this.pool, async (client) => {
|
|
56
|
+
let admit = null;
|
|
57
|
+
// Atomic per-org channel quota (FR7/FR8). Row-lock the org's quota row so
|
|
58
|
+
// concurrent creates for the same org serialize: exactly one can cross the
|
|
59
|
+
// limit. A missing quota row = not-yet-provisioned (E3) -> no cap. The
|
|
60
|
+
// lock_timeout bounds FOR UPDATE contention (no indefinite block).
|
|
61
|
+
if (org) {
|
|
62
|
+
await client.query("SET LOCAL lock_timeout = '3s'");
|
|
63
|
+
try {
|
|
64
|
+
const qr = await client.query("SELECT max_channels FROM org_quotas WHERE org_scope = $1 FOR UPDATE", [org.scope]);
|
|
65
|
+
if (qr.rowCount && qr.rowCount > 0) {
|
|
66
|
+
const cnt = await client.query("SELECT count(*)::int AS n FROM sessions WHERE org_scope = $1", [org.scope]);
|
|
67
|
+
const atLimit = cnt.rows[0].n >= qr.rows[0].max_channels;
|
|
68
|
+
if (atLimit) {
|
|
69
|
+
logOrgEvent({
|
|
70
|
+
event: "quota_check",
|
|
71
|
+
orgScope: org.scope,
|
|
72
|
+
sessionId,
|
|
73
|
+
outcome: "rejected",
|
|
74
|
+
detail: { quota: "channel", used: cnt.rows[0].n, limit: qr.rows[0].max_channels },
|
|
75
|
+
});
|
|
76
|
+
throw new BridgeError(ErrorCode.SESSION_FULL, `Org channel quota reached (${cnt.rows[0].n}/${qr.rows[0].max_channels})`, "Upgrade your tier or delete a channel before creating another");
|
|
77
|
+
}
|
|
78
|
+
admit = { used: cnt.rows[0].n, limit: qr.rows[0].max_channels };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Surface lock_timeout/deadlock as a clean retryable error; rethrow
|
|
83
|
+
// everything else (incl. the SESSION_FULL above) unchanged.
|
|
84
|
+
this._mapLockError(err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await client.query(`INSERT INTO sessions
|
|
89
|
+
(session_id, label, secret_hash, org_id_hash, org_scope, creator_ip, created_at, state)
|
|
90
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`, [
|
|
91
|
+
sessionId,
|
|
92
|
+
label ?? null,
|
|
93
|
+
secretHash,
|
|
94
|
+
org?.idHash ?? null,
|
|
95
|
+
org?.scope ?? null,
|
|
96
|
+
creatorIp || null, // drop empty string, like the file store
|
|
97
|
+
createdAt,
|
|
98
|
+
JSON.stringify(initialDoc),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this._mapCreateError(err, sessionId, label);
|
|
103
|
+
}
|
|
104
|
+
await client.query("INSERT INTO tokens (token_hash, session_id, peer_id, created_at) VALUES ($1, $2, $3, $4)", [tokenEntry.tokenHash, sessionId, tokenEntry.peerId ?? null, tokenEntry.createdAt]);
|
|
105
|
+
return admit;
|
|
106
|
+
});
|
|
107
|
+
// Update caches AFTER the commit.
|
|
108
|
+
this.tokenIndex.set(tokenEntry.tokenHash, sessionId);
|
|
109
|
+
if (label !== undefined)
|
|
110
|
+
this.labelIndex.set(labelIndexKey(label, org?.scope), sessionId);
|
|
111
|
+
// Admitted quota_check logged post-commit (accurate: the channel exists now).
|
|
112
|
+
if (org && channelAdmit) {
|
|
113
|
+
logOrgEvent({
|
|
114
|
+
event: "quota_check",
|
|
115
|
+
orgScope: org.scope,
|
|
116
|
+
sessionId,
|
|
117
|
+
outcome: "admitted",
|
|
118
|
+
detail: { quota: "channel", used: channelAdmit.used, limit: channelAdmit.limit },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const state = {
|
|
122
|
+
sessionId,
|
|
123
|
+
...(label !== undefined ? { label } : {}),
|
|
124
|
+
secretHash,
|
|
125
|
+
...(org !== undefined ? { orgIdHash: org.idHash, orgScope: org.scope } : {}),
|
|
126
|
+
tokens: [tokenEntry],
|
|
127
|
+
createdAt,
|
|
128
|
+
...(creatorIp ? { creatorIp } : {}),
|
|
129
|
+
peers: {},
|
|
130
|
+
messages: [],
|
|
131
|
+
peerEvents: [],
|
|
132
|
+
};
|
|
133
|
+
return state;
|
|
134
|
+
}
|
|
135
|
+
async getSession(sessionId) {
|
|
136
|
+
// A malformed (non-UUID) id can't exist in a uuid column; return not-found
|
|
137
|
+
// rather than letting Postgres raise 22P02 (parity with the file store).
|
|
138
|
+
if (!isValidSessionId(sessionId))
|
|
139
|
+
return null;
|
|
140
|
+
const res = await this.pool.query("SELECT * FROM sessions WHERE session_id = $1", [sessionId]);
|
|
141
|
+
if (res.rowCount === 0)
|
|
142
|
+
return null;
|
|
143
|
+
const tokens = await this.pool.query("SELECT token_hash, peer_id, created_at FROM tokens WHERE session_id = $1 ORDER BY created_at ASC", [sessionId]);
|
|
144
|
+
return this._rowToState(res.rows[0], tokens.rows);
|
|
145
|
+
}
|
|
146
|
+
async getSessionByTokenHash(tokenHash) {
|
|
147
|
+
// Authoritative DB lookup (not the cache) — auth must never read stale.
|
|
148
|
+
const res = await this.pool.query("SELECT session_id FROM tokens WHERE token_hash = $1", [tokenHash]);
|
|
149
|
+
if (res.rowCount === 0)
|
|
150
|
+
return null;
|
|
151
|
+
const sessionId = res.rows[0].session_id;
|
|
152
|
+
const state = await this.getSession(sessionId);
|
|
153
|
+
if (!state)
|
|
154
|
+
return null;
|
|
155
|
+
return { sessionId, state };
|
|
156
|
+
}
|
|
157
|
+
async updateSession(sessionId, updater) {
|
|
158
|
+
if (!isValidSessionId(sessionId)) {
|
|
159
|
+
throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
|
|
160
|
+
}
|
|
161
|
+
const { state: result, admit: agentAdmit } = await withTransaction(this.pool, async (client) => {
|
|
162
|
+
let admit = null;
|
|
163
|
+
const res = await client.query("SELECT * FROM sessions WHERE session_id = $1 FOR UPDATE", [sessionId]);
|
|
164
|
+
if (res.rowCount === 0) {
|
|
165
|
+
throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
|
|
166
|
+
}
|
|
167
|
+
const tokens = await client.query("SELECT token_hash, peer_id, created_at FROM tokens WHERE session_id = $1 ORDER BY created_at ASC", [sessionId]);
|
|
168
|
+
const current = this._rowToState(res.rows[0], tokens.rows);
|
|
169
|
+
const updated = updater(structuredClone(current));
|
|
170
|
+
if (updated.messages.length > this.maxMessagesPerSession) {
|
|
171
|
+
updated.messages = updated.messages.slice(-this.maxMessagesPerSession);
|
|
172
|
+
}
|
|
173
|
+
// Atomic per-org AGENT quota (FR7/FR8). Enforce ONLY when this update adds
|
|
174
|
+
// a net-new peer to a business (org-scoped) session — peer registration is
|
|
175
|
+
// the single mutation that consumes an agent slot. We already hold this
|
|
176
|
+
// session's row lock (FOR UPDATE above); additionally row-lock the org's
|
|
177
|
+
// quota row so concurrent registrations across the org's channels
|
|
178
|
+
// serialize — exactly one can cross the limit (parity with the channel
|
|
179
|
+
// quota in createSession). Release is inherent: deregister drops a peer →
|
|
180
|
+
// the count falls. A missing quota row = not-yet-provisioned (E3) → no cap.
|
|
181
|
+
// `addsPeer` uses the net key-count delta, which is exact here because the
|
|
182
|
+
// only updater that grows `peers` is peer registration, and it adds exactly
|
|
183
|
+
// one key (peers.ts). A heartbeat (lastSeenAt), message append, or
|
|
184
|
+
// deregister never grows the count, so the check is skipped for them. The
|
|
185
|
+
// enforced `total` below is the absolute post-state count (not a delta), so
|
|
186
|
+
// even a hypothetical multi-peer churn could never cross the cap.
|
|
187
|
+
const orgScope = res.rows[0].org_scope;
|
|
188
|
+
const addsPeer = orgScope !== null &&
|
|
189
|
+
Object.keys(updated.peers).length > Object.keys(current.peers).length;
|
|
190
|
+
if (addsPeer) {
|
|
191
|
+
await client.query("SET LOCAL lock_timeout = '3s'");
|
|
192
|
+
try {
|
|
193
|
+
const qr = await client.query("SELECT max_agents FROM org_quotas WHERE org_scope = $1 FOR UPDATE", [orgScope]);
|
|
194
|
+
if (qr.rowCount && qr.rowCount > 0) {
|
|
195
|
+
// Count agents in the org's OTHER channels from the DB (this session's
|
|
196
|
+
// new peers aren't written yet), then add this session's post-update
|
|
197
|
+
// peer count. COALESCE guards a null/absent peers document.
|
|
198
|
+
const others = await client.query(`SELECT COALESCE(
|
|
199
|
+
SUM((SELECT count(*) FROM jsonb_object_keys(COALESCE(s.state->'peers', '{}'::jsonb)))),
|
|
200
|
+
0)::int AS n
|
|
201
|
+
FROM sessions s
|
|
202
|
+
WHERE s.org_scope = $1 AND s.session_id <> $2`, [orgScope, sessionId]);
|
|
203
|
+
const total = others.rows[0].n + Object.keys(updated.peers).length;
|
|
204
|
+
const atLimit = total > qr.rows[0].max_agents;
|
|
205
|
+
if (atLimit) {
|
|
206
|
+
logOrgEvent({
|
|
207
|
+
event: "quota_check",
|
|
208
|
+
orgScope,
|
|
209
|
+
sessionId,
|
|
210
|
+
outcome: "rejected",
|
|
211
|
+
detail: { quota: "agent", used: total, limit: qr.rows[0].max_agents },
|
|
212
|
+
});
|
|
213
|
+
throw new BridgeError(ErrorCode.SESSION_FULL, `Org agent quota reached (${total}/${qr.rows[0].max_agents})`, "Upgrade your tier or disconnect an agent before registering another");
|
|
214
|
+
}
|
|
215
|
+
admit = { used: total, limit: qr.rows[0].max_agents, orgScope: orgScope };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
// Surface lock_timeout/deadlock as a clean retryable error; rethrow
|
|
220
|
+
// everything else (incl. the SESSION_FULL above) unchanged.
|
|
221
|
+
this._mapLockError(err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Persist the document (everything except tokens, which addToken owns) +
|
|
225
|
+
// the scalar columns, in case the updater changed them.
|
|
226
|
+
const doc = this._docFromState(updated);
|
|
227
|
+
await client.query(`UPDATE sessions
|
|
228
|
+
SET label = $2, secret_hash = $3, org_id_hash = $4, org_scope = $5,
|
|
229
|
+
creator_ip = $6, state = $7::jsonb
|
|
230
|
+
WHERE session_id = $1`, [
|
|
231
|
+
sessionId,
|
|
232
|
+
updated.label ?? null,
|
|
233
|
+
updated.secretHash,
|
|
234
|
+
updated.orgIdHash ?? null,
|
|
235
|
+
updated.orgScope ?? null,
|
|
236
|
+
updated.creatorIp ?? null,
|
|
237
|
+
JSON.stringify(doc),
|
|
238
|
+
]);
|
|
239
|
+
return { state: structuredClone(updated), admit };
|
|
240
|
+
});
|
|
241
|
+
// Admitted agent quota_check logged post-commit (the peer is persisted now).
|
|
242
|
+
if (agentAdmit) {
|
|
243
|
+
logOrgEvent({
|
|
244
|
+
event: "quota_check",
|
|
245
|
+
orgScope: agentAdmit.orgScope,
|
|
246
|
+
sessionId,
|
|
247
|
+
outcome: "admitted",
|
|
248
|
+
detail: { quota: "agent", used: agentAdmit.used, limit: agentAdmit.limit },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
async addToken(sessionId, tokenEntry) {
|
|
254
|
+
if (!isValidSessionId(sessionId)) {
|
|
255
|
+
throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
await this.pool.query(`INSERT INTO tokens (token_hash, session_id, peer_id, created_at)
|
|
259
|
+
VALUES ($1, $2, $3, $4) ON CONFLICT (token_hash) DO NOTHING`, [tokenEntry.tokenHash, sessionId, tokenEntry.peerId ?? null, tokenEntry.createdAt]);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
// FK violation => the session does not exist (parity with file store).
|
|
263
|
+
if (err.code === "23503") {
|
|
264
|
+
throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
|
|
265
|
+
}
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
this.tokenIndex.set(tokenEntry.tokenHash, sessionId);
|
|
269
|
+
}
|
|
270
|
+
async deleteSession(sessionId) {
|
|
271
|
+
if (!isValidSessionId(sessionId))
|
|
272
|
+
return; // no-op like the file store
|
|
273
|
+
const res = await this.pool.query("DELETE FROM sessions WHERE session_id = $1 RETURNING label, org_scope", [sessionId]);
|
|
274
|
+
if (res.rowCount && res.rowCount > 0) {
|
|
275
|
+
const { label, org_scope } = res.rows[0];
|
|
276
|
+
if (label !== null) {
|
|
277
|
+
this.labelIndex.delete(labelIndexKey(label, org_scope ?? undefined));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// tokens were cascade-deleted; purge their cache entries for this session.
|
|
281
|
+
for (const [hash, sid] of this.tokenIndex) {
|
|
282
|
+
if (sid === sessionId)
|
|
283
|
+
this.tokenIndex.delete(hash);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async listSessions() {
|
|
287
|
+
const res = await this.pool.query("SELECT session_id FROM sessions");
|
|
288
|
+
return res.rows.map((r) => r.session_id);
|
|
289
|
+
}
|
|
290
|
+
async countAgentsByOrgScope(orgScope) {
|
|
291
|
+
// Mirrors EXACTLY the per-org agent-quota count in updateSession (sum of
|
|
292
|
+
// peer keys across the org's sessions) — minus the self-exclusion, since
|
|
293
|
+
// here we want the full current total. Keeps usage == enforcement.
|
|
294
|
+
const res = await this.pool.query(`SELECT COALESCE(
|
|
295
|
+
SUM((SELECT count(*) FROM jsonb_object_keys(COALESCE(s.state->'peers', '{}'::jsonb)))),
|
|
296
|
+
0)::int AS n
|
|
297
|
+
FROM sessions s
|
|
298
|
+
WHERE s.org_scope = $1`, [orgScope]);
|
|
299
|
+
return res.rows[0].n;
|
|
300
|
+
}
|
|
301
|
+
async setOrgQuota(orgScope, tier, maxChannels, maxAgents) {
|
|
302
|
+
// Upsert the org's enforced quota row (Story 5.1, FR20). The CP owns the
|
|
303
|
+
// tier→limits mapping; the relay persists what it enforces (channel quota in
|
|
304
|
+
// createSession, agent quota in updateSession — both row-lock this row). Idempotent:
|
|
305
|
+
// a tier change (5.7) re-sends and overwrites. updated_at tracks the last push.
|
|
306
|
+
await this.pool.query(`INSERT INTO org_quotas (org_scope, tier, max_channels, max_agents, updated_at)
|
|
307
|
+
VALUES ($1, $2, $3, $4, now())
|
|
308
|
+
ON CONFLICT (org_scope) DO UPDATE SET
|
|
309
|
+
tier = EXCLUDED.tier,
|
|
310
|
+
max_channels = EXCLUDED.max_channels,
|
|
311
|
+
max_agents = EXCLUDED.max_agents,
|
|
312
|
+
updated_at = now()`, [orgScope, tier, maxChannels, maxAgents]);
|
|
313
|
+
}
|
|
314
|
+
getTokenIndex() {
|
|
315
|
+
return this.tokenIndex;
|
|
316
|
+
}
|
|
317
|
+
getSessionIdByLabel(label, orgScope) {
|
|
318
|
+
return this.labelIndex.get(labelIndexKey(label, orgScope)) ?? null;
|
|
319
|
+
}
|
|
320
|
+
isLabelTaken(label, orgScope) {
|
|
321
|
+
return this.labelIndex.has(labelIndexKey(label, orgScope));
|
|
322
|
+
}
|
|
323
|
+
async getGlobalMessageCount() {
|
|
324
|
+
const res = await this.pool.query("SELECT total_messages FROM global_stats WHERE id = true");
|
|
325
|
+
return res.rowCount ? Number(res.rows[0].total_messages) : 0;
|
|
326
|
+
}
|
|
327
|
+
async incrementGlobalMessageCount() {
|
|
328
|
+
// Atomic in SQL — no read-modify-write race.
|
|
329
|
+
await this.pool.query("UPDATE global_stats SET total_messages = total_messages + 1 WHERE id = true");
|
|
330
|
+
}
|
|
331
|
+
// --- Private helpers ---
|
|
332
|
+
/** Assemble a SessionFileState from a sessions row + its token rows. */
|
|
333
|
+
_rowToState(row, tokenRows) {
|
|
334
|
+
const doc = row.state ?? {};
|
|
335
|
+
return {
|
|
336
|
+
sessionId: row.session_id,
|
|
337
|
+
...(row.label !== null ? { label: row.label } : {}),
|
|
338
|
+
secretHash: row.secret_hash,
|
|
339
|
+
...(row.org_id_hash !== null ? { orgIdHash: row.org_id_hash } : {}),
|
|
340
|
+
...(row.org_scope !== null ? { orgScope: row.org_scope } : {}),
|
|
341
|
+
tokens: tokenRows.map((t) => ({
|
|
342
|
+
tokenHash: t.token_hash,
|
|
343
|
+
createdAt: t.created_at instanceof Date ? t.created_at.toISOString() : String(t.created_at),
|
|
344
|
+
...(t.peer_id !== null ? { peerId: t.peer_id } : {}),
|
|
345
|
+
})),
|
|
346
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
347
|
+
...(row.creator_ip !== null ? { creatorIp: row.creator_ip } : {}),
|
|
348
|
+
peers: doc.peers ?? {},
|
|
349
|
+
messages: doc.messages ?? [],
|
|
350
|
+
peerEvents: doc.peerEvents ?? [],
|
|
351
|
+
...(doc.messageQueue !== undefined ? { messageQueue: doc.messageQueue } : {}),
|
|
352
|
+
...(doc.channelMappings !== undefined ? { channelMappings: doc.channelMappings } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/** Extract the JSONB document (everything not in a scalar column or tokens). */
|
|
356
|
+
_docFromState(state) {
|
|
357
|
+
return {
|
|
358
|
+
peers: state.peers,
|
|
359
|
+
messages: state.messages,
|
|
360
|
+
peerEvents: state.peerEvents,
|
|
361
|
+
...(state.messageQueue !== undefined ? { messageQueue: state.messageQueue } : {}),
|
|
362
|
+
...(state.channelMappings !== undefined ? { channelMappings: state.channelMappings } : {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Map a row-lock contention failure to a clean, retryable BridgeError.
|
|
367
|
+
* 55P03 = lock_timeout fired (FOR UPDATE could not acquire within 3s);
|
|
368
|
+
* 40P01 = deadlock_detected. Both bound the critical section so a busy org
|
|
369
|
+
* can never hang a request indefinitely. Any other error is rethrown as-is
|
|
370
|
+
* (including the BridgeError SESSION_FULL raised inside the quota block).
|
|
371
|
+
*/
|
|
372
|
+
_mapLockError(err) {
|
|
373
|
+
const code = err.code;
|
|
374
|
+
if (code === "55P03" || code === "40P01") {
|
|
375
|
+
throw new BridgeError(ErrorCode.LOCK_TIMEOUT, "Org quota check timed out under contention", "Too many concurrent requests for this org — retry in a moment");
|
|
376
|
+
}
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
/** Map a pg INSERT error to the file store's BridgeError contract. */
|
|
380
|
+
_mapCreateError(err, sessionId, label) {
|
|
381
|
+
const e = err;
|
|
382
|
+
if (e.code === "23505") {
|
|
383
|
+
// Positively identify the constraint (don't assume label by elimination).
|
|
384
|
+
if (e.constraint === "sessions_scope_label_uniq") {
|
|
385
|
+
throw new BridgeError(ErrorCode.INVALID_INPUT, `Label "${label}" already in use`, "Choose a different label");
|
|
386
|
+
}
|
|
387
|
+
// sessions_pkey (or any other dup) -> the session id is taken.
|
|
388
|
+
throw new BridgeError(ErrorCode.INVALID_INPUT, `Session ${sessionId} already exists`, "Use a different session ID or join the existing session");
|
|
389
|
+
}
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
//# sourceMappingURL=session-store-postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store-postgres.js","sourceRoot":"","sources":["../../src/services/session-store-postgres.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,73 @@
|
|
|
1
1
|
import type { TokenEntry, SessionFileState } from "../types.js";
|
|
2
|
+
/** Business-org fields persisted on a session, derived from the Org_ID at create time. */
|
|
3
|
+
export interface SessionOrg {
|
|
4
|
+
/** bcrypt hash of the Org_ID (auth verification). */
|
|
5
|
+
idHash: string;
|
|
6
|
+
/** SHA-256 of the Org_ID (stable label-scoping key; never auth). */
|
|
7
|
+
scope: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compute the label-index key. Free channels (no orgScope) use the BARE label
|
|
11
|
+
* so global free-channel uniqueness and resolution are unchanged (NFR6).
|
|
12
|
+
* Business channels namespace the label by their org scope, so two orgs can
|
|
13
|
+
* both hold `sprint-42` (Map keys compare by strict ===, so distinct strings
|
|
14
|
+
* are distinct keys).
|
|
15
|
+
*/
|
|
16
|
+
export declare function labelIndexKey(label: string, orgScope?: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Persistence contract for relay sessions.
|
|
19
|
+
*
|
|
20
|
+
* Relay services depend on this interface, not a concrete store, so the
|
|
21
|
+
* single-tenant file store (`FileSessionStore`) and the multi-tenant Postgres
|
|
22
|
+
* store (`PostgresSessionStore`, Story 2.4b) are interchangeable via constructor
|
|
23
|
+
* injection. `InMemorySessionStore` provides a dependency-free implementation
|
|
24
|
+
* for fast tests and the pre-Postgres business path.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionStore {
|
|
27
|
+
/** Initialize the store (rebuild indexes / open connections). */
|
|
28
|
+
init(): Promise<void>;
|
|
29
|
+
createSession(sessionId: string, label: string | undefined, secretHash: string, tokenEntry: TokenEntry, creatorIp?: string, org?: SessionOrg): Promise<SessionFileState>;
|
|
30
|
+
getSession(sessionId: string): Promise<SessionFileState | null>;
|
|
31
|
+
getSessionByTokenHash(tokenHash: string): Promise<{
|
|
32
|
+
sessionId: string;
|
|
33
|
+
state: SessionFileState;
|
|
34
|
+
} | null>;
|
|
35
|
+
updateSession(sessionId: string, updater: (state: SessionFileState) => SessionFileState): Promise<SessionFileState>;
|
|
36
|
+
addToken(sessionId: string, tokenEntry: TokenEntry): Promise<void>;
|
|
37
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
38
|
+
listSessions(): Promise<string[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Sum of registered peers across all sessions under `orgScope` (Business
|
|
41
|
+
* Edition). This is the SAME definition the per-org agent quota enforces (peer
|
|
42
|
+
* keys in each session's state document), so usage reporting and enforcement
|
|
43
|
+
* agree. Returns 0 for an unknown scope.
|
|
44
|
+
*/
|
|
45
|
+
countAgentsByOrgScope(orgScope: string): Promise<number>;
|
|
46
|
+
/**
|
|
47
|
+
* Upsert an org's enforced tier quota (Business Edition, Epic 5 Story 5.1). The
|
|
48
|
+
* control plane pushes the tier-derived limits; the relay persists them (keyed by
|
|
49
|
+
* `orgScope`) and row-locks the row to enforce channel/agent caps atomically.
|
|
50
|
+
* Idempotent — a re-send (e.g. a tier change, 5.7) overwrites the limits. The free
|
|
51
|
+
* single-tenant store has no per-org quotas, so its implementation is a no-op.
|
|
52
|
+
*/
|
|
53
|
+
setOrgQuota(orgScope: string, tier: string, maxChannels: number, maxAgents: number): Promise<void>;
|
|
54
|
+
/** Read-only token-hash -> sessionId index for auth middleware. */
|
|
55
|
+
getTokenIndex(): ReadonlyMap<string, string>;
|
|
56
|
+
getSessionIdByLabel(label: string, orgScope?: string): string | null;
|
|
57
|
+
isLabelTaken(label: string, orgScope?: string): boolean;
|
|
58
|
+
getGlobalMessageCount(): Promise<number>;
|
|
59
|
+
incrementGlobalMessageCount(): Promise<void>;
|
|
60
|
+
}
|
|
2
61
|
/**
|
|
3
62
|
* Per-session JSON file store with in-memory locking and atomic writes.
|
|
63
|
+
* The single-tenant (free relay) implementation of {@link SessionStore}.
|
|
4
64
|
*
|
|
5
65
|
* Each session is stored as `{sessionId}.json` in the state directory.
|
|
6
66
|
* A per-session promise-chain mutex prevents lost updates from concurrent
|
|
7
67
|
* requests targeting the same session. An in-memory Map provides O(1)
|
|
8
68
|
* token-to-session lookup, rebuilt from disk on startup.
|
|
9
69
|
*/
|
|
10
|
-
export declare class SessionStore {
|
|
70
|
+
export declare class FileSessionStore implements SessionStore {
|
|
11
71
|
private readonly stateDir;
|
|
12
72
|
private readonly maxMessagesPerSession;
|
|
13
73
|
/** Per-session promise-chain mutex. */
|
|
@@ -26,7 +86,7 @@ export declare class SessionStore {
|
|
|
26
86
|
* Create a new session with the given ID, label, secret hash, and initial token.
|
|
27
87
|
* Throws INVALID_INPUT if a session file already exists for this ID.
|
|
28
88
|
*/
|
|
29
|
-
createSession(sessionId: string, label: string | undefined, secretHash: string, tokenEntry: TokenEntry, creatorIp?: string): Promise<SessionFileState>;
|
|
89
|
+
createSession(sessionId: string, label: string | undefined, secretHash: string, tokenEntry: TokenEntry, creatorIp?: string, org?: SessionOrg): Promise<SessionFileState>;
|
|
30
90
|
/**
|
|
31
91
|
* Read a session by ID. Returns null if the session file does not exist.
|
|
32
92
|
*/
|
|
@@ -59,6 +119,8 @@ export declare class SessionStore {
|
|
|
59
119
|
* List all session IDs by scanning .json files in the state directory.
|
|
60
120
|
*/
|
|
61
121
|
listSessions(): Promise<string[]>;
|
|
122
|
+
countAgentsByOrgScope(orgScope: string): Promise<number>;
|
|
123
|
+
setOrgQuota(_orgScope: string, _tier: string, _maxChannels: number, _maxAgents: number): Promise<void>;
|
|
62
124
|
/**
|
|
63
125
|
* Return the token-to-session index (read-only) for auth middleware.
|
|
64
126
|
*/
|
|
@@ -67,11 +129,11 @@ export declare class SessionStore {
|
|
|
67
129
|
* Look up a session ID by its human-readable label.
|
|
68
130
|
* Returns null if no session has the given label.
|
|
69
131
|
*/
|
|
70
|
-
getSessionIdByLabel(label: string): string | null;
|
|
132
|
+
getSessionIdByLabel(label: string, orgScope?: string): string | null;
|
|
71
133
|
/**
|
|
72
|
-
* Check whether a label is already in use
|
|
134
|
+
* Check whether a label is already in use within the given org scope.
|
|
73
135
|
*/
|
|
74
|
-
isLabelTaken(label: string): boolean;
|
|
136
|
+
isLabelTaken(label: string, orgScope?: string): boolean;
|
|
75
137
|
/**
|
|
76
138
|
* Read and parse a session JSON file.
|
|
77
139
|
* Throws SESSION_NOT_FOUND if the file does not exist.
|
|
@@ -95,6 +157,12 @@ export declare class SessionStore {
|
|
|
95
157
|
private _rebuildTokenIndex;
|
|
96
158
|
/**
|
|
97
159
|
* Rebuild the label index entry for a single session.
|
|
160
|
+
*
|
|
161
|
+
* Fail-closed migration guard: a legacy business session (orgIdHash set but no
|
|
162
|
+
* orgScope — e.g. created before per-org scoping) must NOT be indexed under the
|
|
163
|
+
* public bare key, or its label would become cross-org enumerable via the
|
|
164
|
+
* public resolve endpoint. We cannot backfill orgScope (orgId is only stored
|
|
165
|
+
* hashed), so we skip indexing it; the channel remains usable by sessionId.
|
|
98
166
|
*/
|
|
99
167
|
private _rebuildLabelIndex;
|
|
100
168
|
/** Compute the file path for a session ID. */
|
|
@@ -1 +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
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/services/session-store.ts"],"names":[],"mappings":"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"}
|