@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,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Relay-side mirror of the control-plane ↔ relay API contract.
|
|
4
|
+
*
|
|
5
|
+
* The control plane (cogent-business, a SEPARATELY deployed service) defines the
|
|
6
|
+
* authoritative contract in `api/src/contract/relay-contract.ts`. The two repos
|
|
7
|
+
* cannot share a package, so the wire shape is duplicated here BY NECESSITY and
|
|
8
|
+
* pinned on both sides — the CP keeps `relay-contract.test.ts`, the relay keeps
|
|
9
|
+
* `control-plane.test.ts`, so a breaking change to either is caught before deploy.
|
|
10
|
+
* Vault: typescript-master-vault/concepts/Typesafe_Protocols.md (versioned
|
|
11
|
+
* request-response protocol) + Software_Version.md (semver major as the
|
|
12
|
+
* back-compat boundary).
|
|
13
|
+
*
|
|
14
|
+
* Hash discipline: the CP sends ALREADY-DERIVED hashes — `orgScope` = SHA-256(Org_ID)
|
|
15
|
+
* and `orgIdHash` = bcrypt(Org_ID). The relay stores these verbatim (it never sees
|
|
16
|
+
* the plaintext Org_ID). Only the channel `secret` arrives in plaintext (over the
|
|
17
|
+
* privileged TLS link) and is bcrypt-hashed HERE before storage.
|
|
18
|
+
*/
|
|
19
|
+
/** Major the relay supports. Reject any request whose major differs. */
|
|
20
|
+
export declare const CONTROL_PLANE_API_MAJOR = 1;
|
|
21
|
+
/** True if a contract version string is compatible with this relay's major. */
|
|
22
|
+
export declare function isCompatibleVersion(version: string): boolean;
|
|
23
|
+
declare const createChannelPayload: z.ZodObject<{
|
|
24
|
+
orgScope: z.ZodString;
|
|
25
|
+
orgIdHash: z.ZodString;
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
secret: z.ZodString;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
declare const deleteChannelPayload: z.ZodObject<{
|
|
30
|
+
relaySessionId: z.ZodString;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
declare const rotateOrgPayload: z.ZodObject<{
|
|
33
|
+
oldOrgScope: z.ZodString;
|
|
34
|
+
newOrgScope: z.ZodString;
|
|
35
|
+
newOrgIdHash: z.ZodString;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
/**
|
|
38
|
+
* Set an org's enforced tier quota (Epic 5, Story 5.1, FR20). The control plane owns
|
|
39
|
+
* the tier→quota mapping; the relay just persists the limits it enforces (the
|
|
40
|
+
* `org_quotas` row, keyed by `orgScope`) + the `tier` label for observability. Upsert
|
|
41
|
+
* semantics — idempotent, and a later tier change re-sends with new limits (5.7).
|
|
42
|
+
*/
|
|
43
|
+
declare const setOrgQuotaPayload: z.ZodObject<{
|
|
44
|
+
orgScope: z.ZodString;
|
|
45
|
+
tier: z.ZodString;
|
|
46
|
+
maxChannels: z.ZodNumber;
|
|
47
|
+
maxAgents: z.ZodNumber;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
/**
|
|
50
|
+
* Wire envelope, discriminated on `op`. `version` is validated separately (by
|
|
51
|
+
* major) so an incompatible-version request yields a clear version error rather
|
|
52
|
+
* than an opaque "op invalid" — see the route.
|
|
53
|
+
*/
|
|
54
|
+
export declare const RelayRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
55
|
+
version: z.ZodString;
|
|
56
|
+
op: z.ZodLiteral<"channel.create">;
|
|
57
|
+
payload: z.ZodObject<{
|
|
58
|
+
orgScope: z.ZodString;
|
|
59
|
+
orgIdHash: z.ZodString;
|
|
60
|
+
name: z.ZodString;
|
|
61
|
+
secret: z.ZodString;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
64
|
+
version: z.ZodString;
|
|
65
|
+
op: z.ZodLiteral<"channel.delete">;
|
|
66
|
+
payload: z.ZodObject<{
|
|
67
|
+
relaySessionId: z.ZodString;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
70
|
+
version: z.ZodString;
|
|
71
|
+
op: z.ZodLiteral<"org.rotate">;
|
|
72
|
+
payload: z.ZodObject<{
|
|
73
|
+
oldOrgScope: z.ZodString;
|
|
74
|
+
newOrgScope: z.ZodString;
|
|
75
|
+
newOrgIdHash: z.ZodString;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
78
|
+
version: z.ZodString;
|
|
79
|
+
op: z.ZodLiteral<"org.setQuota">;
|
|
80
|
+
payload: z.ZodObject<{
|
|
81
|
+
orgScope: z.ZodString;
|
|
82
|
+
tier: z.ZodString;
|
|
83
|
+
maxChannels: z.ZodNumber;
|
|
84
|
+
maxAgents: z.ZodNumber;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
}, z.core.$strip>], "op">;
|
|
87
|
+
export type RelayRequest = z.infer<typeof RelayRequestSchema>;
|
|
88
|
+
export type CreateChannelPayload = z.infer<typeof createChannelPayload>;
|
|
89
|
+
export type DeleteChannelPayload = z.infer<typeof deleteChannelPayload>;
|
|
90
|
+
export type RotateOrgPayload = z.infer<typeof rotateOrgPayload>;
|
|
91
|
+
export type SetOrgQuotaPayload = z.infer<typeof setOrgQuotaPayload>;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=control-plane-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-plane-contract.d.ts","sourceRoot":"","sources":["../../src/contract/control-plane-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;GAgBG;AAEH,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED,QAAA,MAAM,oBAAoB;;;;;iBASxB,CAAC;AAEH,QAAA,MAAM,oBAAoB;;iBAGxB,CAAC;AAEH,QAAA,MAAM,gBAAgB;;;;iBAIpB,CAAC;AAEH;;;;;GAKG;AACH,QAAA,MAAM,kBAAkB;;;;;iBAStB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAK7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACxE,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Relay-side mirror of the control-plane ↔ relay API contract.
|
|
4
|
+
*
|
|
5
|
+
* The control plane (cogent-business, a SEPARATELY deployed service) defines the
|
|
6
|
+
* authoritative contract in `api/src/contract/relay-contract.ts`. The two repos
|
|
7
|
+
* cannot share a package, so the wire shape is duplicated here BY NECESSITY and
|
|
8
|
+
* pinned on both sides — the CP keeps `relay-contract.test.ts`, the relay keeps
|
|
9
|
+
* `control-plane.test.ts`, so a breaking change to either is caught before deploy.
|
|
10
|
+
* Vault: typescript-master-vault/concepts/Typesafe_Protocols.md (versioned
|
|
11
|
+
* request-response protocol) + Software_Version.md (semver major as the
|
|
12
|
+
* back-compat boundary).
|
|
13
|
+
*
|
|
14
|
+
* Hash discipline: the CP sends ALREADY-DERIVED hashes — `orgScope` = SHA-256(Org_ID)
|
|
15
|
+
* and `orgIdHash` = bcrypt(Org_ID). The relay stores these verbatim (it never sees
|
|
16
|
+
* the plaintext Org_ID). Only the channel `secret` arrives in plaintext (over the
|
|
17
|
+
* privileged TLS link) and is bcrypt-hashed HERE before storage.
|
|
18
|
+
*/
|
|
19
|
+
/** Major the relay supports. Reject any request whose major differs. */
|
|
20
|
+
export const CONTROL_PLANE_API_MAJOR = 1;
|
|
21
|
+
/** True if a contract version string is compatible with this relay's major. */
|
|
22
|
+
export function isCompatibleVersion(version) {
|
|
23
|
+
const major = Number(version.split(".")[0]);
|
|
24
|
+
return Number.isInteger(major) && major === CONTROL_PLANE_API_MAJOR;
|
|
25
|
+
}
|
|
26
|
+
const createChannelPayload = z.object({
|
|
27
|
+
/** SHA-256(Org_ID) — per-org scope (label-scope + quotas). Opaque; stored as-is. */
|
|
28
|
+
orgScope: z.string().min(1),
|
|
29
|
+
/** bcrypt(Org_ID) — the channel's Org_ID auth hash. Opaque; stored as-is. */
|
|
30
|
+
orgIdHash: z.string().min(1),
|
|
31
|
+
/** Human channel label (per-org scoped at the relay). */
|
|
32
|
+
name: z.string().min(1),
|
|
33
|
+
/** Channel password in plaintext; the relay bcrypts it before storage. */
|
|
34
|
+
secret: z.string().min(1),
|
|
35
|
+
});
|
|
36
|
+
const deleteChannelPayload = z.object({
|
|
37
|
+
/** The relay session id (UUID) returned by channel.create. */
|
|
38
|
+
relaySessionId: z.string().min(1),
|
|
39
|
+
});
|
|
40
|
+
const rotateOrgPayload = z.object({
|
|
41
|
+
oldOrgScope: z.string().min(1),
|
|
42
|
+
newOrgScope: z.string().min(1),
|
|
43
|
+
newOrgIdHash: z.string().min(1),
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Set an org's enforced tier quota (Epic 5, Story 5.1, FR20). The control plane owns
|
|
47
|
+
* the tier→quota mapping; the relay just persists the limits it enforces (the
|
|
48
|
+
* `org_quotas` row, keyed by `orgScope`) + the `tier` label for observability. Upsert
|
|
49
|
+
* semantics — idempotent, and a later tier change re-sends with new limits (5.7).
|
|
50
|
+
*/
|
|
51
|
+
const setOrgQuotaPayload = z.object({
|
|
52
|
+
/** SHA-256(Org_ID) — the per-org key (matches org_quotas.org_scope). */
|
|
53
|
+
orgScope: z.string().min(1),
|
|
54
|
+
/** Tier label (free/small/medium/large/xlarge) — stored for observability. */
|
|
55
|
+
tier: z.string().min(1),
|
|
56
|
+
/** Max channels the org may own (enforced atomically on channel.create). */
|
|
57
|
+
maxChannels: z.number().int().nonnegative(),
|
|
58
|
+
/** Max concurrent collaborating agents (enforced at join time). */
|
|
59
|
+
maxAgents: z.number().int().nonnegative(),
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Wire envelope, discriminated on `op`. `version` is validated separately (by
|
|
63
|
+
* major) so an incompatible-version request yields a clear version error rather
|
|
64
|
+
* than an opaque "op invalid" — see the route.
|
|
65
|
+
*/
|
|
66
|
+
export const RelayRequestSchema = z.discriminatedUnion("op", [
|
|
67
|
+
z.object({ version: z.string().min(1), op: z.literal("channel.create"), payload: createChannelPayload }),
|
|
68
|
+
z.object({ version: z.string().min(1), op: z.literal("channel.delete"), payload: deleteChannelPayload }),
|
|
69
|
+
z.object({ version: z.string().min(1), op: z.literal("org.rotate"), payload: rotateOrgPayload }),
|
|
70
|
+
z.object({ version: z.string().min(1), op: z.literal("org.setQuota"), payload: setOrgQuotaPayload }),
|
|
71
|
+
]);
|
|
72
|
+
//# sourceMappingURL=control-plane-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-plane-contract.js","sourceRoot":"","sources":["../../src/contract/control-plane-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;GAgBG;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,+EAA+E;AAC/E,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,uBAAuB,CAAC;AACtE,CAAC;AAED,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,oFAAoF;IACpF,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,6EAA6E;IAC7E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,yDAAyD;IACzD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,0EAA0E;IAC1E,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC1B,CAAC,CAAC;AAEH,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,8DAA8D;IAC9D,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAChC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,8EAA8E;IAC9E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C,mEAAmE;IACnE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CAC1C,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,kBAAkB,CAAC,IAAI,EAAE;IAC3D,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC;IACxG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC;IACxG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAChG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;CACrG,CAAC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { DbPoolConfig } from "./pool.js";
|
|
2
|
+
export { createPool, query, withClient, withTransaction, closePool } from "./pool.js";
|
|
3
|
+
export type { Migration } from "./migrate.js";
|
|
4
|
+
export { loadMigrations, appliedVersions, migrateUp, migrateDown, isUpToDate, listTables, } from "./migrate.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtF,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,cAAc,EACd,eAAe,EACf,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,cAAc,CAAC"}
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtF,OAAO,EACL,cAAc,EACd,eAAe,EACf,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-cli.d.ts","sourceRoot":"","sources":["../../src/db/migrate-cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { loadServerConfig } from "../config.js";
|
|
2
|
+
import { createPool, closePool } from "./pool.js";
|
|
3
|
+
import { migrateUp, migrateDown, appliedVersions } from "./migrate.js";
|
|
4
|
+
/**
|
|
5
|
+
* CLI: `tsx src/db/migrate-cli.ts up` | `down [n]` | `status`.
|
|
6
|
+
* Reads COGENT_SERVER_DATABASE_URL (+ DB_* tuning) from env / config.json.
|
|
7
|
+
*/
|
|
8
|
+
async function main() {
|
|
9
|
+
const cmd = process.argv[2] ?? "up";
|
|
10
|
+
const cfg = loadServerConfig();
|
|
11
|
+
if (!cfg.DATABASE_URL) {
|
|
12
|
+
console.error("COGENT_SERVER_DATABASE_URL is not set — nothing to migrate.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const pool = createPool({
|
|
16
|
+
connectionString: cfg.DATABASE_URL,
|
|
17
|
+
max: cfg.DB_POOL_MAX,
|
|
18
|
+
connectionTimeoutMs: cfg.DB_CONNECTION_TIMEOUT_MS,
|
|
19
|
+
idleTimeoutMs: cfg.DB_IDLE_TIMEOUT_MS,
|
|
20
|
+
statementTimeoutMs: cfg.DB_STATEMENT_TIMEOUT_MS,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
if (cmd === "up") {
|
|
24
|
+
const ran = await migrateUp(pool);
|
|
25
|
+
console.error(ran.length ? `Applied: ${ran.join(", ")}` : "Already up to date.");
|
|
26
|
+
}
|
|
27
|
+
else if (cmd === "down") {
|
|
28
|
+
// Guard: Number("all"/"-1"/"") -> NaN, and slice(-NaN) rolls back EVERYTHING.
|
|
29
|
+
// A typo must not drop the whole schema — require an explicit positive int.
|
|
30
|
+
const n = Number(process.argv[3] ?? "1");
|
|
31
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
32
|
+
console.error(`down <n>: n must be a positive integer (got "${process.argv[3]}")`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const rolled = await migrateDown(pool, n);
|
|
36
|
+
console.error(rolled.length ? `Rolled back: ${rolled.join(", ")}` : "Nothing to roll back.");
|
|
37
|
+
}
|
|
38
|
+
else if (cmd === "status") {
|
|
39
|
+
console.error(`Applied: ${(await appliedVersions(pool)).join(", ") || "(none)"}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.error(`Unknown command "${cmd}". Use: up | down [n] | status`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await closePool(pool);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
main().catch((err) => {
|
|
51
|
+
console.error("Migration failed:", err);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=migrate-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-cli.js","sourceRoot":"","sources":["../../src/db/migrate-cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEvE;;;GAGG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACpC,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAC/B,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QACtB,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QAC7E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC;QACtB,gBAAgB,EAAE,GAAG,CAAC,YAAY;QAClC,GAAG,EAAE,GAAG,CAAC,WAAW;QACpB,mBAAmB,EAAE,GAAG,CAAC,wBAAwB;QACjD,aAAa,EAAE,GAAG,CAAC,kBAAkB;QACrC,kBAAkB,EAAE,GAAG,CAAC,uBAAuB;KAChD,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;YAClC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;QACnF,CAAC;aAAM,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YAC1B,8EAA8E;YAC9E,4EAA4E;YAC5E,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClC,OAAO,CAAC,KAAK,CAAC,gDAAgD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBACnF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;YAC1C,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC;QAC/F,CAAC;aAAM,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC;QACpF,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,oBAAoB,GAAG,gCAAgC,CAAC,CAAC;YACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;IACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
/** One migration = a numeric-prefixed pair of up/down SQL files. */
|
|
3
|
+
export interface Migration {
|
|
4
|
+
version: string;
|
|
5
|
+
up: string;
|
|
6
|
+
down: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load all migrations from the migrations dir, ordered by their numeric prefix.
|
|
10
|
+
* Expects `<version>.up.sql` and `<version>.down.sql` pairs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadMigrations(dir?: string): Promise<Migration[]>;
|
|
13
|
+
/** Versions already applied, in apply order. */
|
|
14
|
+
export declare function appliedVersions(pool: Pool): Promise<string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Apply all unapplied migrations in order. Each migration's SQL AND its ledger
|
|
17
|
+
* insert run in a SINGLE transaction, so a failed migration leaves no partial
|
|
18
|
+
* schema and no ledger row (atomic + reversible). Idempotent: already-applied
|
|
19
|
+
* versions are skipped, so re-running is a no-op. Returns the versions applied.
|
|
20
|
+
*/
|
|
21
|
+
export declare function migrateUp(pool: Pool, dir?: string): Promise<string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Roll back the last `n` applied migrations (most recent first). Each down.sql
|
|
24
|
+
* + its ledger delete run in one transaction. Returns the versions rolled back.
|
|
25
|
+
*/
|
|
26
|
+
export declare function migrateDown(pool: Pool, n?: number, dir?: string): Promise<string[]>;
|
|
27
|
+
/** True if every on-disk migration has been applied. */
|
|
28
|
+
export declare function isUpToDate(pool: Pool, dir?: string): Promise<boolean>;
|
|
29
|
+
/** Convenience for tests/health: list table names in the public schema. */
|
|
30
|
+
export declare function listTables(pool: Pool): Promise<string[]>;
|
|
31
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAK/B,oEAAoE;AACpE,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,GAAG,GAAE,MAAuB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAiBvF;AAYD,gDAAgD;AAChD,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAMnE;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAe3E;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,SAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAuBpF;AAED,wDAAwD;AACxD,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI3E;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO9D"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { withClient, withTransaction } from "./pool.js";
|
|
5
|
+
const MIGRATIONS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "migrations");
|
|
6
|
+
/**
|
|
7
|
+
* Load all migrations from the migrations dir, ordered by their numeric prefix.
|
|
8
|
+
* Expects `<version>.up.sql` and `<version>.down.sql` pairs.
|
|
9
|
+
*/
|
|
10
|
+
export async function loadMigrations(dir = MIGRATIONS_DIR) {
|
|
11
|
+
const files = await fs.readdir(dir);
|
|
12
|
+
const versions = [
|
|
13
|
+
...new Set(files
|
|
14
|
+
.filter((f) => f.endsWith(".up.sql"))
|
|
15
|
+
.map((f) => f.replace(/\.up\.sql$/, ""))),
|
|
16
|
+
].sort(); // numeric prefix => lexical sort is correct for zero-padded versions
|
|
17
|
+
const migrations = [];
|
|
18
|
+
for (const version of versions) {
|
|
19
|
+
const up = await fs.readFile(path.join(dir, `${version}.up.sql`), "utf-8");
|
|
20
|
+
const down = await fs.readFile(path.join(dir, `${version}.down.sql`), "utf-8");
|
|
21
|
+
migrations.push({ version, up, down });
|
|
22
|
+
}
|
|
23
|
+
return migrations;
|
|
24
|
+
}
|
|
25
|
+
/** Ensure the ledger table exists. */
|
|
26
|
+
async function ensureLedger(pool) {
|
|
27
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
28
|
+
version text PRIMARY KEY,
|
|
29
|
+
applied_at timestamptz NOT NULL DEFAULT now()
|
|
30
|
+
)`);
|
|
31
|
+
}
|
|
32
|
+
/** Versions already applied, in apply order. */
|
|
33
|
+
export async function appliedVersions(pool) {
|
|
34
|
+
await ensureLedger(pool);
|
|
35
|
+
const res = await pool.query("SELECT version FROM schema_migrations ORDER BY version ASC");
|
|
36
|
+
return res.rows.map((r) => r.version);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Apply all unapplied migrations in order. Each migration's SQL AND its ledger
|
|
40
|
+
* insert run in a SINGLE transaction, so a failed migration leaves no partial
|
|
41
|
+
* schema and no ledger row (atomic + reversible). Idempotent: already-applied
|
|
42
|
+
* versions are skipped, so re-running is a no-op. Returns the versions applied.
|
|
43
|
+
*/
|
|
44
|
+
export async function migrateUp(pool, dir) {
|
|
45
|
+
await ensureLedger(pool);
|
|
46
|
+
const applied = new Set(await appliedVersions(pool));
|
|
47
|
+
const migrations = await loadMigrations(dir);
|
|
48
|
+
const ran = [];
|
|
49
|
+
for (const m of migrations) {
|
|
50
|
+
if (applied.has(m.version))
|
|
51
|
+
continue;
|
|
52
|
+
await withTransaction(pool, async (client) => {
|
|
53
|
+
await client.query(m.up);
|
|
54
|
+
await client.query("INSERT INTO schema_migrations (version) VALUES ($1)", [m.version]);
|
|
55
|
+
});
|
|
56
|
+
ran.push(m.version);
|
|
57
|
+
}
|
|
58
|
+
return ran;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Roll back the last `n` applied migrations (most recent first). Each down.sql
|
|
62
|
+
* + its ledger delete run in one transaction. Returns the versions rolled back.
|
|
63
|
+
*/
|
|
64
|
+
export async function migrateDown(pool, n = 1, dir) {
|
|
65
|
+
// Guard: a non-finite/NaN n would make slice(-n) return the WHOLE list and
|
|
66
|
+
// roll back everything. Require a positive integer (use a large n to mean "all").
|
|
67
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
68
|
+
throw new Error(`migrateDown: n must be a positive integer (got ${n})`);
|
|
69
|
+
}
|
|
70
|
+
const applied = await appliedVersions(pool);
|
|
71
|
+
const byVersion = new Map((await loadMigrations(dir)).map((m) => [m.version, m]));
|
|
72
|
+
const toRollback = applied.slice(-n).reverse();
|
|
73
|
+
const rolled = [];
|
|
74
|
+
for (const version of toRollback) {
|
|
75
|
+
const m = byVersion.get(version);
|
|
76
|
+
if (!m) {
|
|
77
|
+
throw new Error(`Cannot roll back ${version}: no down migration found on disk`);
|
|
78
|
+
}
|
|
79
|
+
await withTransaction(pool, async (client) => {
|
|
80
|
+
await client.query(m.down);
|
|
81
|
+
await client.query("DELETE FROM schema_migrations WHERE version = $1", [version]);
|
|
82
|
+
});
|
|
83
|
+
rolled.push(version);
|
|
84
|
+
}
|
|
85
|
+
return rolled;
|
|
86
|
+
}
|
|
87
|
+
/** True if every on-disk migration has been applied. */
|
|
88
|
+
export async function isUpToDate(pool, dir) {
|
|
89
|
+
const applied = new Set(await appliedVersions(pool));
|
|
90
|
+
const all = await loadMigrations(dir);
|
|
91
|
+
return all.every((m) => applied.has(m.version));
|
|
92
|
+
}
|
|
93
|
+
/** Convenience for tests/health: list table names in the public schema. */
|
|
94
|
+
export async function listTables(pool) {
|
|
95
|
+
const res = await withClient(pool, (c) => c.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"));
|
|
96
|
+
return res.rows.map((r) => r.tablename);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 0001_init_sessions (up): durable session storage for the business relay
|
|
2
|
+
-- instance (FR35), JSONB-hybrid model.
|
|
3
|
+
--
|
|
4
|
+
-- Only the queryable / unique / auth-critical fields are normalized into columns;
|
|
5
|
+
-- the rest of the session document (peers, messages, peerEvents, offline queues,
|
|
6
|
+
-- channel mappings) lives in `state jsonb` — a 1:1 mirror of the file store's
|
|
7
|
+
-- per-session JSON document, so updateSession is a whole-document UPDATE.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE sessions (
|
|
10
|
+
session_id uuid PRIMARY KEY,
|
|
11
|
+
label text,
|
|
12
|
+
secret_hash text NOT NULL,
|
|
13
|
+
-- Business Edition: bcrypt hash of the Org_ID (auth) + stable SHA-256 scope
|
|
14
|
+
-- (label namespacing). Both NULL on free/public channels.
|
|
15
|
+
org_id_hash text,
|
|
16
|
+
org_scope text,
|
|
17
|
+
creator_ip text,
|
|
18
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
19
|
+
-- Full mutable session document (everything except tokens + the columns above).
|
|
20
|
+
state jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- Per-org label scope (Story 2.3 analogue): a label is unique WITHIN its scope;
|
|
24
|
+
-- free/public channels collapse to the empty scope (global uniqueness). Two orgs
|
|
25
|
+
-- can both hold 'sprint-42' because their scopes differ.
|
|
26
|
+
CREATE UNIQUE INDEX sessions_scope_label_uniq
|
|
27
|
+
ON sessions (COALESCE(org_scope, ''), label)
|
|
28
|
+
WHERE label IS NOT NULL;
|
|
29
|
+
|
|
30
|
+
-- Tokens stay normalized + indexed: bearer-token auth looks a session up by its
|
|
31
|
+
-- SHA-256 token hash on every request (getSessionByTokenHash).
|
|
32
|
+
CREATE TABLE tokens (
|
|
33
|
+
token_hash text PRIMARY KEY,
|
|
34
|
+
session_id uuid NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
35
|
+
peer_id text,
|
|
36
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX tokens_session_idx ON tokens (session_id);
|
|
39
|
+
|
|
40
|
+
-- Single-row monotonic global message counter (mirrors global-stats.json).
|
|
41
|
+
CREATE TABLE global_stats (
|
|
42
|
+
id boolean PRIMARY KEY DEFAULT true,
|
|
43
|
+
total_messages bigint NOT NULL DEFAULT 0,
|
|
44
|
+
CONSTRAINT global_stats_singleton CHECK (id)
|
|
45
|
+
);
|
|
46
|
+
INSERT INTO global_stats (id, total_messages) VALUES (true, 0);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 0002_org_quotas (up): per-org tier quotas, enforced atomically by the relay
|
|
2
|
+
-- (FR7/FR8). The control plane (E3) provisions/updates a row per org (keyed by
|
|
3
|
+
-- the org's stable SHA-256 scope); the relay reads + row-locks it to enforce
|
|
4
|
+
-- limits under concurrency. A missing row means "not yet provisioned" -> the
|
|
5
|
+
-- relay does not cap (fail-open pre-E3; tests insert rows explicitly).
|
|
6
|
+
|
|
7
|
+
CREATE TABLE org_quotas (
|
|
8
|
+
org_scope text PRIMARY KEY,
|
|
9
|
+
tier text NOT NULL DEFAULT 'free',
|
|
10
|
+
max_channels integer NOT NULL,
|
|
11
|
+
max_agents integer NOT NULL,
|
|
12
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
13
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Pool, PoolClient, QueryResult, QueryResultRow } from "pg";
|
|
2
|
+
/** Pool tuning, sourced from server config (COGENT_SERVER_DB_*). */
|
|
3
|
+
export interface DbPoolConfig {
|
|
4
|
+
connectionString: string;
|
|
5
|
+
/** Max concurrent connections — the bounded ceiling (back-pressure). */
|
|
6
|
+
max: number;
|
|
7
|
+
/** Acquire timeout (ms): a connect that can't be served in this window
|
|
8
|
+
* rejects rather than hanging — the graceful-degradation lever. */
|
|
9
|
+
connectionTimeoutMs: number;
|
|
10
|
+
/** Idle client reaped after this long (ms). */
|
|
11
|
+
idleTimeoutMs: number;
|
|
12
|
+
/** Per-statement server-side timeout (ms); 0 disables. */
|
|
13
|
+
statementTimeoutMs: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a bounded Postgres connection pool.
|
|
17
|
+
*
|
|
18
|
+
* The pool size is capped at `max`; when all connections are in use, further
|
|
19
|
+
* acquisitions WAIT, and if none frees within `connectionTimeoutMs` the acquire
|
|
20
|
+
* REJECTS with an error instead of hanging or growing unbounded (the DB-pool
|
|
21
|
+
* analogue of stream back-pressure — bounded buffer + timeout, never a crash).
|
|
22
|
+
*
|
|
23
|
+
* An `error` listener is attached because `pg` emits errors from IDLE clients
|
|
24
|
+
* on the pool object (not via any await); without a listener those would be
|
|
25
|
+
* unhandled and could crash the process.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createPool(cfg: DbPoolConfig): Pool;
|
|
28
|
+
/** Run a single query on a pooled connection (auto-acquired + released). */
|
|
29
|
+
export declare function query<R extends QueryResultRow = QueryResultRow>(pool: Pool, text: string, params?: unknown[]): Promise<QueryResult<R>>;
|
|
30
|
+
/** Acquire a client, run `fn`, and ALWAYS release it (even on throw). */
|
|
31
|
+
export declare function withClient<T>(pool: Pool, fn: (client: PoolClient) => Promise<T>): Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Run `fn` inside a transaction: BEGIN → fn → COMMIT, or ROLLBACK on any throw.
|
|
34
|
+
* The client is always released.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withTransaction<T>(pool: Pool, fn: (client: PoolClient) => Promise<T>): Promise<T>;
|
|
37
|
+
/** Gracefully drain and close the pool (call on shutdown). */
|
|
38
|
+
export declare function closePool(pool: Pool): Promise<void>;
|
|
39
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/db/pool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,IAAI,CAAC;AAIxE,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ;wEACoE;IACpE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAgBlD;AAED,4EAA4E;AAC5E,wBAAsB,KAAK,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACnE,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAEzB;AAED,yEAAyE;AACzE,wBAAsB,UAAU,CAAC,CAAC,EAChC,IAAI,EAAE,IAAI,EACV,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,IAAI,EACV,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED,8DAA8D;AAC9D,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzD"}
|
package/dist/db/pool.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
const { Pool: PgPool } = pg;
|
|
3
|
+
/**
|
|
4
|
+
* Create a bounded Postgres connection pool.
|
|
5
|
+
*
|
|
6
|
+
* The pool size is capped at `max`; when all connections are in use, further
|
|
7
|
+
* acquisitions WAIT, and if none frees within `connectionTimeoutMs` the acquire
|
|
8
|
+
* REJECTS with an error instead of hanging or growing unbounded (the DB-pool
|
|
9
|
+
* analogue of stream back-pressure — bounded buffer + timeout, never a crash).
|
|
10
|
+
*
|
|
11
|
+
* An `error` listener is attached because `pg` emits errors from IDLE clients
|
|
12
|
+
* on the pool object (not via any await); without a listener those would be
|
|
13
|
+
* unhandled and could crash the process.
|
|
14
|
+
*/
|
|
15
|
+
export function createPool(cfg) {
|
|
16
|
+
const pool = new PgPool({
|
|
17
|
+
connectionString: cfg.connectionString,
|
|
18
|
+
max: cfg.max,
|
|
19
|
+
connectionTimeoutMillis: cfg.connectionTimeoutMs,
|
|
20
|
+
idleTimeoutMillis: cfg.idleTimeoutMs,
|
|
21
|
+
// Server-side per-statement timeout bounds runaway queries.
|
|
22
|
+
statement_timeout: cfg.statementTimeoutMs > 0 ? cfg.statementTimeoutMs : undefined,
|
|
23
|
+
});
|
|
24
|
+
// An idle client error (e.g. the DB dropped the connection) must not crash us.
|
|
25
|
+
pool.on("error", (err) => {
|
|
26
|
+
console.error("[db] idle client error:", err.message);
|
|
27
|
+
});
|
|
28
|
+
return pool;
|
|
29
|
+
}
|
|
30
|
+
/** Run a single query on a pooled connection (auto-acquired + released). */
|
|
31
|
+
export async function query(pool, text, params) {
|
|
32
|
+
return pool.query(text, params);
|
|
33
|
+
}
|
|
34
|
+
/** Acquire a client, run `fn`, and ALWAYS release it (even on throw). */
|
|
35
|
+
export async function withClient(pool, fn) {
|
|
36
|
+
const client = await pool.connect();
|
|
37
|
+
try {
|
|
38
|
+
return await fn(client);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
client.release();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run `fn` inside a transaction: BEGIN → fn → COMMIT, or ROLLBACK on any throw.
|
|
46
|
+
* The client is always released.
|
|
47
|
+
*/
|
|
48
|
+
export async function withTransaction(pool, fn) {
|
|
49
|
+
return withClient(pool, async (client) => {
|
|
50
|
+
try {
|
|
51
|
+
await client.query("BEGIN");
|
|
52
|
+
const result = await fn(client);
|
|
53
|
+
await client.query("COMMIT");
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
await client.query("ROLLBACK");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// A rollback failure (e.g. the connection died) is secondary to the
|
|
62
|
+
// original error — swallow it so the real cause propagates.
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Gracefully drain and close the pool (call on shutdown). */
|
|
69
|
+
export async function closePool(pool) {
|
|
70
|
+
await pool.end();
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/db/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAgB5B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,GAAiB;IAC1C,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC;QACtB,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;QACtC,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,uBAAuB,EAAE,GAAG,CAAC,mBAAmB;QAChD,iBAAiB,EAAE,GAAG,CAAC,aAAa;QACpC,4DAA4D;QAC5D,iBAAiB,EAAE,GAAG,CAAC,kBAAkB,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS;KACnF,CAAC,CAAC;IAEH,+EAA+E;IAC/E,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACvB,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,IAAU,EACV,IAAY,EACZ,MAAkB;IAElB,OAAO,IAAI,CAAC,KAAK,CAAI,IAAI,EAAE,MAA6B,CAAC,CAAC;AAC5D,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAU,EACV,EAAsC;IAEtC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAU,EACV,EAAsC;IAEtC,OAAO,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;YAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC7B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,oEAAoE;gBACpE,4DAA4D;YAC9D,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAU;IACxC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;AACnB,CAAC"}
|