@axiom-lattice/pg-stores 3.1.6 → 3.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +29 -0
  3. package/dist/index.d.mts +54 -2
  4. package/dist/index.d.ts +54 -2
  5. package/dist/index.js +424 -47
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +421 -47
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/PostgreSQLA2AApiKeyStore.migrations.test.ts +2 -1
  11. package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +45 -0
  12. package/src/__tests__/PostgreSQLMcpServerConfigStore.test.ts +79 -0
  13. package/src/__tests__/PostgreSQLOpenAuditStore.test.ts +100 -0
  14. package/src/__tests__/PostgreSQLProjectRoomMessageStore.test.ts +14 -0
  15. package/src/__tests__/PostgreSQLProjectRoomReadStateStore.test.ts +136 -0
  16. package/src/__tests__/PostgresSharedResourceStore.openGrants.test.ts +62 -0
  17. package/src/__tests__/create_open_audit_table.test.ts +40 -0
  18. package/src/createPgStoreConfig.ts +12 -1
  19. package/src/index.ts +2 -0
  20. package/src/migrations/add_api_key_grants.ts +25 -0
  21. package/src/migrations/add_share_open_grants.ts +23 -0
  22. package/src/migrations/create_open_audit_table.ts +36 -0
  23. package/src/migrations/project_room_migration.ts +26 -0
  24. package/src/stores/PostgreSQLA2AApiKeyStore.ts +90 -14
  25. package/src/stores/PostgreSQLMcpServerConfigStore.ts +23 -0
  26. package/src/stores/PostgreSQLOpenAuditStore.ts +143 -0
  27. package/src/stores/PostgreSQLProjectRoomMessageStore.ts +36 -1
  28. package/src/stores/PostgreSQLProjectRoomReadStateStore.ts +47 -0
  29. package/src/stores/PostgresSharedResourceStore.ts +5 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "3.1.6",
3
+ "version": "3.1.9",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -25,8 +25,8 @@
25
25
  "@langchain/core": "1.1.30",
26
26
  "pg": "^8.16.3",
27
27
  "uuid": "^9.0.1",
28
- "@axiom-lattice/core": "4.2.6",
29
- "@axiom-lattice/protocols": "4.1.4"
28
+ "@axiom-lattice/core": "4.2.9",
29
+ "@axiom-lattice/protocols": "4.2.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/jest": "^29.5.14",
@@ -55,7 +55,7 @@ describe("PostgreSQLA2AApiKeyStore standalone migrations", () => {
55
55
  mockClientSql.length = 0;
56
56
  });
57
57
 
