@axiom-lattice/pg-stores 4.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "4.0.4",
3
+ "version": "4.1.0",
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": "10.0.4",
29
- "@axiom-lattice/protocols": "6.2.0"
28
+ "@axiom-lattice/core": "10.1.0",
29
+ "@axiom-lattice/protocols": "6.3.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/jest": "^29.5.14",
@@ -0,0 +1,210 @@
1
+ import { randomUUID } from "crypto";
2
+ import { Pool } from "pg";
3
+ import type { CreateShareRecord } from "@axiom-lattice/protocols";
4
+ import { PostgresSharedResourceStore } from "../stores/PostgresSharedResourceStore";
5
+
6
+ const url = process.env.TEST_DATABASE_URL;
7
+ (url ? describe : describe.skip)("PostgresSharedResourceStore kind", () => {
8
+ const runId = randomUUID();
9
+ const tenantId = `t_${runId}`;
10
+ const userId = `u_${runId}`;
11
+ const workspaceId = `w_${runId}`;
12
+ const projectId = `p_${runId}`;
13
+ let store: PostgresSharedResourceStore;
14
+ let admin: Pool;
15
+ let tokenCounter = 0;
16
+
17
+ const nextToken = (prefix: string): string => {
18
+ tokenCounter += 1;
19
+ return `${prefix}_${runId}_${tokenCounter}`;
20
+ };
21
+
22
+ const baseRecord = (token: string) => ({
23
+ token,
24
+ title: "Share",
25
+ createdBy: userId,
26
+ tenantId,
27
+ workspaceId,
28
+ projectId,
29
+ assistantId: null,
30
+ vmIsolation: "project",
31
+ visibility: "public" as const,
32
+ passwordHash: null,
33
+ expiresAt: null,
34
+ maxAccess: null,
35
+ revoked: false,
36
+ });
37
+
38
+ const mcpAppAddress = {
39
+ kind: "mcpApp" as const,
40
+ app: {
41
+ kind: "plugin" as const,
42
+ pluginType: "demo",
43
+ resource: "ui/app.html",
44
+ toolName: "render",
45
+ scope: "signed",
46
+ },
47
+ };
48
+
49
+ beforeAll(async () => {
50
+ store = new PostgresSharedResourceStore({
51
+ poolConfig: { connectionString: url! },
52
+ });
53
+ admin = new Pool({ connectionString: url });
54
+ await store.initialize();
55
+ });
56
+
57
+ afterAll(async () => {
58
+ if (admin) {
59
+ await admin.query(
60
+ "DELETE FROM lattice_shared_resources WHERE tenant_id = $1",
61
+ [tenantId],
62
+ );
63
+ await admin.end();
64
+ }
65
+ await store.dispose();
66
+ });
67
+
68
+ it("round-trips file and mcpApp records", async () => {
69
+ const fileToken = nextToken("file");
70
+ const fileRecord = await store.create({
71
+ ...baseRecord(fileToken),
72
+ kind: "file",
73
+ address: { volume: "project", resourcePath: "docs/readme.md" },
74
+ } satisfies CreateShareRecord);
75
+
76
+ expect(fileRecord.kind).toBe("file");
77
+ expect(fileRecord.address).toEqual({
78
+ volume: "project",
79
+ resourcePath: "docs/readme.md",
80
+ });
81
+
82
+ const foundFile = await store.findByToken(fileToken);
83
+ expect(foundFile?.kind).toBe("file");
84
+ if (foundFile?.kind !== "file") {
85
+ throw new Error("expected file share record");
86
+ }
87
+ expect(foundFile.address).toEqual({
88
+ volume: "project",
89
+ resourcePath: "docs/readme.md",
90
+ });
91
+
92
+ const mcpToken = nextToken("mcp");
93
+ const mcpRecord = await store.create({
94
+ ...baseRecord(mcpToken),
95
+ kind: "mcpApp",
96
+ address: mcpAppAddress,
97
+ } satisfies CreateShareRecord);
98
+
99
+ expect(mcpRecord.kind).toBe("mcpApp");
100
+ if (mcpRecord.kind !== "mcpApp") {
101
+ throw new Error("expected mcpApp share record");
102
+ }
103
+ expect(mcpRecord.address.app.pluginType).toBe("demo");
104
+
105
+ const foundMcp = await store.findByToken(mcpToken);
106
+ expect(foundMcp?.kind).toBe("mcpApp");
107
+ if (foundMcp?.kind !== "mcpApp") {
108
+ throw new Error("expected mcpApp share record");
109
+ }
110
+ expect(foundMcp.address.app.resource).toBe("ui/app.html");
111
+ });
112
+
113
+ it("lists both kinds for the same user", async () => {
114
+ const fileToken = nextToken("list_file");
115
+ await store.create({
116
+ ...baseRecord(fileToken),
117
+ kind: "file",
118
+ address: { volume: "project", resourcePath: "docs/list.md" },
119
+ } satisfies CreateShareRecord);
120
+
121
+ const mcpToken = nextToken("list_mcp");
122
+ await store.create({
123
+ ...baseRecord(mcpToken),
124
+ kind: "mcpApp",
125
+ address: mcpAppAddress,
126
+ } satisfies CreateShareRecord);
127
+
128
+ const records = await store.listByUser(tenantId, userId);
129
+ const tokens = records.map((record) => record.token);
130
+ expect(tokens).toContain(fileToken);
131
+ expect(tokens).toContain(mcpToken);
132
+ expect(new Set(records.map((record) => record.kind))).toEqual(
133
+ new Set(["file", "mcpApp"]),
134
+ );
135
+ });
136
+
137
+ it("rejects an unknown kind at the database level", async () => {
138
+ await expect(
139
+ admin.query(
140
+ `INSERT INTO lattice_shared_resources
141
+ (token, kind, address, created_by, tenant_id, workspace_id, project_id)
142
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
143
+ [
144
+ nextToken("bogus"),
145
+ "bogus",
146
+ JSON.stringify({ volume: "project", resourcePath: "x.txt" }),
147
+ userId,
148
+ tenantId,
149
+ workspaceId,
150
+ projectId,
151
+ ],
152
+ ),
153
+ ).rejects.toThrow(/lattice_shared_resources_kind_check/);
154
+ });
155
+
156
+ it("rejects mcpApp shares with internal visibility", async () => {
157
+ await expect(
158
+ admin.query(
159
+ `INSERT INTO lattice_shared_resources
160
+ (token, kind, address, created_by, tenant_id, workspace_id, project_id, visibility)
161
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
162
+ [
163
+ nextToken("internal"),
164
+ "mcpApp",
165
+ JSON.stringify(mcpAppAddress),
166
+ userId,
167
+ tenantId,
168
+ workspaceId,
169
+ projectId,
170
+ "internal",
171
+ ],
172
+ ),
173
+ ).rejects.toThrow(
174
+ /lattice_shared_resources_mcp_app_visibility_check/,
175
+ );
176
+ });
177
+
178
+ it("maps a legacy row without kind to file via the column default", async () => {
179
+ const legacyToken = nextToken("legacy");
180
+ await admin.query(
181
+ `INSERT INTO lattice_shared_resources
182
+ (token, address, created_by, tenant_id, workspace_id, project_id)
183
+ VALUES ($1, $2, $3, $4, $5, $6)`,
184
+ [
185
+ legacyToken,
186
+ JSON.stringify({ volume: "project", resourcePath: "legacy.txt" }),
187
+ userId,
188
+ tenantId,
189
+ workspaceId,
190
+ projectId,
191
+ ],
192
+ );
193
+
194
+ const { rows } = await admin.query<{ kind: string }>(
195
+ "SELECT kind FROM lattice_shared_resources WHERE token = $1",
196
+ [legacyToken],
197
+ );
198
+ expect(rows[0]?.kind).toBe("file");
199
+
200
+ const legacy = await store.findByToken(legacyToken);
201
+ expect(legacy?.kind).toBe("file");
202
+ if (legacy?.kind !== "file") {
203
+ throw new Error("expected legacy file share record");
204
+ }
205
+ expect(legacy.address).toEqual({
206
+ volume: "project",
207
+ resourcePath: "legacy.txt",
208
+ });
209
+ });
210
+ });
@@ -66,6 +66,7 @@ import { addA2AKeyAssistantIds } from "./migrations/add_a2a_key_assistant_ids";
66
66
  import { addApiKeyGrants } from "./migrations/add_api_key_grants";
