@axiom-lattice/pg-stores 4.0.5 → 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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +15 -0
- package/dist/index.d.mts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +167 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +163 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/PostgresSharedResourceStore.kind.test.ts +210 -0
- package/src/createPgStoreConfig.ts +2 -0
- package/src/index.ts +1 -0
- package/src/migrations/add_share_kind.ts +45 -0
- package/src/stores/PostgresSharedResourceStore.ts +125 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/pg-stores",
|
|
3
|
-
"version": "4.0
|
|
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
|
|
29
|
-
"@axiom-lattice/protocols": "6.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
}
|