@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,204 @@
1
+ /**
2
+ * Local SQLite implementation of ChannelBindingStore (BindingRegistry).
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ Binding,
8
+ BindingRegistry,
9
+ CreateBindingInput,
10
+ } from "@axiom-lattice/protocols";
11
+ import { ensureTable, nowISO, parseISO } from "../database";
12
+ import { randomUUID } from "crypto";
13
+
14
+ const DDL = `
15
+ CREATE TABLE IF NOT EXISTS lt_channel_bindings (
16
+ id TEXT PRIMARY KEY,
17
+ channel TEXT NOT NULL,
18
+ channel_installation_id TEXT NOT NULL,
19
+ tenant_id TEXT NOT NULL,
20
+ sender_id TEXT NOT NULL,
21
+ agent_id TEXT NOT NULL,
22
+ thread_id TEXT,
23
+ workspace_id TEXT,
24
+ project_id TEXT,
25
+ thread_mode TEXT NOT NULL DEFAULT 'fixed',
26
+ sender_display_name TEXT,
27
+ sender_metadata TEXT,
28
+ enabled INTEGER NOT NULL DEFAULT 1,
29
+ created_at TEXT NOT NULL,
30
+ updated_at TEXT NOT NULL
31
+ );
32
+ CREATE INDEX IF NOT EXISTS idx_lt_cb_resolve ON lt_channel_bindings(channel, sender_id, channel_installation_id, tenant_id);
33
+ `;
34
+
35
+ interface BindingRow {
36
+ id: string;
37
+ channel: string;
38
+ channel_installation_id: string;
39
+ tenant_id: string;
40
+ sender_id: string;
41
+ agent_id: string;
42
+ thread_id: string | null;
43
+ workspace_id: string | null;
44
+ project_id: string | null;
45
+ thread_mode: string;
46
+ sender_display_name: string | null;
47
+ sender_metadata: string | null;
48
+ enabled: number;
49
+ created_at: string;
50
+ updated_at: string;
51
+ }
52
+
53
+ export class LocalChannelBindingStore implements BindingRegistry {
54
+ private db: DatabaseWrapper;
55
+
56
+ constructor(db: DatabaseWrapper) {
57
+ this.db = db;
58
+ ensureTable(db, DDL);
59
+ }
60
+
61
+ async resolve(params: {
62
+ channel: string;
63
+ senderId: string;
64
+ channelInstallationId: string;
65
+ tenantId: string;
66
+ }): Promise<Binding | null> {
67
+ const row = this.db.prepare(
68
+ `SELECT * FROM lt_channel_bindings
69
+ WHERE channel = ? AND sender_id = ? AND channel_installation_id = ? AND tenant_id = ? AND enabled = 1
70
+ LIMIT 1`,
71
+ ).get(params.channel, params.senderId, params.channelInstallationId, params.tenantId) as unknown as BindingRow | undefined;
72
+ return row ? mapRowToBinding(row) : null;
73
+ }
74
+
75
+ async create(input: CreateBindingInput): Promise<Binding> {
76
+ const id = randomUUID();
77
+ const now = nowISO();
78
+
79
+ this.db.prepare(
80
+ `INSERT INTO lt_channel_bindings
81
+ (id, channel, channel_installation_id, tenant_id, sender_id, agent_id,
82
+ thread_mode, sender_display_name, sender_metadata, workspace_id, project_id, created_at, updated_at)
83
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
84
+ ).run(
85
+ id, input.channel, input.channelInstallationId, input.tenantId,
86
+ input.senderId, input.agentId, input.threadMode || "fixed",
87
+ input.senderDisplayName || null,
88
+ input.senderMetadata ? JSON.stringify(input.senderMetadata) : null,
89
+ input.workspaceId || null, input.projectId || null,
90
+ now, now,
91
+ );
92
+
93
+ return (await this.getById(id))!;
94
+ }
95
+
96
+ async update(id: string, patch: Partial<Binding>): Promise<Binding> {
97
+ const existing = await this.getById(id);
98
+ if (!existing) throw new Error(`Binding ${id} not found`);
99
+
100
+ const updated = {
101
+ channel: patch.channel ?? existing.channel,
102
+ channelInstallationId: patch.channelInstallationId ?? existing.channelInstallationId,
103
+ senderId: patch.senderId ?? existing.senderId,
104
+ agentId: patch.agentId ?? existing.agentId,
105
+ threadId: patch.threadId !== undefined ? patch.threadId : existing.threadId,
106
+ workspaceId: patch.workspaceId !== undefined ? patch.workspaceId : existing.workspaceId,
107
+ projectId: patch.projectId !== undefined ? patch.projectId : existing.projectId,
108
+ threadMode: patch.threadMode ?? existing.threadMode,
109
+ senderDisplayName: patch.senderDisplayName !== undefined ? patch.senderDisplayName : existing.senderDisplayName,
110
+ senderMetadata: patch.senderMetadata !== undefined ? patch.senderMetadata : existing.senderMetadata,
111
+ enabled: patch.enabled ?? existing.enabled,
112
+ };
113
+
114
+ const now = nowISO();
115
+
116
+ this.db.prepare(
117
+ `UPDATE lt_channel_bindings SET
118
+ channel = ?, channel_installation_id = ?, sender_id = ?, agent_id = ?,
119
+ thread_id = ?, workspace_id = ?, project_id = ?, thread_mode = ?,
120
+ sender_display_name = ?, sender_metadata = ?, enabled = ?, updated_at = ?
121
+ WHERE id = ?`,
122
+ ).run(
123
+ updated.channel, updated.channelInstallationId, updated.senderId,
124
+ updated.agentId, updated.threadId || null, updated.workspaceId || null,
125
+ updated.projectId || null, updated.threadMode, updated.senderDisplayName || null,
126
+ updated.senderMetadata ? JSON.stringify(updated.senderMetadata) : null,
127
+ updated.enabled ? 1 : 0, now, id,
128
+ );
129
+
130
+ return (await this.getById(id))!;
131
+ }
132
+
133
+ async delete(id: string): Promise<void> {
134
+ this.db.prepare(`DELETE FROM lt_channel_bindings WHERE id = ?`).run(id);
135
+ }
136
+
137
+ async list(params: {
138
+ channel?: string;
139
+ agentId?: string;
140
+ tenantId: string;
141
+ channelInstallationId?: string;
142
+ limit?: number;
143
+ offset?: number;
144
+ }): Promise<Binding[]> {
145
+ const conditions: string[] = ["tenant_id = ?"];
146
+ const values: unknown[] = [params.tenantId];
147
+
148
+ if (params.channel) { conditions.push("channel = ?"); values.push(params.channel); }
149
+ if (params.agentId) { conditions.push("agent_id = ?"); values.push(params.agentId); }
150
+ if (params.channelInstallationId) { conditions.push("channel_installation_id = ?"); values.push(params.channelInstallationId); }
151
+
152
+ const limit = params.limit ?? 50;
153
+ const offset = params.offset ?? 0;
154
+ values.push(limit, offset);
155
+
156
+ const rows = this.db.prepare(
157
+ `SELECT * FROM lt_channel_bindings
158
+ WHERE ${conditions.join(" AND ")}
159
+ ORDER BY created_at DESC
160
+ LIMIT ? OFFSET ?`,
161
+ ).all(...values) as unknown as BindingRow[];
162
+
163
+ return rows.map(mapRowToBinding);
164
+ }
165
+
166
+ async import(bindings: CreateBindingInput[]): Promise<Binding[]> {
167
+ const result: Binding[] = [];
168
+ for (const input of bindings) {
169
+ result.push(await this.create(input));
170
+ }
171
+ return result;
172
+ }
173
+
174
+ async export(params: { tenantId: string }): Promise<Binding[]> {
175
+ return this.list({ tenantId: params.tenantId, limit: 10000, offset: 0 });
176
+ }
177
+
178
+ private async getById(id: string): Promise<Binding | null> {
179
+ const row = this.db.prepare(
180
+ `SELECT * FROM lt_channel_bindings WHERE id = ?`,
181
+ ).get(id) as unknown as BindingRow | undefined;
182
+ return row ? mapRowToBinding(row) : null;
183
+ }
184
+ }
185
+
186
+ function mapRowToBinding(row: BindingRow): Binding {
187
+ return {
188
+ id: row.id,
189
+ channel: row.channel,
190
+ channelInstallationId: row.channel_installation_id,
191
+ tenantId: row.tenant_id,
192
+ senderId: row.sender_id,
193
+ agentId: row.agent_id,
194
+ threadId: row.thread_id || undefined,
195
+ workspaceId: row.workspace_id || undefined,
196
+ projectId: row.project_id || undefined,
197
+ threadMode: row.thread_mode as Binding["threadMode"],
198
+ senderDisplayName: row.sender_display_name || undefined,
199
+ senderMetadata: row.sender_metadata ? JSON.parse(row.sender_metadata) : undefined,
200
+ enabled: row.enabled === 1,
201
+ createdAt: parseISO(row.created_at),
202
+ updatedAt: parseISO(row.updated_at),
203
+ };
204
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Local SQLite implementation of ChannelInstallationStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ ChannelInstallationStore,
8
+ ChannelInstallation,
9
+ ChannelInstallationType,
10
+ CreateChannelInstallationRequest,
11
+ UpdateChannelInstallationRequest,
12
+ } from "@axiom-lattice/protocols";
13
+ import { ensureTable, nowISO, parseISO } from "../database";
14
+
15
+ const DDL = `
16
+ CREATE TABLE IF NOT EXISTS lt_channel_installations (
17
+ id TEXT PRIMARY KEY,
18
+ tenant_id TEXT NOT NULL,
19
+ channel TEXT NOT NULL,
20
+ name TEXT,
21
+ config TEXT NOT NULL,
22
+ enabled INTEGER NOT NULL DEFAULT 1,
23
+ fallback_agent_id TEXT,
24
+ reject_when_no_binding INTEGER NOT NULL DEFAULT 0,
25
+ created_at TEXT NOT NULL,
26
+ updated_at TEXT NOT NULL
27
+ );
28
+ CREATE INDEX IF NOT EXISTS idx_lt_ci_tenant ON lt_channel_installations(tenant_id);
29
+ `;
30
+
31
+ interface Row {
32
+ id: string;
33
+ tenant_id: string;
34
+ channel: string;
35
+ name: string | null;
36
+ config: string;
37
+ enabled: number;
38
+ fallback_agent_id: string | null;
39
+ reject_when_no_binding: number;
40
+ created_at: string;
41
+ updated_at: string;
42
+ }
43
+
44
+ export class LocalChannelInstallationStore implements ChannelInstallationStore {
45
+ private db: DatabaseWrapper;
46
+
47
+ constructor(db: DatabaseWrapper) {
48
+ this.db = db;
49
+ ensureTable(db, DDL);
50
+ }
51
+
52
+ async getInstallationById(installationId: string): Promise<ChannelInstallation | null> {
53
+ const row = this.db.prepare(
54
+ `SELECT * FROM lt_channel_installations WHERE id = ?`,
55
+ ).get(installationId) as Row | undefined;
56
+ return row ? mapRow(row) : null;
57
+ }
58
+
59
+ async getInstallationsByTenant(
60
+ tenantId: string,
61
+ channel?: ChannelInstallationType,
62
+ ): Promise<ChannelInstallation[]> {
63
+ let rows: Row[];
64
+ if (channel) {
65
+ rows = this.db.prepare(
66
+ `SELECT * FROM lt_channel_installations WHERE tenant_id = ? AND channel = ? ORDER BY created_at DESC`,
67
+ ).all(tenantId, channel) as Row[];
68
+ } else {
69
+ rows = this.db.prepare(
70
+ `SELECT * FROM lt_channel_installations WHERE tenant_id = ? ORDER BY created_at DESC`,
71
+ ).all(tenantId) as Row[];
72
+ }
73
+ return rows.map(mapRow);
74
+ }
75
+
76
+ async createInstallation(
77
+ tenantId: string,
78
+ installationId: string,
79
+ data: CreateChannelInstallationRequest,
80
+ ): Promise<ChannelInstallation> {
81
+ const now = nowISO();
82
+
83
+ this.db.prepare(
84
+ `INSERT INTO lt_channel_installations
85
+ (id, tenant_id, channel, name, config, enabled, fallback_agent_id, reject_when_no_binding, created_at, updated_at)
86
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
87
+ ON CONFLICT(id) DO UPDATE SET
88
+ channel = excluded.channel,
89
+ name = excluded.name,
90
+ config = excluded.config,
91
+ enabled = excluded.enabled,
92
+ fallback_agent_id = excluded.fallback_agent_id,
93
+ reject_when_no_binding = excluded.reject_when_no_binding,
94
+ updated_at = excluded.updated_at`,
95
+ ).run(
96
+ installationId, tenantId, data.channel,
97
+ data.name || null, JSON.stringify(data.config),
98
+ data.enabled !== false ? 1 : 0,
99
+ data.fallbackAgentId || null,
100
+ data.rejectWhenNoBinding ? 1 : 0,
101
+ now, now,
102
+ );
103
+
104
+ return {
105
+ id: installationId,
106
+ tenantId,
107
+ channel: data.channel,
108
+ name: data.name,
109
+ config: data.config,
110
+ enabled: data.enabled !== false,
111
+ fallbackAgentId: data.fallbackAgentId,
112
+ rejectWhenNoBinding: data.rejectWhenNoBinding ?? false,
113
+ createdAt: parseISO(now),
114
+ updatedAt: parseISO(now),
115
+ };
116
+ }
117
+
118
+ async updateInstallation(
119
+ tenantId: string,
120
+ installationId: string,
121
+ updates: UpdateChannelInstallationRequest,
122
+ ): Promise<ChannelInstallation | null> {
123
+ const existing = await this.getInstallationById(installationId);
124
+ if (!existing) return null;
125
+
126
+ const setClauses: string[] = [];
127
+ const values: unknown[] = [];
128
+
129
+ if (updates.name !== undefined) { setClauses.push("name = ?"); values.push(updates.name || null); }
130
+ if (updates.config !== undefined) { setClauses.push("config = ?"); values.push(JSON.stringify(updates.config)); }
131
+ if (updates.enabled !== undefined) { setClauses.push("enabled = ?"); values.push(updates.enabled ? 1 : 0); }
132
+ if (updates.fallbackAgentId !== undefined) { setClauses.push("fallback_agent_id = ?"); values.push(updates.fallbackAgentId || null); }
133
+ if (updates.rejectWhenNoBinding !== undefined) { setClauses.push("reject_when_no_binding = ?"); values.push(updates.rejectWhenNoBinding ? 1 : 0); }
134
+
135
+ if (setClauses.length === 0) return existing;
136
+
137
+ const now = nowISO();
138
+ setClauses.push("updated_at = ?");
139
+ values.push(now);
140
+ values.push(installationId);
141
+
142
+ this.db.prepare(
143
+ `UPDATE lt_channel_installations SET ${setClauses.join(", ")} WHERE id = ?`,
144
+ ).run(...values);
145
+
146
+ return this.getInstallationById(installationId);
147
+ }
148
+
149
+ async deleteInstallation(tenantId: string, installationId: string): Promise<boolean> {
150
+ const result = this.db.prepare(
151
+ `DELETE FROM lt_channel_installations WHERE id = ? AND tenant_id = ?`,
152
+ ).run(installationId, tenantId);
153
+ return result.changes > 0;
154
+ }
155
+ }
156
+
157
+ function mapRow(row: Row): ChannelInstallation {
158
+ return {
159
+ id: row.id,
160
+ tenantId: row.tenant_id,
161
+ channel: row.channel as ChannelInstallationType,
162
+ name: row.name || undefined,
163
+ config: JSON.parse(row.config),
164
+ enabled: row.enabled === 1,
165
+ fallbackAgentId: row.fallback_agent_id || undefined,
166
+ rejectWhenNoBinding: row.reject_when_no_binding === 1,
167
+ createdAt: parseISO(row.created_at),
168
+ updatedAt: parseISO(row.updated_at),
169
+ };
170
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Local SQLite implementation of DatabaseConfigStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ DatabaseConfigStore,
8
+ DatabaseConfigEntry,
9
+ CreateDatabaseConfigRequest,
10
+ UpdateDatabaseConfigRequest,
11
+ DatabaseConfig,
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_database_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
+ created_at TEXT NOT NULL,
25
+ updated_at TEXT NOT NULL,
26
+ PRIMARY KEY (tenant_id, id)
27
+ );
28
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_lt_dbconfig_key ON lt_database_configs(tenant_id, key);
29
+ `;
30
+
31
+ interface ConfigRow {
32
+ id: string;
33
+ tenant_id: string;
34
+ key: string;
35
+ name: string | null;
36
+ description: string | null;
37
+ config: string;
38
+ created_at: string;
39
+ updated_at: string;
40
+ }
41
+
42
+ export class LocalDatabaseConfigStore implements DatabaseConfigStore {
43
+ private db: DatabaseWrapper;
44
+
45
+ constructor(db: DatabaseWrapper) {
46
+ this.db = db;
47
+ ensureTable(db, DDL);
48
+ }
49
+
50
+ async getAllConfigs(tenantId: string): Promise<DatabaseConfigEntry[]> {
51
+ const rows = this.db.prepare(
52
+ `SELECT * FROM lt_database_configs WHERE tenant_id = ? ORDER BY created_at DESC`,
53
+ ).all(tenantId) as unknown as ConfigRow[];
54
+ return rows.map((r) => mapRowToEntry(r));
55
+ }
56
+
57
+ async getAllConfigsWithoutTenant(): Promise<DatabaseConfigEntry[]> {
58
+ const rows = this.db.prepare(
59
+ `SELECT * FROM lt_database_configs ORDER BY created_at DESC`,
60
+ ).all() as unknown as ConfigRow[];
61
+ return rows.map((r) => mapRowToEntry(r));
62
+ }
63
+
64
+ async getConfigById(tenantId: string, id: string): Promise<DatabaseConfigEntry | null> {
65
+ const row = this.db.prepare(
66
+ `SELECT * FROM lt_database_configs WHERE tenant_id = ? AND id = ?`,
67
+ ).get(tenantId, id) as unknown as ConfigRow | undefined;
68
+ return row ? mapRowToEntry(row) : null;
69
+ }
70
+
71
+ async getConfigByKey(tenantId: string, key: string): Promise<DatabaseConfigEntry | null> {
72
+ const row = this.db.prepare(
73
+ `SELECT * FROM lt_database_configs WHERE tenant_id = ? AND key = ?`,
74
+ ).get(tenantId, key) as unknown as ConfigRow | undefined;
75
+ return row ? mapRowToEntry(row) : null;
76
+ }
77
+
78
+ async createConfig(
79
+ tenantId: string,
80
+ id: string,
81
+ data: CreateDatabaseConfigRequest,
82
+ ): Promise<DatabaseConfigEntry> {
83
+ const now = nowISO();
84
+ const configWithEncrypted = encryptPasswordInConfig(data.config);
85
+
86
+ this.db.prepare(
87
+ `INSERT INTO lt_database_configs (id, tenant_id, key, name, description, config, created_at, updated_at)
88
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
89
+ ON CONFLICT(tenant_id, id) DO UPDATE SET
90
+ key = excluded.key,
91
+ name = excluded.name,
92
+ description = excluded.description,
93
+ config = excluded.config,
94
+ updated_at = excluded.updated_at`,
95
+ ).run(id, tenantId, data.key, data.name || null, data.description || null, JSON.stringify(configWithEncrypted), now, now);
96
+
97
+ return {
98
+ id,
99
+ tenantId,
100
+ key: data.key,
101
+ config: data.config,
102
+ name: data.name,
103
+ description: data.description,
104
+ createdAt: parseISO(now),
105
+ updatedAt: parseISO(now),
106
+ };
107
+ }
108
+
109
+ async updateConfig(
110
+ tenantId: string,
111
+ id: string,
112
+ updates: Partial<UpdateDatabaseConfigRequest>,
113
+ ): Promise<DatabaseConfigEntry | null> {
114
+ const existing = await this.getConfigById(tenantId, id);
115
+ if (!existing) return null;
116
+
117
+ const setClauses: string[] = [];
118
+ const values: unknown[] = [];
119
+
120
+ if (updates.key !== undefined) { setClauses.push("key = ?"); values.push(updates.key); }
121
+ if (updates.name !== undefined) { setClauses.push("name = ?"); values.push(updates.name || null); }
122
+ if (updates.description !== undefined) { setClauses.push("description = ?"); values.push(updates.description || null); }
123
+ if (updates.config !== undefined) {
124
+ setClauses.push("config = ?");
125
+ values.push(JSON.stringify(encryptPasswordInConfig(updates.config)));
126
+ }
127
+
128
+ if (setClauses.length === 0) return existing;
129
+
130
+ const now = nowISO();
131
+ setClauses.push("updated_at = ?");
132
+ values.push(now);
133
+ values.push(tenantId, id);
134
+
135
+ this.db.prepare(
136
+ `UPDATE lt_database_configs SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`,
137
+ ).run(...values);
138
+
139
+ return this.getConfigById(tenantId, id);
140
+ }
141
+
142
+ async deleteConfig(tenantId: string, id: string): Promise<boolean> {
143
+ const result = this.db.prepare(
144
+ `DELETE FROM lt_database_configs WHERE tenant_id = ? AND id = ?`,
145
+ ).run(tenantId, id);
146
+ return result.changes > 0;
147
+ }
148
+
149
+ async hasConfig(tenantId: string, id: string): Promise<boolean> {
150
+ const row = this.db.prepare(
151
+ `SELECT 1 FROM lt_database_configs WHERE tenant_id = ? AND id = ? LIMIT 1`,
152
+ ).get(tenantId, id);
153
+ return row !== undefined;
154
+ }
155
+ }
156
+
157
+ function mapRowToEntry(row: ConfigRow): DatabaseConfigEntry {
158
+ const config = JSON.parse(row.config) as DatabaseConfig;
159
+ if (config.password) {
160
+ try {
161
+ config.password = decrypt(config.password);
162
+ } catch {
163
+ // password might not be encrypted (legacy data)
164
+ }
165
+ }
166
+ return {
167
+ id: row.id,
168
+ tenantId: row.tenant_id,
169
+ key: row.key,
170
+ config,
171
+ name: row.name || undefined,
172
+ description: row.description || undefined,
173
+ createdAt: parseISO(row.created_at),
174
+ updatedAt: parseISO(row.updated_at),
175
+ };
176
+ }
177
+
178
+ function encryptPasswordInConfig(config: DatabaseConfig): DatabaseConfig {
179
+ if (config.password) {
180
+ return { ...config, password: encrypt(config.password) };
181
+ }
182
+ return config;
183
+ }