67
67
  import { createOpenAuditTable } from "./migrations/create_open_audit_table";
68
68
  import { addShareOpenGrants } from "./migrations/add_share_open_grants";
69
+ import { addShareKind } from "./migrations/add_share_kind";
69
70
  import { taskMigrations } from "./migrations/task_migration";
70
71
  import { createMenuItemsTable } from "./migrations/menu_items_migration";
71
72
  import { addFileContentType } from "./migrations/menu_items_add_file_type";
@@ -224,6 +225,7 @@ export async function createPgStoreConfig(connectionString: string) {
224
225
  mm.register(createAgentRunProjectTaskMigration); // v203
225
226
  mm.register(agentRunProjectTaskRunningIndexMigration); // v204
226
227
  mm.register(addModelProviderKindDefault); // v207
228
+ mm.register(addShareKind); // v208
227
229
 
228
230
  await mm.migrate();
229
231
 
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export * from "./migrations/channel_installation_migrations";
50
50
  export * from "./migrations/channel_installations_alter_migration";
51
51
  export * from "./migrations/channel_bindings_migration";
52
52
  export * from "./migrations/shared_resources_migration";
53
+ export * from "./migrations/add_share_kind";
53
54
  export * from "./migrations/a2a_api_key_migration";
54
55
  export * from "./migrations/agent_web_apps_migration";
55
56
  export * from "./migrations/workflow_tracking_migrations";
@@ -0,0 +1,45 @@
1
+ import type { PoolClient } from "pg";
2
+ import type { Migration } from "./migration";
3
+
4
+ export const addShareKind: Migration = {
5
+ version: 208,
6
+ name: "add_share_kind",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ ALTER TABLE lattice_shared_resources
10
+ ADD COLUMN IF NOT EXISTS kind VARCHAR(16) NOT NULL DEFAULT 'file'
11
+ `);
12
+ await client.query(`
13
+ ALTER TABLE lattice_shared_resources
14
+ DROP CONSTRAINT IF EXISTS lattice_shared_resources_kind_check
15
+ `);
16
+ await client.query(`
17
+ ALTER TABLE lattice_shared_resources
18
+ ADD CONSTRAINT lattice_shared_resources_kind_check
19
+ CHECK (kind IN ('file', 'mcpApp'))
20
+ `);
21
+ await client.query(`
22
+ ALTER TABLE lattice_shared_resources
23
+ DROP CONSTRAINT IF EXISTS lattice_shared_resources_mcp_app_visibility_check
24
+ `);
25
+ await client.query(`
26
+ ALTER TABLE lattice_shared_resources
27
+ ADD CONSTRAINT lattice_shared_resources_mcp_app_visibility_check
28
+ CHECK (kind <> 'mcpApp' OR visibility IN ('public', 'password'))
29
+ `);
30
+ },
31
+ down: async (client: PoolClient) => {
32
+ await client.query(`
33
+ ALTER TABLE lattice_shared_resources
34
+ DROP CONSTRAINT IF EXISTS lattice_shared_resources_mcp_app_visibility_check
35
+ `);
36
+ await client.query(`
37
+ ALTER TABLE lattice_shared_resources
38
+ DROP CONSTRAINT IF EXISTS lattice_shared_resources_kind_check
39
+ `);
40
+ await client.query(`
41
+ ALTER TABLE lattice_shared_resources
42
+ DROP COLUMN IF EXISTS kind
43
+ `);
44
+ },
45
+ };
@@ -1,9 +1,16 @@
1
1
  import { Pool } from "pg";
2
2
  import type { PoolConfig } from "pg";
3
- import type { ShareRecord, ResourceAddress } from "@axiom-lattice/protocols";
3
+ import type {
4
+ CreateShareRecord,
5
+ McpAppShareApp,
6
+ ShareRecord,
7
+ ShareVisibility,
8
+ } from "@axiom-lattice/protocols";
4
9
  import type { SharedResourceStore } from "./SharedResourceStore";
5
10
  import { MigrationManager } from "../migrations/migration";
6
11
  import { createSharedResourcesTable } from "../migrations/shared_resources_migration";
12
+ import { addShareOpenGrants } from "../migrations/add_share_open_grants";
13
+ import { addShareKind } from "../migrations/add_share_kind";
7
14
 
8
15
  export interface PostgresSharedResourceStoreOptions {
9
16
  pool?: Pool;
@@ -11,9 +18,64 @@ export interface PostgresSharedResourceStoreOptions {
11
18
  autoMigrate?: boolean;
12
19
  }
13
20
 
21
+ /**
22
+ * Raised when a persisted MCP App share row does not carry a valid app target.
23
+ */
24
+ export class InvalidMcpAppShareAddressError extends Error {
25
+ /** Machine-readable failure code. */
26
+ readonly code = "INVALID_MCP_APP_SHARE_ADDRESS" as const;
27
+
28
+ /**
29
+ * @param token - Share token owning the invalid address.
30
+ */
31
+ constructor(token: string) {
32
+ super(`Share ${token} has an invalid MCP App address`);
33
+ this.name = "InvalidMcpAppShareAddressError";
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Raised when a persisted MCP App share row uses an unsupported visibility.
39
+ */
40
+ export class InvalidMcpAppShareVisibilityError extends Error {
41
+ /** Machine-readable failure code. */
42
+ readonly code = "INVALID_MCP_APP_SHARE_VISIBILITY" as const;
43
+ /** Visibility value found in the row. */
44
+ readonly visibility: string;
45
+
46
+ /**
47
+ * @param token - Share token owning the invalid visibility.
48
+ * @param visibility - Visibility value found in the row.
49
+ */
50
+ constructor(token: string, visibility: string) {
51
+ super(
52
+ `Share ${token} has unsupported MCP App visibility: ${visibility}`,
53
+ );
54
+ this.name = "InvalidMcpAppShareVisibilityError";
55
+ this.visibility = visibility;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Raised when a persisted file share row does not carry a valid resource address.
61
+ */
62
+ export class InvalidFileShareAddressError extends Error {
63
+ /** Machine-readable failure code. */
64
+ readonly code = "INVALID_FILE_SHARE_ADDRESS" as const;
65
+
66
+ /**
67
+ * @param token - Share token owning the invalid address.
68
+ */
69
+ constructor(token: string) {
70
+ super(`Share ${token} has an invalid file address`);
71
+ this.name = "InvalidFileShareAddressError";
72
+ }
73
+ }
74
+
14
75
  type ShareRow = {
15
76
  token: string;
16
- address: ResourceAddress;
77
+ kind: string | null;
78
+ address: unknown;
17
79
  title: string | null;
18
80
  created_by: string;
19
81
  tenant_id: string;
@@ -57,6 +119,8 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
57
119
 
58
120
  this.migrationManager = new MigrationManager(this.pool);
59
121
  this.migrationManager.register(createSharedResourcesTable);
122
+ this.migrationManager.register(addShareOpenGrants);
123
+ this.migrationManager.register(addShareKind);
60
124
 
61
125
  if (options.autoMigrate !== false) {
62
126
  this.initialize().catch((error) => {
@@ -99,19 +163,18 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
99
163
  return this._mapRow(rows[0]);
100
164
  }
101
165
 
102
- async create(
103
- record: Omit<ShareRecord, "accessCount" | "createdAt" | "version">,
104
- ): Promise<ShareRecord> {
166
+ async create(record: CreateShareRecord): Promise<ShareRecord> {
105
167
  await this.ensureInitialized();
106
168
  const { rows } = await this.pool.query<ShareRow>(
107
169
  `INSERT INTO lattice_shared_resources
108
- (token, address, title, created_by, tenant_id, workspace_id, project_id,
170
+ (token, kind, address, title, created_by, tenant_id, workspace_id, project_id,
109
171
  assistant_id, vm_isolation, visibility, password_hash,
110
172
  expires_at, max_access, open_grants, revoked)
111
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
173
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
112
174
  RETURNING *`,
113
175
  [
114
176
  record.token,
177
+ record.kind ?? "file",
115
178
  JSON.stringify(record.address),
116
179
  record.title,
117
180
  record.createdBy,
@@ -210,16 +273,9 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
210
273
  }
211
274
 
212
275
  private _mapRow(row: ShareRow): ShareRecord {
213
- let address: ResourceAddress;
214
- if (typeof row.address === "string") {
215
- address = JSON.parse(row.address) as ResourceAddress;
216
- } else {
217
- address = row.address as ResourceAddress;
218
- }
219
-
220
- return {
276
+ const address = parseShareAddress(row.address);
277
+ const base = {
221
278
  token: row.token,
222
- address,
223
279
  title: row.title,
224
280
  createdBy: row.created_by,
225
281
  tenantId: row.tenant_id,
@@ -227,7 +283,6 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
227
283
  projectId: row.project_id,
228
284
  assistantId: row.assistant_id,
229
285
  vmIsolation: row.vm_isolation,
230
- visibility: row.visibility as ShareRecord["visibility"],
231
286
  passwordHash: row.password_hash,
232
287
  expiresAt: row.expires_at ? new Date(row.expires_at) : null,
233
288
  maxAccess: row.max_access,
@@ -237,5 +292,58 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
237
292
  createdAt: new Date(row.created_at),
238
293
  version: row.version ?? 1,
239
294
  };
295
+
296
+ if (row.kind === "mcpApp") {
297
+ if (!isRecord(address) || address.kind !== "mcpApp") {
298
+ throw new InvalidMcpAppShareAddressError(row.token);
299
+ }
300
+ const app = address.app;
301
+ if (!isMcpAppShareApp(app)) {
302
+ throw new InvalidMcpAppShareAddressError(row.token);
303
+ }
304
+ if (row.visibility !== "public" && row.visibility !== "password") {
305
+ throw new InvalidMcpAppShareVisibilityError(row.token, row.visibility);
306
+ }
307
+ return {
308
+ ...base,
309
+ kind: "mcpApp",
310
+ address: { kind: "mcpApp", app },
311
+ visibility: row.visibility,
312
+ };
313
+ }
314
+
315
+ if (
316
+ !isRecord(address) ||
317
+ typeof address.volume !== "string" ||
318
+ typeof address.resourcePath !== "string"
319
+ ) {
320
+ throw new InvalidFileShareAddressError(row.token);
321
+ }
322
+ return {
323
+ ...base,
324
+ kind: "file",
325
+ address: {
326
+ volume: address.volume,
327
+ resourcePath: address.resourcePath,
328
+ },
329
+ visibility: row.visibility as ShareVisibility,
330
+ };
331
+ }
332
+ }
333
+
334
+ function isRecord(value: unknown): value is Record<string, unknown> {
335
+ return typeof value === "object" && value !== null && !Array.isArray(value);
336
+ }
337
+
338
+ function isMcpAppShareApp(value: unknown): value is McpAppShareApp {
339
+ return isRecord(value) && (value.kind === "plugin" || value.kind === "mcp");
340
+ }
341
+
342
+ function parseShareAddress(raw: unknown): unknown {
343
+ if (typeof raw !== "string") return raw;
344
+ try {
345
+ return JSON.parse(raw) as unknown;
346
+ } catch {
347
+ return null;
240
348
  }
241
349
  }