58
- it("registers and applies both the table and assistant_ids migrations", async () => {
58
+ it("registers and applies the table, assistant_ids, and grants migrations", async () => {
59
59
  const store = new PostgreSQLA2AApiKeyStore({
60
60
  poolConfig: "postgres://unused",
61
61
  autoMigrate: false,
@@ -66,6 +66,7 @@ describe("PostgreSQLA2AApiKeyStore standalone migrations", () => {
66
66
  expect(mockRecordedMigrationNames).toEqual([
67
67
  "create_a2a_api_keys_table",
68
68
  "add_a2a_key_assistant_ids",
69
+ "add_api_key_grants",
69
70
  ]);
70
71
  expect(
71
72
  mockClientSql.some((sql) => sql.includes("ADD COLUMN IF NOT EXISTS assistant_ids")),
@@ -131,3 +131,48 @@ describe("PostgreSQLA2AApiKeyStore", () => {
131
131
  expect("workspaceId" in (entry ?? {})).toBe(false);
132
132
  });
133
133
  });
134
+
135
+ describe("PostgreSQLA2AApiKeyStore grants column", () => {
136
+ const pool2 = { query: mockQuery } as unknown as Pool;
137
+
138
+ beforeEach(() => {
139
+ jest.clearAllMocks();
140
+ });
141
+
142
+ it("create persists grants as JSON", async () => {
143
+ const store = new PostgreSQLA2AApiKeyStore({ pool: pool2 });
144
+ const grants = [{ domain: "kb", items: ["search"] }];
145
+ mockQuery.mockResolvedValueOnce({
146
+ rows: [{
147
+ id: "k1", key_value: "enc:a2a_x", tenant_id: "t1", project_id: "p1",
148
+ assistant_ids: null, grants, label: null, enabled: true,
149
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
150
+ updated_at: new Date("2026-01-01T00:00:00.000Z"),
151
+ }],
152
+ });
153
+
154
+ const record = await store.create({
155
+ tenantId: "t1", projectId: "p1", grants,
156
+ });
157
+
158
+ const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]];
159
+ expect(sql).toContain("grants");
160
+ expect(params[4]).toBe(JSON.stringify(grants));
161
+ expect(record.grants).toEqual(grants);
162
+ });
163
+
164
+ it("mapRowToRecord projects grants back", async () => {
165
+ const store = new PostgreSQLA2AApiKeyStore({ pool: pool2 });
166
+ const grants = [{ domain: "kb" }];
167
+ mockQuery.mockResolvedValueOnce({
168
+ rows: [{
169
+ id: "k1", key_value: "enc:a2a_x", tenant_id: "t1", project_id: "p1",
170
+ assistant_ids: null, grants, label: null, enabled: true,
171
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
172
+ updated_at: new Date("2026-01-01T00:00:00.000Z"),
173
+ }],
174
+ });
175
+ const record = await store.findById("k1");
176
+ expect(record?.grants).toEqual(grants);
177
+ });
178
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import type { Pool } from "pg";
3
+
4
+ // The @axiom-lattice/core barrel cannot load under jest in this package (ESM-only
5
+ // transitive deps), so mock it with the real encryption module — node:crypto only.
6
+ jest.mock("@axiom-lattice/core", () => ({
7
+ ...jest.requireActual("../../../core/src/util/encryption"),
8
+ }));
9
+
10
+ import { PostgreSQLMcpServerConfigStore } from "../stores/PostgreSQLMcpServerConfigStore";
11
+
12
+ const makeRow = (configJson: string, isEnvEncrypted: boolean) => ({
13
+ id: "cfg-1",
14
+ tenant_id: "tenant-1",
15
+ key: "remote",
16
+ name: null,
17
+ description: null,
18
+ config: configJson,
19
+ selected_tools: [],
20
+ is_env_encrypted: isEnvEncrypted,
21
+ status: "disconnected",
22
+ created_at: new Date(),
23
+ updated_at: new Date(),
24
+ });
25
+
26
+ describe("PostgreSQLMcpServerConfigStore header encryption", () => {
27
+ it("encrypts header values at rest and decrypts on read", async () => {
28
+ let storedConfig = "";
29
+ let storedFlag = false;
30
+ const query = jest.fn(async (sql: string, values: unknown[]) => {
31
+ if (sql.includes("INSERT INTO lattice_mcp_server_configs")) {
32
+ storedConfig = values[5] as string;
33
+ storedFlag = values[7] as boolean;
34
+ return { rowCount: 1 };
35
+ }
36
+ if (sql.includes("SELECT id, tenant_id, key")) {
37
+ return { rows: [makeRow(storedConfig, storedFlag)] };
38
+ }
39
+ return { rows: [] };
40
+ });
41
+ const store = new PostgreSQLMcpServerConfigStore({ pool: { query } as unknown as Pool });
42
+
43
+ await store.createConfig("tenant-1", "cfg-1", {
44
+ key: "remote",
45
+ config: {
46
+ name: "remote",
47
+ version: "1.0.0",
48
+ transport: "streamable_http",
49
+ url: "https://mcp.example.com/mcp",
50
+ headers: { Authorization: "Bearer sk-secret-token" },
51
+ },
52
+ });
53
+
54
+ expect(storedFlag).toBe(true);
55
+ const atRest = JSON.parse(storedConfig);
56
+ expect(atRest.headers.Authorization).not.toBe("Bearer sk-secret-token");
57
+
58
+ const read = await store.getConfigByKey("tenant-1", "remote");
59
+ expect(read?.config.headers?.Authorization).toBe("Bearer sk-secret-token");
60
+ expect(read?.config.headers?.Authorization).not.toBe(atRest.headers.Authorization);
61
+ });
62
+
63
+ it("leaves legacy rows without headers untouched", async () => {
64
+ const legacyConfig = JSON.stringify({
65
+ name: "legacy", version: "1.0.0", transport: "stdio", command: "npx",
66
+ });
67
+ const query = jest.fn(async (sql: string) => {
68
+ if (sql.includes("SELECT id, tenant_id, key")) {
69
+ return { rows: [makeRow(legacyConfig, false)] };
70
+ }
71
+ return { rows: [] };
72
+ });
73
+ const store = new PostgreSQLMcpServerConfigStore({ pool: { query } as unknown as Pool });
74
+
75
+ const read = await store.getConfigByKey("tenant-1", "remote");
76
+ expect(read?.config.headers).toBeUndefined();
77
+ expect(read?.config.command).toBe("npx");
78
+ });
79
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
2
+ import type { Pool } from "pg";
3
+
4
+ const mockQuery = jest.fn();
5
+ import { PostgreSQLOpenAuditStore } from "../stores/PostgreSQLOpenAuditStore";
6
+
7
+ const pool = { query: mockQuery } as unknown as Pool;
8
+
9
+ describe("PostgreSQLOpenAuditStore", () => {
10
+ beforeEach(() => jest.clearAllMocks());
11
+
12
+ it("append inserts a row with JSON args", async () => {
13
+ const store = new PostgreSQLOpenAuditStore({ pool });
14
+ mockQuery.mockResolvedValueOnce({ rows: [] });
15
+ await store.append({
16
+ tenantId: "t1",
17
+ credentialId: "k1",
18
+ credentialKind: "api_key",
19
+ projectId: "p1",
20
+ domain: "kb",
21
+ action: "search",
22
+ args: { query: "x" },
23
+ status: "ok",
24
+ durationMs: 12.7,
25
+ });
26
+ const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]];
27
+ expect(sql).toContain("INSERT INTO lattice_open_audit");
28
+ expect(params[6]).toBe(JSON.stringify({ query: "x" }));
29
+ expect(params[7]).toBe("ok");
30
+ expect(params[9]).toBe(13);
31
+ });
32
+
33
+ it("append persists null args safely", async () => {
34
+ const store = new PostgreSQLOpenAuditStore({ pool });
35
+ mockQuery.mockResolvedValueOnce({ rows: [] });
36
+ await store.append({
37
+ tenantId: "t1",
38
+ credentialId: "k1",
39
+ credentialKind: "browser_token",
40
+ projectId: "p1",
41
+ domain: "kb",
42
+ action: "search",
43
+ args: undefined,
44
+ status: "error",
45
+ errorCode: "GRANT_DENIED",
46
+ durationMs: 1,
47
+ });
48
+ const params = mockQuery.mock.calls[0][1] as unknown[];
49
+ expect(params[6]).toBeNull();
50
+ expect(params[8]).toBe("GRANT_DENIED");
51
+ });
52
+ });
53
+
54
+ describe("PostgreSQLOpenAuditStore.query", () => {
55
+ beforeEach(() => jest.clearAllMocks());
56
+
57
+ it("builds a tenant-scoped filtered query", async () => {
58
+ const store = new PostgreSQLOpenAuditStore({ pool });
59
+ mockQuery.mockResolvedValueOnce({ rows: [] });
60
+ await store.query({
61
+ tenantId: "t1",
62
+ domain: "kb",
63
+ status: "error",
64
+ from: "2026-01-01T00:00:00Z",
65
+ limit: 10,
66
+ offset: 5,
67
+ });
68
+ const [sql, values] = mockQuery.mock.calls[0] as [string, unknown[]];
69
+ expect(sql).toContain("tenant_id = $1");
70
+ expect(sql).toContain("domain = $2");
71
+ expect(sql).toContain("status = $3");
72
+ expect(sql).toContain("created_at >= $4");
73
+ expect(sql).toContain("ORDER BY created_at DESC");
74
+ expect(values).toEqual(["t1", "kb", "error", "2026-01-01T00:00:00Z", 10, 5]);
75
+ });
76
+
77
+ it("maps rows back to records", async () => {
78
+ const store = new PostgreSQLOpenAuditStore({ pool });
79
+ mockQuery.mockResolvedValueOnce({
80
+ rows: [
81
+ {
82
+ id: "a1",
83
+ tenant_id: "t1",
84
+ credential_id: "k1",
85
+ credential_kind: "api_key",
86
+ project_id: "p1",
87
+ domain: "kb",
88
+ action: "search",
89
+ args: { q: "x" },
90
+ status: "ok",
91
+ error_code: null,
92
+ duration_ms: 7,
93
+ created_at: new Date("2026-01-01T00:00:00Z"),
94
+ },
95
+ ],
96
+ });
97
+ const rows = await store.query({ tenantId: "t1" });
98
+ expect(rows[0]).toMatchObject({ id: "a1", status: "ok", errorCode: undefined });
99
+ });
100
+ });
@@ -100,4 +100,18 @@ describe("PostgreSQLProjectRoomMessageStore", () => {
100
100
  await expect(new PostgreSQLProjectRoomMessageStore({ pool }).findById("tenant-1", "message-1"))
101
101
  .rejects.toThrow("Invalid project room message row");
102
102
  });
103
+
104
+ it("skips individually invalid rows when listing while preserving valid rows", async () => {
105
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
106
+ const malformed = { ...row, id: "message-bad", content: { text: "missing type" } };
107
+ query.mockResolvedValueOnce({ rows: [row, malformed] });
108
+ const store = new PostgreSQLProjectRoomMessageStore({ pool });
109
+ await expect(store.list({ tenantId: "tenant-1", roomId: "room-1", limit: 10 }))
110
+ .resolves.toEqual([expect.objectContaining({ id: "message-1" })]);
111
+ expect(warn).toHaveBeenCalledWith(
112
+ "[ProjectRoomMessageStore] Skipping invalid message row",
113
+ expect.objectContaining({ tenantId: "tenant-1", roomId: "room-1", rowId: "message-bad" }),
114
+ );
115
+ warn.mockRestore();
116
+ });
103
117
  });
@@ -0,0 +1,136 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
3
+ import { Pool } from "pg";
4
+ import type { ProjectRoomMessage } from "@axiom-lattice/protocols";
5
+
6
+ import { MigrationManager } from "../migrations/migration";
7
+ import { createProjectRoomReadStates, createProjectRoomTables } from "../migrations/project_room_migration";
8
+ import { PostgreSQLProjectRoomMessageStore } from "../stores/PostgreSQLProjectRoomMessageStore";
9
+ import { PostgreSQLProjectRoomReadStateStore } from "../stores/PostgreSQLProjectRoomReadStateStore";
10
+
11
+ const query = jest.fn();
12
+ const mockPool = { query } as unknown as Pool;
13
+ const readStateRow = {
14
+ tenant_id: "tenant-1", room_id: "room-1", user_id: "user-1",
15
+ last_read_at: new Date("2026-01-02T00:00:00.000Z"), updated_at: new Date("2026-01-02T00:00:01.000Z"),
16
+ };
17
+
18
+ describe("PostgreSQLProjectRoomReadStateStore SQL contract", () => {
19
+ beforeEach(() => query.mockReset());
20
+
21
+ it("returns null when no marker exists and selects by the full primary key", async () => {
22
+ query.mockResolvedValueOnce({ rows: [] });
23
+ const store = new PostgreSQLProjectRoomReadStateStore({ pool: mockPool });
24
+ await expect(store.get("tenant-1", "room-1", "user-1")).resolves.toBeNull();
25
+ expect(query.mock.calls[0][0]).toContain("FROM lattice_project_room_read_states");
26
+ expect(query.mock.calls[0][0]).toContain("WHERE tenant_id = $1 AND room_id = $2 AND user_id = $3");
27
+ expect(query.mock.calls[0][1]).toEqual(["tenant-1", "room-1", "user-1"]);
28
+ });
29
+
30
+ it("upserts monotonically with GREATEST and returns the stored row", async () => {
31
+ query.mockResolvedValueOnce({ rows: [readStateRow] });
32
+ const store = new PostgreSQLProjectRoomReadStateStore({ pool: mockPool });
33
+ const lastReadAt = new Date("2026-01-02T00:00:00.000Z");
34
+ await expect(store.markRead({ tenantId: "tenant-1", roomId: "room-1", userId: "user-1", lastReadAt }))
35
+ .resolves.toEqual({
36
+ tenantId: "tenant-1", roomId: "room-1", userId: "user-1",
37
+ lastReadAt, updatedAt: new Date("2026-01-02T00:00:01.000Z"),
38
+ });
39
+ const [sql, params] = query.mock.calls[0] as [string, unknown[]];
40
+ expect(sql).toContain("ON CONFLICT (tenant_id, room_id, user_id) DO UPDATE");
41
+ expect(sql).toContain("GREATEST(lattice_project_room_read_states.last_read_at, EXCLUDED.last_read_at)");
42
+ expect(params).toEqual(["tenant-1", "room-1", "user-1", lastReadAt]);
43
+ });
44
+
45
+ it("rejects an invalid markRead horizon before querying", async () => {
46
+ const store = new PostgreSQLProjectRoomReadStateStore({ pool: mockPool });
47
+ await expect(store.markRead({ tenantId: "tenant-1", roomId: "room-1", userId: "user-1", lastReadAt: new Date("bad") }))
48
+ .rejects.toThrow(RangeError);
49
+ expect(query).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it("rejects malformed read state rows", async () => {
53
+ query.mockResolvedValueOnce({ rows: [{ ...readStateRow, last_read_at: "2026-01-02" }] });
54
+ const store = new PostgreSQLProjectRoomReadStateStore({ pool: mockPool });
55
+ await expect(store.get("tenant-1", "room-1", "user-1")).rejects.toThrow("Invalid project room read state row");
56
+ });
57
+ });
58
+
59
+ describe("PostgreSQLProjectRoomMessageStore countAfter SQL contract", () => {
60
+ beforeEach(() => query.mockReset());
61
+
62
+ it("counts strictly after the horizon and excludes one human author", async () => {
63
+ query.mockResolvedValueOnce({ rows: [{ count: 2 }] });
64
+ const store = new PostgreSQLProjectRoomMessageStore({ pool: mockPool });
65
+ const after = new Date("2026-01-01T00:00:00.000Z");
66
+ await expect(store.countAfter({ tenantId: "tenant-1", roomId: "room-1", after, excludeAuthorUserId: "user-1" }))
67
+ .resolves.toBe(2);
68
+ const [sql, params] = query.mock.calls[0] as [string, unknown[]];
69
+ expect(sql).toContain("created_at > $3");
70
+ expect(sql).toContain("($4::text IS NULL OR author->>'type' <> 'human' OR author->>'userId' <> $4)");
71
+ expect(params).toEqual(["tenant-1", "room-1", after, "user-1"]);
72
+ });
73
+
74
+ it("passes null when no author is excluded and rejects invalid horizons", async () => {
75
+ query.mockResolvedValueOnce({ rows: [{ count: 0 }] });
76
+ const store = new PostgreSQLProjectRoomMessageStore({ pool: mockPool });
77
+ const after = new Date("2026-01-01T00:00:00.000Z");
78
+ await expect(store.countAfter({ tenantId: "tenant-1", roomId: "room-1", after })).resolves.toBe(0);
79
+ expect(query.mock.calls[0][1]).toEqual(["tenant-1", "room-1", after, null]);
80
+ await expect(store.countAfter({ tenantId: "tenant-1", roomId: "room-1", after: new Date(Number.NaN) }))
81
+ .rejects.toThrow(RangeError);
82
+ expect(query).toHaveBeenCalledTimes(1);
83
+ });
84
+ });
85
+
86
+ const databaseUrl = process.env.TEST_DATABASE_URL;
87
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
88
+
89
+ describeIfDatabase("PostgreSQLProjectRoomReadStateStore", () => {
90
+ let pool: Pool;
91
+ let store: PostgreSQLProjectRoomReadStateStore;
92
+ let messages: PostgreSQLProjectRoomMessageStore;
93
+ const tenantId = `read-state-${randomUUID()}`;
94
+
95
+ beforeAll(async () => {
96
+ pool = new Pool({ connectionString: databaseUrl });
97
+ const mm = new MigrationManager(pool);
98
+ mm.register(createProjectRoomTables);
99
+ mm.register(createProjectRoomReadStates);
100
+ await mm.migrate();
101
+ store = new PostgreSQLProjectRoomReadStateStore({ pool });
102
+ messages = new PostgreSQLProjectRoomMessageStore({ pool });
103
+ });
104
+
105
+ afterAll(async () => {
106
+ await pool.query("DELETE FROM lattice_project_room_messages WHERE tenant_id = $1", [tenantId]);
107
+ await pool.query("DELETE FROM lattice_project_room_read_states WHERE tenant_id = $1", [tenantId]);
108
+ await pool.end();
109
+ });
110
+
111
+ it("returns null before any report and upserts monotonically", async () => {
112
+ await expect(store.get(tenantId, "room-1", "u1")).resolves.toBeNull();
113
+ await store.markRead({ tenantId, roomId: "room-1", userId: "u1", lastReadAt: new Date("2026-01-02T00:00:00Z") });
114
+ await store.markRead({ tenantId, roomId: "room-1", userId: "u1", lastReadAt: new Date("2026-01-01T00:00:00Z") });
115
+ const state = await store.get(tenantId, "room-1", "u1");
116
+ expect(state?.lastReadAt.toISOString()).toBe("2026-01-02T00:00:00.000Z");
117
+ });
118
+
119
+ it("counts messages after a horizon excluding one human author", async () => {
120
+ const roomId = `room-${randomUUID()}`;
121
+ const base = (id: string, author: ProjectRoomMessage["author"]): Omit<ProjectRoomMessage, "createdAt"> => ({
122
+ id, tenantId, workspaceId: "w1", projectId: "p1", roomId, author,
123
+ content: { type: "text", text: id }, mentions: [], source: "user",
124
+ });
125
+ await messages.create({ ...base(`m0-${roomId}`, { type: "human", userId: "u2" }) });
126
+ await new Promise((resolve) => setTimeout(resolve, 20));
127
+ await messages.create({ ...base(`m1-${roomId}`, { type: "human", userId: "u1" }) });
128
+ await messages.create({ ...base(`m2-${roomId}`, { type: "human", userId: "u2" }) });
129
+ await messages.create({ ...base(`m3-${roomId}`, { type: "bot", membershipId: "b1", assistantId: "a1" }) });
130
+ const first = await messages.findById(tenantId, `m0-${roomId}`);
131
+ const count = await messages.countAfter({
132
+ tenantId, roomId, after: first!.createdAt, excludeAuthorUserId: "u1",
133
+ });
134
+ expect(count).toBe(2);
135
+ });
136
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
2
+ import type { Pool } from "pg";
3
+ import { PostgresSharedResourceStore } from "../stores/PostgresSharedResourceStore";
4
+
5
+ const mockQuery = jest.fn();
6
+
7
+ describe("PostgresSharedResourceStore openGrants", () => {
8
+ const pool = { query: mockQuery } as unknown as Pool;
9
+ beforeEach(() => jest.clearAllMocks());
10
+
11
+ it("persists open_grants as JSON on create", async () => {
12
+ const store = new PostgresSharedResourceStore({ pool } as never);
13
+ const openGrants = [{ domain: "kb", items: ["search"] }];
14
+ mockQuery.mockResolvedValueOnce({
15
+ rows: [
16
+ {
17
+ token: "tok",
18
+ address: JSON.stringify({ volume: "v", resourcePath: "index.html" }),
19
+ title: null,
20
+ created_by: "u1",
21
+ tenant_id: "t1",
22
+ workspace_id: "w1",
23
+ project_id: "p1",
24
+ assistant_id: null,
25
+ vm_isolation: "project",
26
+ visibility: "public",
27
+ password_hash: null,
28
+ expires_at: null,
29
+ max_access: null,
30
+ open_grants: openGrants,
31
+ access_count: 0,
32
+ revoked: false,
33
+ created_at: new Date().toISOString(),
34
+ version: 1,
35
+ },
36
+ ],
37
+ });
38
+
39
+ const record = await store.create({
40
+ token: "tok",
41
+ address: { volume: "v", resourcePath: "index.html" },
42
+ title: null,
43
+ createdBy: "u1",
44
+ tenantId: "t1",
45
+ workspaceId: "w1",
46
+ projectId: "p1",
47
+ assistantId: null,
48
+ vmIsolation: "project",
49
+ visibility: "public",
50
+ passwordHash: null,
51
+ expiresAt: null,
52
+ maxAccess: null,
53
+ openGrants,
54
+ revoked: false,
55
+ } as never);
56
+
57
+ const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]];
58
+ expect(sql).toContain("open_grants");
59
+ expect(params).toContain(JSON.stringify(openGrants));
60
+ expect(record.openGrants).toEqual(openGrants);
61
+ });
62
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * create_open_audit_table migration tests
3
+ */
4
+
5
+ import { describe, expect, it, jest } from "@jest/globals";
6
+ import type { PoolClient } from "pg";
7
+ import { createOpenAuditTable } from "../migrations/create_open_audit_table";
8
+
9
+ describe("create_open_audit_table migration", () => {
10
+ it("has name create_open_audit_table", () => {
11
+ expect(createOpenAuditTable.name).toBe("create_open_audit_table");
12
+ });
13
+
14
+ it("has version 179", () => {
15
+ expect(createOpenAuditTable.version).toBe(179);
16
+ });
17
+
18
+ it("creates the audit table and tenant index in up()", async () => {
19
+ const queries: string[] = [];
20
+ const client = {
21
+ query: jest.fn(async (sql: string) => {
22
+ queries.push(sql);
23
+ }),
24
+ };
25
+ await createOpenAuditTable.up(client as unknown as PoolClient);
26
+ expect(queries.some((q) => q.includes("CREATE TABLE IF NOT EXISTS lattice_open_audit"))).toBe(true);
27
+ expect(queries.some((q) => q.includes("idx_lattice_open_audit_tenant_created"))).toBe(true);
28
+ });
29
+
30
+ it("drops the table in down()", async () => {
31
+ const queries: string[] = [];
32
+ const client = {
33
+ query: jest.fn(async (sql: string) => {
34
+ queries.push(sql);
35
+ }),
36
+ };
37
+ await createOpenAuditTable.down!(client as unknown as PoolClient);
38
+ expect(queries.some((q) => q.includes("DROP TABLE IF EXISTS lattice_open_audit"))).toBe(true);
39
+ });
40
+ });
@@ -17,6 +17,7 @@ import { ThreadMessageQueueStore } from "./stores/ThreadMessageQueueStore";
17
17
  import { ChannelBindingStore } from "./stores/ChannelBindingStore";
18
18
  import { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
19
19
  import { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
20
+ import { PostgreSQLOpenAuditStore } from "./stores/PostgreSQLOpenAuditStore";
20
21
  import { PostgreSQLAgentWebAppStore } from "./stores/PostgreSQLAgentWebAppStore";
21
22
  import { PostgreSQLScheduleStorage } from "./stores/PostgreSQLScheduleStorage";
22
23
  import { PostgreSQLTaskStore } from "./stores/PostgreSQLTaskStore";
@@ -67,6 +68,9 @@ import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantSta
67
68
  import { evalMigrations } from "./migrations/eval_migrations";
68
69
  import { createA2AApiKeysTable } from "./migrations/a2a_api_key_migration";
69
70
  import { addA2AKeyAssistantIds } from "./migrations/add_a2a_key_assistant_ids";
71
+ import { addApiKeyGrants } from "./migrations/add_api_key_grants";
72
+ import { createOpenAuditTable } from "./migrations/create_open_audit_table";
73
+ import { addShareOpenGrants } from "./migrations/add_share_open_grants";
70
74
  import { taskMigrations } from "./migrations/task_migration";
71
75
  import { createMenuItemsTable } from "./migrations/menu_items_migration";
72
76
  import { addFileContentType } from "./migrations/menu_items_add_file_type";
@@ -82,12 +86,13 @@ import {
82
86
  import { createAgentWebAppsTable } from "./migrations/agent_web_apps_migration";
83
87
  import { addAgentWebAppsIdentity } from "./migrations/add_agent_web_apps_identity_migration";
84
88
  import { createCapabilityBundlesTable } from "./migrations/capability_bundle_migration";
85
- import { createProjectRoomTables } from "./migrations/project_room_migration";
89
+ import { createProjectRoomReadStates, createProjectRoomTables } from "./migrations/project_room_migration";
86
90
  import { PostgreSQLCapabilityBundleStore } from "./stores/PostgreSQLCapabilityBundleStore";
87
91
  import { PostgreSQLProjectRoomStore } from "./stores/PostgreSQLProjectRoomStore";
88
92
  import { PostgreSQLProjectMembershipStore } from "./stores/PostgreSQLProjectMembershipStore";
89
93
  import { PostgreSQLProjectBotMembershipStore } from "./stores/PostgreSQLProjectBotMembershipStore";
90
94
  import { PostgreSQLProjectRoomMessageStore } from "./stores/PostgreSQLProjectRoomMessageStore";
95
+ import { PostgreSQLProjectRoomReadStateStore } from "./stores/PostgreSQLProjectRoomReadStateStore";
91
96
 
92
97
  export async function createPgStoreConfig(connectionString: string) {
93
98
  const pool = new Pool({ connectionString });
@@ -147,6 +152,9 @@ export async function createPgStoreConfig(connectionString: string) {
147
152
  mm.register(addWorkflowRunsTenantStatusUpdatedIndex); // v161
148
153
  mm.register(addProjectKindColumn); // v165
149
154
  mm.register(addA2AKeyAssistantIds); // v167
155
+ mm.register(addApiKeyGrants); // v178
156
+ mm.register(createOpenAuditTable); // v179
157
+ mm.register(addShareOpenGrants); // v180
150
158
  mm.register(addTaskWorkItemEventKeyMigration); // v168
151
159
  mm.register(createAgentWebAppsTable); // v169
152
160
  mm.register(createCapabilityBundlesTable); // v170
@@ -155,6 +163,7 @@ export async function createPgStoreConfig(connectionString: string) {
155
163
  mm.register(addTrustedRunContextColumn); // v173
156
164
  mm.register(addProjectLifecycleEventIndex); // v175
157
165
  mm.register(addAgentWebAppsIdentity); // v176
166
+ mm.register(createProjectRoomReadStates); // v177
158
167
 
159
168
  await mm.migrate();
160
169
 
@@ -187,6 +196,7 @@ export async function createPgStoreConfig(connectionString: string) {
187
196
  task: new PostgreSQLTaskStore(opts),
188
197
  taskWorkItem: taskWorkItemStore,
189
198
  a2aApiKey: new PostgreSQLA2AApiKeyStore(opts),
199
+ openAudit: new PostgreSQLOpenAuditStore(opts),
190
200
  agentWebApp: new PostgreSQLAgentWebAppStore(opts),
191
201
  capabilityBundle: new PostgreSQLCapabilityBundleStore(opts),
192
202
  schedule: new PostgreSQLScheduleStorage(opts),
@@ -197,6 +207,7 @@ export async function createPgStoreConfig(connectionString: string) {
197
207
  projectMembership: new PostgreSQLProjectMembershipStore(opts),
198
208
  projectBotMembership: new PostgreSQLProjectBotMembershipStore(opts),
199
209
  projectRoomMessage: new PostgreSQLProjectRoomMessageStore(opts),
210
+ projectRoomReadState: new PostgreSQLProjectRoomReadStateStore(opts),
200
211
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
201
212
  checkpoint,
202
213
  };
package/src/index.ts CHANGED
@@ -57,6 +57,7 @@ export * from "./stores/ThreadMessageQueueStore";
57
57
  export * from "./stores/ChannelIdentityMappingStore";
58
58
  export * from "./stores/PostgreSQLChannelInstallationStore";
59
59
  export * from "./stores/PostgreSQLA2AApiKeyStore";
60
+ export * from "./stores/PostgreSQLOpenAuditStore";
60
61
  export * from "./stores/PostgreSQLAgentWebAppStore";
61
62
  export * from "./stores/PostgreSQLTaskWorkItemStore";
62
63
  export * from "./stores/PostgreSQLCapabilityBundleStore";
@@ -75,6 +76,7 @@ export * from "./stores/PostgreSQLProjectRoomStore";
75
76
  export * from "./stores/PostgreSQLProjectMembershipStore";
76
77
  export * from "./stores/PostgreSQLProjectBotMembershipStore";
77
78
  export * from "./stores/PostgreSQLProjectRoomMessageStore";
79
+ export * from "./stores/PostgreSQLProjectRoomReadStateStore";
78
80
  export * from "./migrations/project_room_migration";
79
81
 
80
82
  export * from "./PGVectorStoreProvider";
@@ -0,0 +1,25 @@
1
+ import type { Migration } from "./migration";
2
+ import type { PoolClient } from "pg";
3
+
4
+ /**
5
+ * Adds the Open-door grants JSONB column to API keys.
6
+ *
7
+ * Absent/empty grants → legacy derivation via effectiveGrants()
8
+ * (assistantIds → agent-domain grant). The A2A door never reads this column.
9
+ */
10
+ export const addApiKeyGrants: Migration = {
11
+ version: 178,
12
+ name: "add_api_key_grants",
13
+ up: async (client: PoolClient) => {
14
+ await client.query(`
15
+ ALTER TABLE lattice_a2a_api_keys
16
+ ADD COLUMN IF NOT EXISTS grants JSONB
17
+ `);
18
+ },
19
+ down: async (client: PoolClient) => {
20
+ await client.query(`
21
+ ALTER TABLE lattice_a2a_api_keys
22
+ DROP COLUMN IF EXISTS grants
23
+ `);
24
+ },
25
+ };
@@ -0,0 +1,23 @@
1
+ import type { Migration } from "./migration";
2
+ import type { PoolClient } from "pg";
3
+
4
+ /**
5
+ * Adds Open-door grants to resource shares. Sandbox tokens minted for a share
6
+ * append these (sanitized against the V1 whitelist) to their self grant.
7
+ */
8
+ export const addShareOpenGrants: Migration = {
9
+ version: 180,
10
+ name: "add_share_open_grants",
11
+ up: async (client: PoolClient) => {
12
+ await client.query(`
13
+ ALTER TABLE lattice_shared_resources
14
+ ADD COLUMN IF NOT EXISTS open_grants JSONB
15
+ `);
16
+ },
17
+ down: async (client: PoolClient) => {
18
+ await client.query(`
19
+ ALTER TABLE lattice_shared_resources
20
+ DROP COLUMN IF EXISTS open_grants
21
+ `);
22
+ },
23
+ };