@axiom-lattice/local-stores 1.0.1

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.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Local SQLite implementation of McpServerConfigStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ McpServerConfigStore,
8
+ McpServerConfigEntry,
9
+ CreateMcpServerConfigRequest,
10
+ UpdateMcpServerConfigRequest,
11
+ McpServerConfig,
12
+ } from "@axiom-lattice/protocols";
13
+ import { ensureTable, nowISO, parseISO } from "../database";
14
+ import { encrypt, decrypt } from "@axiom-lattice/core";
15
+
16
+ const DDL = `
17
+ CREATE TABLE IF NOT EXISTS lt_mcp_configs (
18
+ id TEXT NOT NULL,
19
+ tenant_id TEXT NOT NULL,
20
+ key TEXT NOT NULL,
21
+ name TEXT,
22
+ description TEXT,
23
+ config TEXT NOT NULL,
24
+ selected_tools TEXT DEFAULT '[]',
25
+ is_env_encrypted INTEGER NOT NULL DEFAULT 0,
26
+ status TEXT NOT NULL DEFAULT 'disconnected',
27
+ created_at TEXT NOT NULL,
28
+ updated_at TEXT NOT NULL,
29
+ PRIMARY KEY (tenant_id, id)
30
+ );
31
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_lt_mcp_key ON lt_mcp_configs(tenant_id, key);
32
+ `;
33
+
34
+ interface ConfigRow {
35
+ id: string;
36
+ tenant_id: string;
37
+ key: string;
38
+ name: string | null;
39
+ description: string | null;
40
+ config: string;
41
+ selected_tools: string;
42
+ is_env_encrypted: number;
43
+ status: string;
44
+ created_at: string;
45
+ updated_at: string;
46
+ }
47
+
48
+ export class LocalMcpServerConfigStore implements McpServerConfigStore {
49
+ private db: DatabaseWrapper;
50
+
51
+ constructor(db: DatabaseWrapper) {
52
+ this.db = db;
53
+ ensureTable(db, DDL);
54
+ }
55
+
56
+ async getAllConfigs(tenantId: string): Promise<McpServerConfigEntry[]> {
57
+ const rows = this.db.prepare(
58
+ `SELECT * FROM lt_mcp_configs WHERE tenant_id = ? ORDER BY created_at DESC`,
59
+ ).all(tenantId) as unknown as ConfigRow[];
60
+ return rows.map(mapRowToEntry);
61
+ }
62
+
63
+ async getAllConfigsWithoutTenant(): Promise<McpServerConfigEntry[]> {
64
+ const rows = this.db.prepare(
65
+ `SELECT * FROM lt_mcp_configs ORDER BY created_at DESC`,
66
+ ).all() as unknown as ConfigRow[];
67
+ return rows.map(mapRowToEntry);
68
+ }
69
+
70
+ async getConfigById(tenantId: string, id: string): Promise<McpServerConfigEntry | null> {
71
+ const row = this.db.prepare(
72
+ `SELECT * FROM lt_mcp_configs WHERE tenant_id = ? AND id = ?`,
73
+ ).get(tenantId, id) as unknown as ConfigRow | undefined;
74
+ return row ? mapRowToEntry(row) : null;
75
+ }
76
+
77
+ async getConfigByKey(tenantId: string, key: string): Promise<McpServerConfigEntry | null> {
78
+ const row = this.db.prepare(
79
+ `SELECT * FROM lt_mcp_configs WHERE tenant_id = ? AND key = ?`,
80
+ ).get(tenantId, key) as unknown as ConfigRow | undefined;
81
+ return row ? mapRowToEntry(row) : null;
82
+ }
83
+
84
+ async createConfig(
85
+ tenantId: string,
86
+ id: string,
87
+ data: CreateMcpServerConfigRequest,
88
+ ): Promise<McpServerConfigEntry> {
89
+ const now = nowISO();
90
+ const { config: configWithEncryptedEnv, isEnvEncrypted } = encryptEnvInConfig(data.config);
91
+
92
+ this.db.prepare(
93
+ `INSERT INTO lt_mcp_configs (id, tenant_id, key, name, description, config, selected_tools, is_env_encrypted, status, created_at, updated_at)
94
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
95
+ ON CONFLICT(tenant_id, id) DO UPDATE SET
96
+ key = excluded.key,
97
+ name = excluded.name,
98
+ description = excluded.description,
99
+ config = excluded.config,
100
+ selected_tools = excluded.selected_tools,
101
+ is_env_encrypted = excluded.is_env_encrypted,
102
+ status = excluded.status,
103
+ updated_at = excluded.updated_at`,
104
+ ).run(
105
+ id, tenantId, data.key, data.name || null, data.description || null,
106
+ JSON.stringify(configWithEncryptedEnv),
107
+ JSON.stringify(data.selectedTools || []),
108
+ isEnvEncrypted ? 1 : 0, "disconnected",
109
+ now, now,
110
+ );
111
+
112
+ return {
113
+ id,
114
+ tenantId,
115
+ key: data.key,
116
+ config: data.config,
117
+ name: data.name,
118
+ description: data.description,
119
+ selectedTools: data.selectedTools || [],
120
+ isEnvEncrypted,
121
+ status: "disconnected",
122
+ createdAt: parseISO(now),
123
+ updatedAt: parseISO(now),
124
+ };
125
+ }
126
+
127
+ async updateConfig(
128
+ tenantId: string,
129
+ id: string,
130
+ updates: Partial<UpdateMcpServerConfigRequest>,
131
+ ): Promise<McpServerConfigEntry | null> {
132
+ const existing = await this.getConfigById(tenantId, id);
133
+ if (!existing) return null;
134
+
135
+ const setClauses: string[] = [];
136
+ const values: unknown[] = [];
137
+
138
+ if (updates.key !== undefined) { setClauses.push("key = ?"); values.push(updates.key); }
139
+ if (updates.name !== undefined) { setClauses.push("name = ?"); values.push(updates.name || null); }
140
+ if (updates.description !== undefined) { setClauses.push("description = ?"); values.push(updates.description || null); }
141
+ if (updates.config !== undefined) {
142
+ const { config: configWithEncryptedEnv, isEnvEncrypted } = encryptEnvInConfig(updates.config);
143
+ setClauses.push("config = ?"); values.push(JSON.stringify(configWithEncryptedEnv));
144
+ setClauses.push("is_env_encrypted = ?"); values.push(isEnvEncrypted ? 1 : 0);
145
+ }
146
+ if (updates.selectedTools !== undefined) { setClauses.push("selected_tools = ?"); values.push(JSON.stringify(updates.selectedTools)); }
147
+ if (updates.status !== undefined) { setClauses.push("status = ?"); values.push(updates.status); }
148
+
149
+ if (setClauses.length === 0) return existing;
150
+
151
+ const now = nowISO();
152
+ setClauses.push("updated_at = ?");
153
+ values.push(now);
154
+ values.push(tenantId, id);
155
+
156
+ this.db.prepare(
157
+ `UPDATE lt_mcp_configs SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`,
158
+ ).run(...values);
159
+
160
+ return this.getConfigById(tenantId, id);
161
+ }
162
+
163
+ async deleteConfig(tenantId: string, id: string): Promise<boolean> {
164
+ const result = this.db.prepare(
165
+ `DELETE FROM lt_mcp_configs WHERE tenant_id = ? AND id = ?`,
166
+ ).run(tenantId, id);
167
+ return result.changes > 0;
168
+ }
169
+
170
+ async hasConfig(tenantId: string, id: string): Promise<boolean> {
171
+ const row = this.db.prepare(
172
+ `SELECT 1 FROM lt_mcp_configs WHERE tenant_id = ? AND id = ? LIMIT 1`,
173
+ ).get(tenantId, id);
174
+ return row !== undefined;
175
+ }
176
+ }
177
+
178
+ function mapRowToEntry(row: ConfigRow): McpServerConfigEntry {
179
+ const config: McpServerConfig = JSON.parse(row.config);
180
+
181
+ if (config.env && row.is_env_encrypted) {
182
+ try {
183
+ const decryptedEnv: Record<string, string> = {};
184
+ for (const [key, value] of Object.entries(config.env)) {
185
+ decryptedEnv[key] = decrypt(value);
186
+ }
187
+ config.env = decryptedEnv;
188
+ } catch (error) {
189
+ console.error("Failed to decrypt MCP server env:", error);
190
+ throw new Error("Failed to decrypt MCP server configuration");
191
+ }
192
+ }
193
+
194
+ return {
195
+ id: row.id,
196
+ tenantId: row.tenant_id,
197
+ key: row.key,
198
+ config,
199
+ name: row.name || undefined,
200
+ description: row.description || undefined,
201
+ selectedTools: JSON.parse(row.selected_tools || "[]"),
202
+ isEnvEncrypted: row.is_env_encrypted === 1,
203
+ status: row.status as McpServerConfigEntry["status"],
204
+ createdAt: parseISO(row.created_at),
205
+ updatedAt: parseISO(row.updated_at),
206
+ };
207
+ }
208
+
209
+ function encryptEnvInConfig(config: McpServerConfig): { config: McpServerConfig; isEnvEncrypted: boolean } {
210
+ const configCopy = { ...config };
211
+ let isEnvEncrypted = false;
212
+
213
+ if (configCopy.env && Object.keys(configCopy.env).length > 0) {
214
+ const encryptedEnv: Record<string, string> = {};
215
+ for (const [key, value] of Object.entries(configCopy.env)) {
216
+ encryptedEnv[key] = encrypt(value);
217
+ }
218
+ configCopy.env = encryptedEnv;
219
+ isEnvEncrypted = true;
220
+ }
221
+
222
+ return { config: configCopy, isEnvEncrypted };
223
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Local SQLite implementation of MetricsServerConfigStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ MetricsServerConfigStore,
8
+ MetricsServerConfigEntry,
9
+ CreateMetricsServerConfigRequest,
10
+ UpdateMetricsServerConfigRequest,
11
+ } from "@axiom-lattice/protocols";
12
+ import { ensureTable, nowISO, parseISO } from "../database";
13
+
14
+ const DDL = `
15
+ CREATE TABLE IF NOT EXISTS lt_metrics_configs (
16
+ id TEXT NOT NULL,
17
+ tenant_id TEXT NOT NULL,
18
+ key TEXT NOT NULL,
19
+ name TEXT,
20
+ description TEXT,
21
+ config TEXT NOT NULL,
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL,
24
+ PRIMARY KEY (tenant_id, id)
25
+ );
26
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_lt_metrics_key ON lt_metrics_configs(tenant_id, key);
27
+ `;
28
+
29
+ interface ConfigRow {
30
+ id: string;
31
+ tenant_id: string;
32
+ key: string;
33
+ name: string | null;
34
+ description: string | null;
35
+ config: string;
36
+ created_at: string;
37
+ updated_at: string;
38
+ }
39
+
40
+ export class LocalMetricsServerConfigStore implements MetricsServerConfigStore {
41
+ private db: DatabaseWrapper;
42
+
43
+ constructor(db: DatabaseWrapper) {
44
+ this.db = db;
45
+ ensureTable(db, DDL);
46
+ }
47
+
48
+ async getAllConfigs(tenantId: string): Promise<MetricsServerConfigEntry[]> {
49
+ const rows = this.db.prepare(
50
+ `SELECT * FROM lt_metrics_configs WHERE tenant_id = ? ORDER BY created_at DESC`,
51
+ ).all(tenantId) as unknown as ConfigRow[];
52
+ return rows.map(mapRowToEntry);
53
+ }
54
+
55
+ async getAllConfigsWithoutTenant(): Promise<MetricsServerConfigEntry[]> {
56
+ const rows = this.db.prepare(
57
+ `SELECT * FROM lt_metrics_configs ORDER BY created_at DESC`,
58
+ ).all() as unknown as ConfigRow[];
59
+ return rows.map(mapRowToEntry);
60
+ }
61
+
62
+ async getConfigById(tenantId: string, id: string): Promise<MetricsServerConfigEntry | null> {
63
+ const row = this.db.prepare(
64
+ `SELECT * FROM lt_metrics_configs WHERE tenant_id = ? AND id = ?`,
65
+ ).get(tenantId, id) as unknown as ConfigRow | undefined;
66
+ return row ? mapRowToEntry(row) : null;
67
+ }
68
+
69
+ async getConfigByKey(tenantId: string, key: string): Promise<MetricsServerConfigEntry | null> {
70
+ const row = this.db.prepare(
71
+ `SELECT * FROM lt_metrics_configs WHERE tenant_id = ? AND key = ?`,
72
+ ).get(tenantId, key) as unknown as ConfigRow | undefined;
73
+ return row ? mapRowToEntry(row) : null;
74
+ }
75
+
76
+ async createConfig(
77
+ tenantId: string,
78
+ id: string,
79
+ data: CreateMetricsServerConfigRequest,
80
+ ): Promise<MetricsServerConfigEntry> {
81
+ const now = nowISO();
82
+
83
+ this.db.prepare(
84
+ `INSERT INTO lt_metrics_configs (id, tenant_id, key, name, description, config, created_at, updated_at)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
86
+ ON CONFLICT(tenant_id, id) DO UPDATE SET
87
+ key = excluded.key,
88
+ name = excluded.name,
89
+ description = excluded.description,
90
+ config = excluded.config,
91
+ updated_at = excluded.updated_at`,
92
+ ).run(id, tenantId, data.key, data.name || null, data.description || null, JSON.stringify(data.config), now, now);
93
+
94
+ return {
95
+ id,
96
+ tenantId,
97
+ key: data.key,
98
+ config: data.config,
99
+ name: data.name,
100
+ description: data.description,
101
+ createdAt: parseISO(now),
102
+ updatedAt: parseISO(now),
103
+ };
104
+ }
105
+
106
+ async updateConfig(
107
+ tenantId: string,
108
+ id: string,
109
+ updates: Partial<UpdateMetricsServerConfigRequest>,
110
+ ): Promise<MetricsServerConfigEntry | null> {
111
+ const existing = await this.getConfigById(tenantId, id);
112
+ if (!existing) return null;
113
+
114
+ const setClauses: string[] = [];
115
+ const values: unknown[] = [];
116
+
117
+ if (updates.key !== undefined) { setClauses.push("key = ?"); values.push(updates.key); }
118
+ if (updates.name !== undefined) { setClauses.push("name = ?"); values.push(updates.name || null); }
119
+ if (updates.description !== undefined) { setClauses.push("description = ?"); values.push(updates.description || null); }
120
+ if (updates.config !== undefined) { setClauses.push("config = ?"); values.push(JSON.stringify(updates.config)); }
121
+
122
+ if (setClauses.length === 0) return existing;
123
+
124
+ const now = nowISO();
125
+ setClauses.push("updated_at = ?");
126
+ values.push(now);
127
+ values.push(tenantId, id);
128
+
129
+ this.db.prepare(
130
+ `UPDATE lt_metrics_configs SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`,
131
+ ).run(...values);
132
+
133
+ return this.getConfigById(tenantId, id);
134
+ }
135
+
136
+ async deleteConfig(tenantId: string, id: string): Promise<boolean> {
137
+ const result = this.db.prepare(
138
+ `DELETE FROM lt_metrics_configs WHERE tenant_id = ? AND id = ?`,
139
+ ).run(tenantId, id);
140
+ return result.changes > 0;
141
+ }
142
+
143
+ async hasConfig(tenantId: string, id: string): Promise<boolean> {
144
+ const row = this.db.prepare(
145
+ `SELECT 1 FROM lt_metrics_configs WHERE tenant_id = ? AND id = ? LIMIT 1`,
146
+ ).get(tenantId, id);
147
+ return row !== undefined;
148
+ }
149
+ }
150
+
151
+ function mapRowToEntry(row: ConfigRow): MetricsServerConfigEntry {
152
+ return {
153
+ id: row.id,
154
+ tenantId: row.tenant_id,
155
+ key: row.key,
156
+ config: JSON.parse(row.config),
157
+ name: row.name || undefined,
158
+ description: row.description || undefined,
159
+ createdAt: parseISO(row.created_at),
160
+ updatedAt: parseISO(row.updated_at),
161
+ };
162
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Local SQLite implementation of ProjectStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ ProjectStore,
8
+ Project,
9
+ CreateProjectRequest,
10
+ UpdateProjectRequest,
11
+ } from "@axiom-lattice/protocols";
12
+ import { ensureTable, nowISO, parseISO } from "../database";
13
+
14
+ const DDL = `
15
+ CREATE TABLE IF NOT EXISTS lt_projects (
16
+ id TEXT NOT NULL,
17
+ tenant_id TEXT NOT NULL,
18
+ workspace_id TEXT NOT NULL,
19
+ name TEXT NOT NULL,
20
+ description TEXT,
21
+ config TEXT,
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL,
24
+ PRIMARY KEY (tenant_id, id)
25
+ );
26
+ CREATE INDEX IF NOT EXISTS idx_lt_projects_workspace ON lt_projects(tenant_id, workspace_id);
27
+ `;
28
+
29
+ interface ProjectRow {
30
+ id: string;
31
+ tenant_id: string;
32
+ workspace_id: string;
33
+ name: string;
34
+ description: string | null;
35
+ config: string | null;
36
+ created_at: string;
37
+ updated_at: string;
38
+ }
39
+
40
+ export class LocalProjectStore implements ProjectStore {
41
+ private db: DatabaseWrapper;
42
+
43
+ constructor(db: DatabaseWrapper) {
44
+ this.db = db;
45
+ ensureTable(db, DDL);
46
+ }
47
+
48
+ async getProjectsByWorkspace(tenantId: string, workspaceId: string): Promise<Project[]> {
49
+ const rows = this.db.prepare(
50
+ `SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? ORDER BY created_at DESC`,
51
+ ).all(tenantId, workspaceId) as unknown as ProjectRow[];
52
+ return rows.map(mapRowToProject);
53
+ }
54
+
55
+ async getProjectById(tenantId: string, id: string): Promise<Project | null> {
56
+ const row = this.db.prepare(
57
+ `SELECT * FROM lt_projects WHERE tenant_id = ? AND id = ?`,
58
+ ).get(tenantId, id) as unknown as ProjectRow | undefined;
59
+ return row ? mapRowToProject(row) : null;
60
+ }
61
+
62
+ async createProject(
63
+ tenantId: string,
64
+ workspaceId: string,
65
+ id: string,
66
+ data: CreateProjectRequest,
67
+ ): Promise<Project> {
68
+ const now = nowISO();
69
+
70
+ this.db.prepare(
71
+ `INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
73
+ ON CONFLICT(tenant_id, id) DO UPDATE SET
74
+ workspace_id = excluded.workspace_id,
75
+ name = excluded.name,
76
+ description = excluded.description,
77
+ config = excluded.config,
78
+ updated_at = excluded.updated_at`,
79
+ ).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, now, now);
80
+
81
+ return {
82
+ id,
83
+ tenantId,
84
+ workspaceId,
85
+ name: data.name,
86
+ description: data.description,
87
+ config: data.config,
88
+ createdAt: parseISO(now),
89
+ updatedAt: parseISO(now),
90
+ };
91
+ }
92
+
93
+ async updateProject(
94
+ tenantId: string,
95
+ id: string,
96
+ updates: UpdateProjectRequest,
97
+ ): Promise<Project | null> {
98
+ const existing = await this.getProjectById(tenantId, id);
99
+ if (!existing) return null;
100
+
101
+ const setClauses: string[] = [];
102
+ const values: unknown[] = [];
103
+
104
+ if (updates.name !== undefined) {
105
+ setClauses.push("name = ?");
106
+ values.push(updates.name);
107
+ }
108
+ if (updates.description !== undefined) {
109
+ setClauses.push("description = ?");
110
+ values.push(updates.description || null);
111
+ }
112
+ if (updates.config !== undefined) {
113
+ setClauses.push("config = ?");
114
+ values.push(updates.config ? JSON.stringify(updates.config) : null);
115
+ }
116
+
117
+ if (setClauses.length === 0) return existing;
118
+
119
+ const now = nowISO();
120
+ setClauses.push("updated_at = ?");
121
+ values.push(now);
122
+ values.push(tenantId, id);
123
+
124
+ this.db.prepare(
125
+ `UPDATE lt_projects SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`,
126
+ ).run(...values);
127
+
128
+ return this.getProjectById(tenantId, id);
129
+ }
130
+
131
+ async deleteProject(tenantId: string, id: string): Promise<boolean> {
132
+ const result = this.db.prepare(
133
+ `DELETE FROM lt_projects WHERE tenant_id = ? AND id = ?`,
134
+ ).run(tenantId, id);
135
+ return result.changes > 0;
136
+ }
137
+ }
138
+
139
+ function mapRowToProject(row: ProjectRow): Project {
140
+ return {
141
+ id: row.id,
142
+ tenantId: row.tenant_id,
143
+ workspaceId: row.workspace_id,
144
+ name: row.name,
145
+ description: row.description || undefined,
146
+ config: row.config ? JSON.parse(row.config) as Record<string, unknown> : undefined,
147
+ createdAt: parseISO(row.created_at),
148
+ updatedAt: parseISO(row.updated_at),
149
+ };
150
+ }