@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,89 @@
|
|
|
1
|
+
import { BridgeError, ErrorCode } from "@essentialai/cogent";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory, two-tier rate limiter for session-join attempts.
|
|
4
|
+
*
|
|
5
|
+
* Tier 1 (per source+channel, `${ip}::${sessionId}`): a low threshold for good
|
|
6
|
+
* UX — a single brute-forcing client trips it quickly.
|
|
7
|
+
*
|
|
8
|
+
* Tier 2 (per channel, `channel::${sessionId}`): a higher threshold that is the
|
|
9
|
+
* SECURITY backstop. The channel id comes from the URL path and cannot be
|
|
10
|
+
* spoofed, so it holds even when an attacker rotates `X-Forwarded-For` /
|
|
11
|
+
* `X-Real-IP` to evade tier 1. `check()` blocks if EITHER tier is locked.
|
|
12
|
+
*
|
|
13
|
+
* Single-node MVP: state is process-local. Multi-node (vNext) moves this to a
|
|
14
|
+
* shared store (Postgres / Redis), at which point tier 1 can key on the
|
|
15
|
+
* trusted-proxy-validated real client IP. The clock is injectable for tests.
|
|
16
|
+
*
|
|
17
|
+
* Tradeoff (accepted): tier 2 means an attacker who knows a sessionId can lock
|
|
18
|
+
* NEW joins on that channel for `lockoutMs` with `channelMaxFailures` bad
|
|
19
|
+
* attempts. Already-joined agents keep their bearer tokens (join issues, does
|
|
20
|
+
* not gate, existing sessions). The higher tier-2 threshold keeps this from
|
|
21
|
+
* tripping on the rare legitimate failure; per-org quotas (E5) bound it further.
|
|
22
|
+
*/
|
|
23
|
+
export class JoinRateLimiter {
|
|
24
|
+
perSourceMax;
|
|
25
|
+
channelMax;
|
|
26
|
+
windowMs;
|
|
27
|
+
lockoutMs;
|
|
28
|
+
now;
|
|
29
|
+
attempts = new Map();
|
|
30
|
+
constructor(perSourceMax, channelMax, windowMs, lockoutMs, now = () => Date.now()) {
|
|
31
|
+
this.perSourceMax = perSourceMax;
|
|
32
|
+
this.channelMax = channelMax;
|
|
33
|
+
this.windowMs = windowMs;
|
|
34
|
+
this.lockoutMs = lockoutMs;
|
|
35
|
+
this.now = now;
|
|
36
|
+
}
|
|
37
|
+
/** Tier-1 key: source IP + channel (best-effort; IP may be spoofable). */
|
|
38
|
+
static sourceKey(ip, sessionId) {
|
|
39
|
+
return `${ip}::${sessionId}`;
|
|
40
|
+
}
|
|
41
|
+
/** Tier-2 key: channel only (non-spoofable; the security backstop). */
|
|
42
|
+
static channelKey(sessionId) {
|
|
43
|
+
return `channel::${sessionId}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Throw RATE_LIMITED if EITHER the source tier or the channel tier is locked.
|
|
47
|
+
* Call BEFORE verifying credentials.
|
|
48
|
+
*/
|
|
49
|
+
check(sourceKey, channelKey) {
|
|
50
|
+
const t = this.now();
|
|
51
|
+
const locked = (key) => {
|
|
52
|
+
const rec = this.attempts.get(key);
|
|
53
|
+
return rec && rec.lockedUntil > t ? rec.lockedUntil : 0;
|
|
54
|
+
};
|
|
55
|
+
const until = Math.max(locked(sourceKey), locked(channelKey));
|
|
56
|
+
if (until > 0) {
|
|
57
|
+
const retryAfterSec = Math.ceil((until - t) / 1000);
|
|
58
|
+
throw new BridgeError(ErrorCode.RATE_LIMITED, "Too many failed attempts", `Wait ${retryAfterSec}s before trying again`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Record a failed attempt against both tiers. */
|
|
62
|
+
recordFailure(sourceKey, channelKey) {
|
|
63
|
+
this._fail(sourceKey, this.perSourceMax);
|
|
64
|
+
this._fail(channelKey, this.channelMax);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clear the SOURCE tier after a successful join. The channel tier is left to
|
|
68
|
+
* decay with its window — a single success must not reset a channel-wide
|
|
69
|
+
* brute-force counter.
|
|
70
|
+
*/
|
|
71
|
+
recordSuccess(sourceKey) {
|
|
72
|
+
this.attempts.delete(sourceKey);
|
|
73
|
+
}
|
|
74
|
+
/** Increment a key's failure count within its window; lock past `threshold`. */
|
|
75
|
+
_fail(key, threshold) {
|
|
76
|
+
const t = this.now();
|
|
77
|
+
let rec = this.attempts.get(key);
|
|
78
|
+
// Start a fresh window if none exists or the window has elapsed.
|
|
79
|
+
if (!rec || t - rec.windowStart > this.windowMs) {
|
|
80
|
+
rec = { failures: 0, windowStart: t, lockedUntil: 0 };
|
|
81
|
+
}
|
|
82
|
+
rec.failures += 1;
|
|
83
|
+
if (rec.failures >= threshold) {
|
|
84
|
+
rec.lockedUntil = t + this.lockoutMs;
|
|
85
|
+
}
|
|
86
|
+
this.attempts.set(key, rec);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=join-rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-rate-limiter.js","sourceRoot":"","sources":["../../src/services/join-rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-org structured observability (Story 2.7).
|
|
3
|
+
*
|
|
4
|
+
* Emits one JSON line per event to stderr — the project's logging convention
|
|
5
|
+
* (console.error + JSON.stringify, as in middleware/request-logger.ts; vault:
|
|
6
|
+
* Error_Reporting.md §"Runtime Error Reporting" — runtime issues are surfaced
|
|
7
|
+
* via console output / logging mechanisms).
|
|
8
|
+
*
|
|
9
|
+
* Every event carries `org_id`: the session's **org_scope** (SHA-256 of the
|
|
10
|
+
* Org_ID) — a stable, non-reversible org tag, NOT the Org_ID itself. The scope
|
|
11
|
+
* is already stored in plaintext (sessions.org_scope) and a SHA-256 of a
|
|
12
|
+
* high-entropy Org_ID is not brute-forceable, so it is safe to log. Free/public
|
|
13
|
+
* channels have no org → `org_id: null`.
|
|
14
|
+
*
|
|
15
|
+
* SECURITY NOTE: org_scope is an UNSALTED SHA-256. It is safe to log only
|
|
16
|
+
* because the Org_ID is required to be high-entropy (the join credential is "a
|
|
17
|
+
* long Org_ID that is hard to brute-force"); a low-entropy/human-chosen Org_ID
|
|
18
|
+
* would let a logged scope confirm a guess offline. Enforce Org_ID entropy at
|
|
19
|
+
* the control plane (E3).
|
|
20
|
+
*
|
|
21
|
+
* Secrets NEVER reach the log: `redact()` strips any field whose key looks
|
|
22
|
+
* sensitive (secret/password/token/orgId/hash/authorization/bearer/credential),
|
|
23
|
+
* so a careless caller cannot leak credentials through the `detail` payload.
|
|
24
|
+
* This makes "no secret values in logs" structural, not by-convention.
|
|
25
|
+
*/
|
|
26
|
+
export type OrgEventName = "quota_check" | "auth_failure" | "message_relay";
|
|
27
|
+
export interface OrgEvent {
|
|
28
|
+
event: OrgEventName;
|
|
29
|
+
/** org_scope (SHA-256). null/undefined → free/public channel (no org). */
|
|
30
|
+
orgScope?: string | null;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
/** e.g. "admitted" | "rejected" | "invalid_credentials" | "relayed". */
|
|
33
|
+
outcome?: string;
|
|
34
|
+
/** Extra non-secret context; sensitive keys are redacted defensively. */
|
|
35
|
+
detail?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Return a copy of `obj` with sensitive values replaced by "[REDACTED]".
|
|
39
|
+
* Recurses into nested plain objects AND arrays; exotic objects (Error/Map/etc.)
|
|
40
|
+
* are collapsed to a safe tag — so "no secrets in logs" holds even if a future
|
|
41
|
+
* caller passes structured detail.
|
|
42
|
+
*/
|
|
43
|
+
export declare function redact(obj: Record<string, unknown>): Record<string, unknown>;
|
|
44
|
+
/** Test seam: swap the output sink. Pass no arg to restore the default. */
|
|
45
|
+
export declare function _setObsSink(fn?: (line: string) => void): void;
|
|
46
|
+
/**
|
|
47
|
+
* Emit one structured org-tagged event. `org_id` is always present (null for
|
|
48
|
+
* free channels). The `detail` payload is redacted before serialization.
|
|
49
|
+
*/
|
|
50
|
+
export declare function logOrgEvent(e: OrgEvent): void;
|
|
51
|
+
//# sourceMappingURL=obs-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obs-log.d.ts","sourceRoot":"","sources":["../../src/services/obs-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,eAAe,CAAC;AAE5E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAkBD;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAM5E;AAKD,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE7D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAsB7C"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-org structured observability (Story 2.7).
|
|
3
|
+
*
|
|
4
|
+
* Emits one JSON line per event to stderr — the project's logging convention
|
|
5
|
+
* (console.error + JSON.stringify, as in middleware/request-logger.ts; vault:
|
|
6
|
+
* Error_Reporting.md §"Runtime Error Reporting" — runtime issues are surfaced
|
|
7
|
+
* via console output / logging mechanisms).
|
|
8
|
+
*
|
|
9
|
+
* Every event carries `org_id`: the session's **org_scope** (SHA-256 of the
|
|
10
|
+
* Org_ID) — a stable, non-reversible org tag, NOT the Org_ID itself. The scope
|
|
11
|
+
* is already stored in plaintext (sessions.org_scope) and a SHA-256 of a
|
|
12
|
+
* high-entropy Org_ID is not brute-forceable, so it is safe to log. Free/public
|
|
13
|
+
* channels have no org → `org_id: null`.
|
|
14
|
+
*
|
|
15
|
+
* SECURITY NOTE: org_scope is an UNSALTED SHA-256. It is safe to log only
|
|
16
|
+
* because the Org_ID is required to be high-entropy (the join credential is "a
|
|
17
|
+
* long Org_ID that is hard to brute-force"); a low-entropy/human-chosen Org_ID
|
|
18
|
+
* would let a logged scope confirm a guess offline. Enforce Org_ID entropy at
|
|
19
|
+
* the control plane (E3).
|
|
20
|
+
*
|
|
21
|
+
* Secrets NEVER reach the log: `redact()` strips any field whose key looks
|
|
22
|
+
* sensitive (secret/password/token/orgId/hash/authorization/bearer/credential),
|
|
23
|
+
* so a careless caller cannot leak credentials through the `detail` payload.
|
|
24
|
+
* This makes "no secret values in logs" structural, not by-convention.
|
|
25
|
+
*/
|
|
26
|
+
/** Keys whose VALUES must never be logged (substring, case-insensitive). */
|
|
27
|
+
const SENSITIVE = /(secret|password|passwd|token|org[_-]?id|authorization|bearer|hash|credential)/i;
|
|
28
|
+
/** Recursively neutralize a value so no secret can hide inside it. */
|
|
29
|
+
function redactValue(v) {
|
|
30
|
+
if (v === null || typeof v !== "object")
|
|
31
|
+
return v; // scalars pass through
|
|
32
|
+
if (Array.isArray(v))
|
|
33
|
+
return v.map(redactValue); // scan array elements too
|
|
34
|
+
const proto = Object.getPrototypeOf(v);
|
|
35
|
+
// Exotic objects (Error, Map, Set, Date, class instances) can carry data in
|
|
36
|
+
// non-enumerable or non-string-keyed fields that key-scanning can't see —
|
|
37
|
+
// collapse them to a safe tag rather than risk a leak.
|
|
38
|
+
if (proto !== Object.prototype && proto !== null)
|
|
39
|
+
return "[REDACTED:object]";
|
|
40
|
+
return redact(v);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Return a copy of `obj` with sensitive values replaced by "[REDACTED]".
|
|
44
|
+
* Recurses into nested plain objects AND arrays; exotic objects (Error/Map/etc.)
|
|
45
|
+
* are collapsed to a safe tag — so "no secrets in logs" holds even if a future
|
|
46
|
+
* caller passes structured detail.
|
|
47
|
+
*/
|
|
48
|
+
export function redact(obj) {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
51
|
+
out[k] = SENSITIVE.test(k) ? "[REDACTED]" : redactValue(v);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Output sink — overridable for tests via {@link _setObsSink}. */
|
|
56
|
+
let sink = (line) => console.error(line);
|
|
57
|
+
/** Test seam: swap the output sink. Pass no arg to restore the default. */
|
|
58
|
+
export function _setObsSink(fn) {
|
|
59
|
+
sink = fn ?? ((line) => console.error(line));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Emit one structured org-tagged event. `org_id` is always present (null for
|
|
63
|
+
* free channels). The `detail` payload is redacted before serialization.
|
|
64
|
+
*/
|
|
65
|
+
export function logOrgEvent(e) {
|
|
66
|
+
// Observability must NEVER break a request: a bad value in detail (BigInt,
|
|
67
|
+
// circular ref) would otherwise throw from JSON.stringify into a hot path or
|
|
68
|
+
// a committed relay. Swallow any serialization/sink error.
|
|
69
|
+
try {
|
|
70
|
+
const entry = {
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
level: e.event === "auth_failure" ? "warn" : "info",
|
|
73
|
+
message: e.event,
|
|
74
|
+
org_id: e.orgScope ?? null,
|
|
75
|
+
};
|
|
76
|
+
if (e.sessionId !== undefined)
|
|
77
|
+
entry.sessionId = e.sessionId;
|
|
78
|
+
if (e.outcome !== undefined)
|
|
79
|
+
entry.outcome = e.outcome;
|
|
80
|
+
if (e.detail)
|
|
81
|
+
entry.detail = redact(e.detail);
|
|
82
|
+
sink(JSON.stringify(entry));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
try {
|
|
86
|
+
sink(JSON.stringify({ level: "error", message: "obs_log_failure", event: e?.event ?? null }));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* give up — never propagate a logging error */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=obs-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obs-log.js","sourceRoot":"","sources":["../../src/services/obs-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAeH,4EAA4E;AAC5E,MAAM,SAAS,GACb,iFAAiF,CAAC;AAEpF,sEAAsE;AACtE,SAAS,WAAW,CAAC,CAAU;IAC7B,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC,CAAC,uBAAuB;IAC1E,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,0BAA0B;IAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IACvC,4EAA4E;IAC5E,0EAA0E;IAC1E,uDAAuD;IACvD,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,mBAAmB,CAAC;IAC7E,OAAO,MAAM,CAAC,CAA4B,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,GAA4B;IACjD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,mEAAmE;AACnE,IAAI,IAAI,GAA2B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAEjE,2EAA2E;AAC3E,MAAM,UAAU,WAAW,CAAC,EAA2B;IACrD,IAAI,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,CAAW;IACrC,2EAA2E;IAC3E,6EAA6E;IAC7E,2DAA2D;IAC3D,IAAI,CAAC;QACH,MAAM,KAAK,GAA4B;YACrC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;YACnD,OAAO,EAAE,CAAC,CAAC,KAAK;YAChB,MAAM,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;SAC3B,CAAC;QACF,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS;YAAE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;QAC7D,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QACvD,IAAI,CAAC,CAAC,MAAM;YAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QAChG,CAAC;QAAC,MAAM,CAAC;YACP,+CAA+C;QACjD,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { TokenEntry, SessionFileState } from "../types.js";
|
|
2
|
+
import type { SessionStore, SessionOrg } from "./session-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dependency-free, in-memory implementation of {@link SessionStore}.
|
|
5
|
+
*
|
|
6
|
+
* Backs sessions with a `Map` instead of the filesystem, but preserves the
|
|
7
|
+
* file store's observable semantics exactly: a per-session promise-chain mutex,
|
|
8
|
+
* label uniqueness, message cap, token/label indexes, and the same BridgeError
|
|
9
|
+
* throw-contract. Used for fast, isolated tests and as the pre-Postgres business
|
|
10
|
+
* substrate that `PostgresSessionStore` (Story 2.4b) parallels.
|
|
11
|
+
*
|
|
12
|
+
* Stored state is deep-cloned on every read and write (via `structuredClone`)
|
|
13
|
+
* so callers cannot mutate the store by reference — matching the file store,
|
|
14
|
+
* which re-reads fresh objects from disk on each access.
|
|
15
|
+
*/
|
|
16
|
+
export declare class InMemorySessionStore implements SessionStore {
|
|
17
|
+
private readonly maxMessagesPerSession;
|
|
18
|
+
private readonly sessions;
|
|
19
|
+
private readonly locks;
|
|
20
|
+
private readonly tokenIndex;
|
|
21
|
+
private readonly labelIndex;
|
|
22
|
+
private globalMessageCount;
|
|
23
|
+
constructor(maxMessagesPerSession: number);
|
|
24
|
+
/** No persisted state to rebuild. */
|
|
25
|
+
init(): Promise<void>;
|
|
26
|
+
createSession(sessionId: string, label: string | undefined, secretHash: string, tokenEntry: TokenEntry, creatorIp?: string, org?: SessionOrg): Promise<SessionFileState>;
|
|
27
|
+
getSession(sessionId: string): Promise<SessionFileState | null>;
|
|
28
|
+
getSessionByTokenHash(tokenHash: string): Promise<{
|
|
29
|
+
sessionId: string;
|
|
30
|
+
state: SessionFileState;
|
|
31
|
+
} | null>;
|
|
32
|
+
updateSession(sessionId: string, updater: (state: SessionFileState) => SessionFileState): Promise<SessionFileState>;
|
|
33
|
+
addToken(sessionId: string, tokenEntry: TokenEntry): Promise<void>;
|
|
34
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
35
|
+
listSessions(): Promise<string[]>;
|
|
36
|
+
countAgentsByOrgScope(orgScope: string): Promise<number>;
|
|
37
|
+
/** In-memory org quotas (Story 5.1). Held so the store faithfully implements the
|
|
38
|
+
* interface; this store does not enforce caps (enforcement is PostgresSessionStore). */
|
|
39
|
+
private readonly orgQuotas;
|
|
40
|
+
setOrgQuota(orgScope: string, tier: string, maxChannels: number, maxAgents: number): Promise<void>;
|
|
41
|
+
/** Read back an org's in-memory quota (test/introspection; not on the interface). */
|
|
42
|
+
getOrgQuota(orgScope: string): {
|
|
43
|
+
tier: string;
|
|
44
|
+
maxChannels: number;
|
|
45
|
+
maxAgents: number;
|
|
46
|
+
} | undefined;
|
|
47
|
+
getTokenIndex(): ReadonlyMap<string, string>;
|
|
48
|
+
getSessionIdByLabel(label: string, orgScope?: string): string | null;
|
|
49
|
+
isLabelTaken(label: string, orgScope?: string): boolean;
|
|
50
|
+
getGlobalMessageCount(): Promise<number>;
|
|
51
|
+
incrementGlobalMessageCount(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Per-session promise-chain mutex. Serializes all operations on a given
|
|
54
|
+
* session to prevent lost updates from concurrent requests.
|
|
55
|
+
*/
|
|
56
|
+
private _withLock;
|
|
57
|
+
private _rebuildTokenIndex;
|
|
58
|
+
private _rebuildLabelIndex;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=session-store-memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store-memory.d.ts","sourceRoot":"","sources":["../../src/services/session-store-memory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnE;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAE/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,kBAAkB,CAAK;gBAEnB,qBAAqB,EAAE,MAAM;IAIzC,qCAAqC;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,UAAU,EACtB,SAAS,CAAC,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,UAAU,GACf,OAAO,CAAC,gBAAgB,CAAC;IAsCtB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAK/D,qBAAqB,CACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,GAAG,IAAI,CAAC;IAa3D,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,gBAAgB,GACrD,OAAO,CAAC,gBAAgB,CAAC;IAsBtB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAelE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB/C,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIjC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAU9D;6FACyF;IACzF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+E;IAEnG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxG,qFAAqF;IACrF,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAInG,aAAa,IAAI,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;IAI5C,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIpE,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAIjD,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxC,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;IAQlD;;;OAGG;YACW,SAAS;IAwBvB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,kBAAkB;CAO3B"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { BridgeError, ErrorCode } from "@essentialai/cogent";
|
|
2
|
+
import { labelIndexKey } from "./session-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dependency-free, in-memory implementation of {@link SessionStore}.
|
|
5
|
+
*
|
|
6
|
+
* Backs sessions with a `Map` instead of the filesystem, but preserves the
|
|
7
|
+
* file store's observable semantics exactly: a per-session promise-chain mutex,
|
|
8
|
+
* label uniqueness, message cap, token/label indexes, and the same BridgeError
|
|
9
|
+
* throw-contract. Used for fast, isolated tests and as the pre-Postgres business
|
|
10
|
+
* substrate that `PostgresSessionStore` (Story 2.4b) parallels.
|
|
11
|
+
*
|
|
12
|
+
* Stored state is deep-cloned on every read and write (via `structuredClone`)
|
|
13
|
+
* so callers cannot mutate the store by reference — matching the file store,
|
|
14
|
+
* which re-reads fresh objects from disk on each access.
|
|
15
|
+
*/
|
|
16
|
+
export class InMemorySessionStore {
|
|
17
|
+
maxMessagesPerSession;
|
|
18
|
+
sessions = new Map();
|
|
19
|
+
locks = new Map();
|
|
20
|
+
tokenIndex = new Map();
|
|
21
|
+
labelIndex = new Map();
|
|
22
|
+
globalMessageCount = 0;
|
|
23
|
+
constructor(maxMessagesPerSession) {
|
|
24
|
+
this.maxMessagesPerSession = maxMessagesPerSession;
|
|
25
|
+
}
|
|
26
|
+
/** No persisted state to rebuild. */
|
|
27
|
+
async init() {
|
|
28
|
+
// no-op
|
|
29
|
+
}
|
|
30
|
+
async createSession(sessionId, label, secretHash, tokenEntry, creatorIp, org) {
|
|
31
|
+
return this._withLock(sessionId, async () => {
|
|
32
|
+
if (this.sessions.has(sessionId)) {
|
|
33
|
+
throw new BridgeError(ErrorCode.INVALID_INPUT, `Session ${sessionId} already exists`, "Use a different session ID or join the existing session");
|
|
34
|
+
}
|
|
35
|
+
if (label !== undefined && this.isLabelTaken(label, org?.scope)) {
|
|
36
|
+
throw new BridgeError(ErrorCode.INVALID_INPUT, `Label "${label}" already in use`, "Choose a different label");
|
|
37
|
+
}
|
|
38
|
+
const state = {
|
|
39
|
+
sessionId,
|
|
40
|
+
...(label !== undefined ? { label } : {}),
|
|
41
|
+
secretHash,
|
|
42
|
+
...(org !== undefined ? { orgIdHash: org.idHash, orgScope: org.scope } : {}),
|
|
43
|
+
tokens: [tokenEntry],
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
...(creatorIp ? { creatorIp } : {}),
|
|
46
|
+
peers: {},
|
|
47
|
+
messages: [],
|
|
48
|
+
peerEvents: [],
|
|
49
|
+
};
|
|
50
|
+
this.sessions.set(sessionId, structuredClone(state));
|
|
51
|
+
this._rebuildTokenIndex(state);
|
|
52
|
+
this._rebuildLabelIndex(state);
|
|
53
|
+
return structuredClone(state);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async getSession(sessionId) {
|
|
57
|
+
const state = this.sessions.get(sessionId);
|
|
58
|
+
return state ? structuredClone(state) : null;
|
|
59
|
+
}
|
|
60
|
+
async getSessionByTokenHash(tokenHash) {
|
|
61
|
+
const sessionId = this.tokenIndex.get(tokenHash);
|
|
62
|
+
if (!sessionId)
|
|
63
|
+
return null;
|
|
64
|
+
const state = await this.getSession(sessionId);
|
|
65
|
+
if (!state) {
|
|
66
|
+
this.tokenIndex.delete(tokenHash);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return { sessionId, state };
|
|
70
|
+
}
|
|
71
|
+
async updateSession(sessionId, updater) {
|
|
72
|
+
return this._withLock(sessionId, async () => {
|
|
73
|
+
const current = this.sessions.get(sessionId);
|
|
74
|
+
if (!current) {
|
|
75
|
+
throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
|
|
76
|
+
}
|
|
77
|
+
const updated = updater(structuredClone(current));
|
|
78
|
+
if (updated.messages.length > this.maxMessagesPerSession) {
|
|
79
|
+
updated.messages = updated.messages.slice(-this.maxMessagesPerSession);
|
|
80
|
+
}
|
|
81
|
+
this.sessions.set(sessionId, structuredClone(updated));
|
|
82
|
+
return structuredClone(updated);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async addToken(sessionId, tokenEntry) {
|
|
86
|
+
await this._withLock(sessionId, async () => {
|
|
87
|
+
const current = this.sessions.get(sessionId);
|
|
88
|
+
if (!current) {
|
|
89
|
+
throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`, "Check the session ID or create a new session");
|
|
90
|
+
}
|
|
91
|
+
current.tokens.push(structuredClone(tokenEntry));
|
|
92
|
+
this.tokenIndex.set(tokenEntry.tokenHash, sessionId);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async deleteSession(sessionId) {
|
|
96
|
+
await this._withLock(sessionId, async () => {
|
|
97
|
+
const state = this.sessions.get(sessionId);
|
|
98
|
+
if (state) {
|
|
99
|
+
for (const token of state.tokens) {
|
|
100
|
+
this.tokenIndex.delete(token.tokenHash);
|
|
101
|
+
}
|
|
102
|
+
if (state.label) {
|
|
103
|
+
this.labelIndex.delete(labelIndexKey(state.label, state.orgScope));
|
|
104
|
+
}
|
|
105
|
+
this.sessions.delete(sessionId);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// Lock entry pruned inside _withLock (prune-if-mine), not out-of-band here.
|
|
109
|
+
}
|
|
110
|
+
async listSessions() {
|
|
111
|
+
return [...this.sessions.keys()];
|
|
112
|
+
}
|
|
113
|
+
async countAgentsByOrgScope(orgScope) {
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const state of this.sessions.values()) {
|
|
116
|
+
if (state.orgScope === orgScope) {
|
|
117
|
+
total += Object.keys(state.peers).length;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return total;
|
|
121
|
+
}
|
|
122
|
+
/** In-memory org quotas (Story 5.1). Held so the store faithfully implements the
|
|
123
|
+
* interface; this store does not enforce caps (enforcement is PostgresSessionStore). */
|
|
124
|
+
orgQuotas = new Map();
|
|
125
|
+
async setOrgQuota(orgScope, tier, maxChannels, maxAgents) {
|
|
126
|
+
this.orgQuotas.set(orgScope, { tier, maxChannels, maxAgents });
|
|
127
|
+
}
|
|
128
|
+
/** Read back an org's in-memory quota (test/introspection; not on the interface). */
|
|
129
|
+
getOrgQuota(orgScope) {
|
|
130
|
+
return this.orgQuotas.get(orgScope);
|
|
131
|
+
}
|
|
132
|
+
getTokenIndex() {
|
|
133
|
+
return this.tokenIndex;
|
|
134
|
+
}
|
|
135
|
+
getSessionIdByLabel(label, orgScope) {
|
|
136
|
+
return this.labelIndex.get(labelIndexKey(label, orgScope)) ?? null;
|
|
137
|
+
}
|
|
138
|
+
isLabelTaken(label, orgScope) {
|
|
139
|
+
return this.labelIndex.has(labelIndexKey(label, orgScope));
|
|
140
|
+
}
|
|
141
|
+
async getGlobalMessageCount() {
|
|
142
|
+
return this.globalMessageCount;
|
|
143
|
+
}
|
|
144
|
+
async incrementGlobalMessageCount() {
|
|
145
|
+
await this._withLock("__global_stats__", async () => {
|
|
146
|
+
this.globalMessageCount += 1;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// --- Private methods ---
|
|
150
|
+
/**
|
|
151
|
+
* Per-session promise-chain mutex. Serializes all operations on a given
|
|
152
|
+
* session to prevent lost updates from concurrent requests.
|
|
153
|
+
*/
|
|
154
|
+
async _withLock(sessionId, fn) {
|
|
155
|
+
const prev = this.locks.get(sessionId) ?? Promise.resolve();
|
|
156
|
+
let release;
|
|
157
|
+
const next = new Promise((resolve) => {
|
|
158
|
+
release = resolve;
|
|
159
|
+
});
|
|
160
|
+
this.locks.set(sessionId, next);
|
|
161
|
+
await prev;
|
|
162
|
+
try {
|
|
163
|
+
return await fn();
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
release();
|
|
167
|
+
// Prune only if still ours -- bounds map growth without evicting a queued
|
|
168
|
+
// waiter (see FileSessionStore._withLock for the rationale).
|
|
169
|
+
if (this.locks.get(sessionId) === next) {
|
|
170
|
+
this.locks.delete(sessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
_rebuildTokenIndex(state) {
|
|
175
|
+
for (const token of state.tokens) {
|
|
176
|
+
this.tokenIndex.set(token.tokenHash, state.sessionId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
_rebuildLabelIndex(state) {
|
|
180
|
+
if (!state.label)
|
|
181
|
+
return;
|
|
182
|
+
// Fail-closed: never index a legacy business session (orgIdHash, no orgScope)
|
|
183
|
+
// under the public bare key (see FileSessionStore._rebuildLabelIndex).
|
|
184
|
+
if (state.orgIdHash !== undefined && state.orgScope === undefined)
|
|
185
|
+
return;
|
|
186
|
+
this.labelIndex.set(labelIndexKey(state.label, state.orgScope), state.sessionId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=session-store-memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store-memory.js","sourceRoot":"","sources":["../../src/services/session-store-memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import type { TokenEntry, SessionFileState } from "../types.js";
|
|
3
|
+
import type { SessionStore, SessionOrg } from "./session-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Postgres implementation of {@link SessionStore} (Business Edition, FR35),
|
|
6
|
+
* JSONB-hybrid: queryable/unique/auth scalars are columns; the rest of the
|
|
7
|
+
* session document lives in a `state jsonb` column — a 1:1 mirror of the file
|
|
8
|
+
* store's per-session JSON, so `updateSession` is a whole-document UPDATE.
|
|
9
|
+
*
|
|
10
|
+
* The synchronous index methods (`getSessionIdByLabel`, `isLabelTaken`,
|
|
11
|
+
* `getTokenIndex`) are served from in-memory caches rebuilt on `init()` and kept
|
|
12
|
+
* in sync on writes — exactly as `FileSessionStore` does. This is correct for
|
|
13
|
+
* the single-node MVP (all writes go through this instance); multi-node (vNext)
|
|
14
|
+
* will need cache invalidation or a shared lookup. `getSessionByTokenHash`
|
|
15
|
+
* queries the DB directly so bearer-token auth is always authoritative.
|
|
16
|
+
*
|
|
17
|
+
* The pool is injected (DI) and owned by the caller (index.ts closes it).
|
|
18
|
+
*/
|
|
19
|
+
export declare class PostgresSessionStore implements SessionStore {
|
|
20
|
+
private readonly pool;
|
|
21
|
+
private readonly maxMessagesPerSession;
|
|
22
|
+
/** label-scope key -> sessionId (mirrors FileSessionStore.labelIndex). */
|
|
23
|
+
private readonly labelIndex;
|
|
24
|
+
/** SHA-256 tokenHash -> sessionId. */
|
|
25
|
+
private readonly tokenIndex;
|
|
26
|
+
constructor(pool: Pool, maxMessagesPerSession: number);
|
|
27
|
+
init(): Promise<void>;
|
|
28
|
+
createSession(sessionId: string, label: string | undefined, secretHash: string, tokenEntry: TokenEntry, creatorIp?: string, org?: SessionOrg): Promise<SessionFileState>;
|
|
29
|
+
getSession(sessionId: string): Promise<SessionFileState | null>;
|
|
30
|
+
getSessionByTokenHash(tokenHash: string): Promise<{
|
|
31
|
+
sessionId: string;
|
|
32
|
+
state: SessionFileState;
|
|
33
|
+
} | null>;
|
|
34
|
+
updateSession(sessionId: string, updater: (state: SessionFileState) => SessionFileState): Promise<SessionFileState>;
|
|
35
|
+
addToken(sessionId: string, tokenEntry: TokenEntry): Promise<void>;
|
|
36
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
37
|
+
listSessions(): Promise<string[]>;
|
|
38
|
+
countAgentsByOrgScope(orgScope: string): Promise<number>;
|
|
39
|
+
setOrgQuota(orgScope: string, tier: string, maxChannels: number, maxAgents: number): Promise<void>;
|
|
40
|
+
getTokenIndex(): ReadonlyMap<string, string>;
|
|
41
|
+
getSessionIdByLabel(label: string, orgScope?: string): string | null;
|
|
42
|
+
isLabelTaken(label: string, orgScope?: string): boolean;
|
|
43
|
+
getGlobalMessageCount(): Promise<number>;
|
|
44
|
+
incrementGlobalMessageCount(): Promise<void>;
|
|
45
|
+
/** Assemble a SessionFileState from a sessions row + its token rows. */
|
|
46
|
+
private _rowToState;
|
|
47
|
+
/** Extract the JSONB document (everything not in a scalar column or tokens). */
|
|
48
|
+
private _docFromState;
|
|
49
|
+
/**
|
|
50
|
+
* Map a row-lock contention failure to a clean, retryable BridgeError.
|
|
51
|
+
* 55P03 = lock_timeout fired (FOR UPDATE could not acquire within 3s);
|
|
52
|
+
* 40P01 = deadlock_detected. Both bound the critical section so a busy org
|
|
53
|
+
* can never hang a request indefinitely. Any other error is rethrown as-is
|
|
54
|
+
* (including the BridgeError SESSION_FULL raised inside the quota block).
|
|
55
|
+
*/
|
|
56
|
+
private _mapLockError;
|
|
57
|
+
/** Map a pg INSERT error to the file store's BridgeError contract. */
|
|
58
|
+
private _mapCreateError;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=session-store-postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store-postgres.d.ts","sourceRoot":"","sources":["../../src/services/session-store-postgres.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAkCnE;;;;;;;;;;;;;;GAcG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAE/C,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,sCAAsC;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;gBAE5C,IAAI,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM;IAK/C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BrB,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,UAAU,EACtB,SAAS,CAAC,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,UAAU,GACf,OAAO,CAAC,gBAAgB,CAAC;IAwGtB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAgB/D,qBAAqB,CACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,GAAG,IAAI,CAAC;IAa3D,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,gBAAgB,GACrD,OAAO,CAAC,gBAAgB,CAAC;IAqItB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BlE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB/C,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAKjC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAexD,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBxG,aAAa,IAAI,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;IAI5C,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIpE,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAIjD,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC;IAOxC,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;IAOlD,wEAAwE;IACxE,OAAO,CAAC,WAAW;IAuBnB,gFAAgF;IAChF,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAYrB,sEAAsE;IACtE,OAAO,CAAC,eAAe;CAoBxB"}
|