@axiom-lattice/pg-stores 3.3.5 → 3.3.6
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 +7 -0
- package/dist/index.d.mts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +153 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +160 -78
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/scripts/verify-model-provider-migrations.ts +214 -0
- package/scripts/verify-store.ts +232 -0
- package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +41 -4
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +48 -4
- package/src/__tests__/PostgreSQLConnectionStore.integration.test.ts +60 -1
- package/src/__tests__/PostgreSQLDatabaseConfigStore.test.ts +53 -0
- package/src/__tests__/PostgreSQLMcpServerConfigStore.test.ts +43 -0
- package/src/__tests__/PostgreSQLMetricsServerConfigStore.test.ts +59 -0
- package/src/__tests__/PostgreSQLModelProviderStore.test.ts +58 -0
- package/src/createPgStoreConfig.ts +6 -1
- package/src/migrations/connection_config_migrations.ts +56 -0
- package/src/migrations/model_provider_migrations.ts +74 -0
- package/src/stores/PostgreSQLA2AApiKeyStore.ts +4 -17
- package/src/stores/PostgreSQLChannelInstallationStore.ts +4 -4
- package/src/stores/PostgreSQLDatabaseConfigStore.ts +2 -7
- package/src/stores/PostgreSQLMcpServerConfigStore.ts +9 -19
- package/src/stores/PostgreSQLMetricsServerConfigStore.ts +3 -13
- package/src/stores/PostgreSQLModelProviderStore.ts +128 -25
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, 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 { clearEncryptionKeyCache, encrypt } from "@axiom-lattice/core";
|
|
11
|
+
import { PostgreSQLDatabaseConfigStore } from "../stores/PostgreSQLDatabaseConfigStore";
|
|
12
|
+
|
|
13
|
+
describe("PostgreSQLDatabaseConfigStore secret handling", () => {
|
|
14
|
+
const originalKey = process.env.LATTICE_ENCRYPTION_KEY;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (originalKey === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
18
|
+
else process.env.LATTICE_ENCRYPTION_KEY = originalKey;
|
|
19
|
+
clearEncryptionKeyCache();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("empties an undecryptable password but still lists the config", async () => {
|
|
23
|
+
process.env.LATTICE_ENCRYPTION_KEY = "key-one";
|
|
24
|
+
clearEncryptionKeyCache();
|
|
25
|
+
const encrypted = encrypt("db-password");
|
|
26
|
+
|
|
27
|
+
process.env.LATTICE_ENCRYPTION_KEY = "key-two";
|
|
28
|
+
clearEncryptionKeyCache();
|
|
29
|
+
|
|
30
|
+
const query = jest.fn(async () => ({
|
|
31
|
+
rows: [{
|
|
32
|
+
id: "cfg-1",
|
|
33
|
+
tenant_id: "tenant-1",
|
|
34
|
+
key: "warehouse",
|
|
35
|
+
name: "Warehouse",
|
|
36
|
+
description: null,
|
|
37
|
+
config: JSON.stringify({
|
|
38
|
+
host: "db.example.com",
|
|
39
|
+
database: "analytics",
|
|
40
|
+
password: encrypted,
|
|
41
|
+
}),
|
|
42
|
+
created_at: new Date(),
|
|
43
|
+
updated_at: new Date(),
|
|
44
|
+
}],
|
|
45
|
+
}));
|
|
46
|
+
const store = new PostgreSQLDatabaseConfigStore({ pool: { query } as unknown as Pool });
|
|
47
|
+
|
|
48
|
+
const listed = await store.getAllConfigs("tenant-1");
|
|
49
|
+
expect(listed).toHaveLength(1);
|
|
50
|
+
expect(listed[0].config.password).toBe("");
|
|
51
|
+
expect(listed[0].config.host).toBe("db.example.com");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -7,6 +7,7 @@ jest.mock("@axiom-lattice/core", () => ({
|
|
|
7
7
|
...jest.requireActual("../../../core/src/util/encryption"),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
|
+
import { clearEncryptionKeyCache } from "@axiom-lattice/core";
|
|
10
11
|
import { PostgreSQLMcpServerConfigStore } from "../stores/PostgreSQLMcpServerConfigStore";
|
|
11
12
|
|
|
12
13
|
const makeRow = (configJson: string, isEnvEncrypted: boolean) => ({
|
|
@@ -60,6 +61,48 @@ describe("PostgreSQLMcpServerConfigStore header encryption", () => {
|
|
|
60
61
|
expect(read?.config.headers?.Authorization).not.toBe(atRest.headers.Authorization);
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
it("empties undecryptable secrets but still returns the config", async () => {
|
|
65
|
+
const previous = process.env.LATTICE_ENCRYPTION_KEY;
|
|
66
|
+
try {
|
|
67
|
+
let storedConfig = "";
|
|
68
|
+
let storedFlag = false;
|
|
69
|
+
const query = jest.fn(async (sql: string, values: unknown[]) => {
|
|
70
|
+
if (sql.includes("INSERT INTO lattice_mcp_server_configs")) {
|
|
71
|
+
storedConfig = values[5] as string;
|
|
72
|
+
storedFlag = values[7] as boolean;
|
|
73
|
+
return { rowCount: 1 };
|
|
74
|
+
}
|
|
75
|
+
if (sql.includes("SELECT id, tenant_id, key")) {
|
|
76
|
+
return { rows: [makeRow(storedConfig, storedFlag)] };
|
|
77
|
+
}
|
|
78
|
+
return { rows: [] };
|
|
79
|
+
});
|
|
80
|
+
const store = new PostgreSQLMcpServerConfigStore({ pool: { query } as unknown as Pool });
|
|
81
|
+
|
|
82
|
+
process.env.LATTICE_ENCRYPTION_KEY = "test-key-one";
|
|
83
|
+
clearEncryptionKeyCache();
|
|
84
|
+
await store.createConfig("tenant-1", "cfg-1", {
|
|
85
|
+
key: "remote",
|
|
86
|
+
config: {
|
|
87
|
+
name: "remote", version: "1.0.0", transport: "streamable_http",
|
|
88
|
+
url: "https://mcp.example.com/mcp",
|
|
89
|
+
headers: { Authorization: "Bearer sk-secret-token" },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.env.LATTICE_ENCRYPTION_KEY = "test-key-two";
|
|
94
|
+
clearEncryptionKeyCache();
|
|
95
|
+
|
|
96
|
+
const read = await store.getConfigByKey("tenant-1", "remote");
|
|
97
|
+
expect(read?.config.headers?.Authorization).toBe("");
|
|
98
|
+
expect(read?.config.url).toBe("https://mcp.example.com/mcp");
|
|
99
|
+
} finally {
|
|
100
|
+
if (previous === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
101
|
+
else process.env.LATTICE_ENCRYPTION_KEY = previous;
|
|
102
|
+
clearEncryptionKeyCache();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
63
106
|
it("leaves legacy rows without headers untouched", async () => {
|
|
64
107
|
const legacyConfig = JSON.stringify({
|
|
65
108
|
name: "legacy", version: "1.0.0", transport: "stdio", command: "npx",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { afterEach, 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 { clearEncryptionKeyCache, encrypt } from "@axiom-lattice/core";
|
|
11
|
+
import { PostgreSQLMetricsServerConfigStore } from "../stores/PostgreSQLMetricsServerConfigStore";
|
|
12
|
+
|
|
13
|
+
const row = {
|
|
14
|
+
id: "cfg-1",
|
|
15
|
+
tenant_id: "tenant-1",
|
|
16
|
+
key: "metrics",
|
|
17
|
+
name: "Metrics",
|
|
18
|
+
description: null,
|
|
19
|
+
config: "",
|
|
20
|
+
created_at: new Date(),
|
|
21
|
+
updated_at: new Date(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("PostgreSQLMetricsServerConfigStore secret handling", () => {
|
|
25
|
+
const originalKey = process.env.LATTICE_ENCRYPTION_KEY;
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (originalKey === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
29
|
+
else process.env.LATTICE_ENCRYPTION_KEY = originalKey;
|
|
30
|
+
clearEncryptionKeyCache();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("empties an undecryptable secret but still lists the config", async () => {
|
|
34
|
+
process.env.LATTICE_ENCRYPTION_KEY = "key-one";
|
|
35
|
+
clearEncryptionKeyCache();
|
|
36
|
+
const encrypted = encrypt("sk-secret");
|
|
37
|
+
|
|
38
|
+
process.env.LATTICE_ENCRYPTION_KEY = "key-two";
|
|
39
|
+
clearEncryptionKeyCache();
|
|
40
|
+
|
|
41
|
+
const query = jest.fn(async () => ({
|
|
42
|
+
rows: [{
|
|
43
|
+
...row,
|
|
44
|
+
config: JSON.stringify({
|
|
45
|
+
baseUrl: "https://metrics.example/api/v1",
|
|
46
|
+
apiKey: encrypted,
|
|
47
|
+
password: "plaintext-legacy",
|
|
48
|
+
}),
|
|
49
|
+
}],
|
|
50
|
+
}));
|
|
51
|
+
const store = new PostgreSQLMetricsServerConfigStore({ pool: { query } as unknown as Pool });
|
|
52
|
+
|
|
53
|
+
const listed = await store.getAllConfigs("tenant-1");
|
|
54
|
+
expect(listed).toHaveLength(1);
|
|
55
|
+
expect(listed[0].config.apiKey).toBe("");
|
|
56
|
+
expect(listed[0].config.password).toBe("");
|
|
57
|
+
expect(listed[0].config.baseUrl).toBe("https://metrics.example/api/v1");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterEach, 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 { clearEncryptionKeyCache, encrypt } from "@axiom-lattice/core";
|
|
11
|
+
import { PostgreSQLModelProviderStore } from "../stores/PostgreSQLModelProviderStore";
|
|
12
|
+
|
|
13
|
+
describe("PostgreSQLModelProviderStore secret handling", () => {
|
|
14
|
+
const originalKey = process.env.LATTICE_ENCRYPTION_KEY;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (originalKey === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
18
|
+
else process.env.LATTICE_ENCRYPTION_KEY = originalKey;
|
|
19
|
+
clearEncryptionKeyCache();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("empties an undecryptable API key but still lists the provider", async () => {
|
|
23
|
+
process.env.LATTICE_ENCRYPTION_KEY = "key-one";
|
|
24
|
+
clearEncryptionKeyCache();
|
|
25
|
+
const encrypted = encrypt("sk-secret");
|
|
26
|
+
|
|
27
|
+
process.env.LATTICE_ENCRYPTION_KEY = "key-two";
|
|
28
|
+
clearEncryptionKeyCache();
|
|
29
|
+
|
|
30
|
+
const query = jest.fn(async () => ({
|
|
31
|
+
rows: [{
|
|
32
|
+
id: "prov-1",
|
|
33
|
+
tenant_id: "tenant-1",
|
|
34
|
+
name: "openai",
|
|
35
|
+
display_name: "OpenAI",
|
|
36
|
+
protocol: "openai-compatible",
|
|
37
|
+
api_style: "chat-completions",
|
|
38
|
+
llm_provider: "openai",
|
|
39
|
+
base_url: "https://api.openai.com/v1",
|
|
40
|
+
api_key_enc: encrypted,
|
|
41
|
+
api_key_hint: "cret",
|
|
42
|
+
enabled: 1,
|
|
43
|
+
status: "ready",
|
|
44
|
+
last_error: null,
|
|
45
|
+
last_discovered_at: null,
|
|
46
|
+
created_by: null,
|
|
47
|
+
created_at: new Date(),
|
|
48
|
+
updated_at: new Date(),
|
|
49
|
+
}],
|
|
50
|
+
}));
|
|
51
|
+
const store = new PostgreSQLModelProviderStore({ pool: { query } as unknown as Pool });
|
|
52
|
+
|
|
53
|
+
const listed = await store.listProviders("tenant-1");
|
|
54
|
+
expect(listed).toHaveLength(1);
|
|
55
|
+
expect(listed[0].apiKey).toBe("");
|
|
56
|
+
expect(listed[0].baseURL).toBe("https://api.openai.com/v1");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -55,7 +55,7 @@ import { createChannelInstallationsTable } from "./migrations/channel_installati
|
|
|
55
55
|
import { alterChannelInstallationsTable } from "./migrations/channel_installations_alter_migration";
|
|
56
56
|
import { createChannelBindingsTable } from "./migrations/channel_bindings_migration";
|
|
57
57
|
import { createDatabaseConfigsTable } from "./migrations/database_config_migrations";
|
|
58
|
-
import { createConnectionConfigsTable, addConnectionSecretFields } from "./migrations/connection_config_migrations";
|
|
58
|
+
import { createConnectionConfigsTable, addConnectionSecretFields, reencryptLegacyConnectionSecrets } from "./migrations/connection_config_migrations";
|
|
59
59
|
import { createMetricsConfigsTable } from "./migrations/metrics_config_migrations";
|
|
60
60
|
import { createMcpServerConfigsTable } from "./migrations/mcp_server_config_migrations";
|
|
61
61
|
import { registerThreadMessageQueueMigrations } from "./migrations/queue_migrations";
|
|
@@ -96,6 +96,8 @@ import {
|
|
|
96
96
|
createModelProviderModelsTable,
|
|
97
97
|
addModelProviderApiStyle,
|
|
98
98
|
addModelProviderModelStale,
|
|
99
|
+
addModelProviderModelExtra,
|
|
100
|
+
addModelProviderModelDefault,
|
|
99
101
|
} from "./migrations/model_provider_migrations";
|
|
100
102
|
import { PostgreSQLCapabilityBundleStore } from "./stores/PostgreSQLCapabilityBundleStore";
|
|
101
103
|
import { PostgreSQLProjectRoomStore } from "./stores/PostgreSQLProjectRoomStore";
|
|
@@ -184,8 +186,11 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
184
186
|
mm.register(createDistributedLeases); // v185
|
|
185
187
|
mm.register(createOutboxTable); // v187
|
|
186
188
|
mm.register(addConnectionSecretFields); // v188
|
|
189
|
+
mm.register(reencryptLegacyConnectionSecrets); // v191
|
|
187
190
|
mm.register(createPluginDeployments); // v189
|
|
188
191
|
mm.register(dropPluginDeploymentAuthToken); // v190
|
|
192
|
+
mm.register(addModelProviderModelExtra); // v191
|
|
193
|
+
mm.register(addModelProviderModelDefault); // v192
|
|
189
194
|
|
|
190
195
|
await mm.migrate();
|
|
191
196
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { PoolClient } from "pg";
|
|
9
9
|
import { Migration } from "./migration";
|
|
10
|
+
import { encrypt, isEncryptedValue } from "@axiom-lattice/core";
|
|
10
11
|
|
|
11
12
|
export const createConnectionConfigsTable: Migration = {
|
|
12
13
|
version: 160,
|
|
@@ -73,3 +74,58 @@ export const addConnectionSecretFields: Migration = {
|
|
|
73
74
|
`);
|
|
74
75
|
},
|
|
75
76
|
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Re-encrypt declared secret fields that are still stored as plaintext.
|
|
80
|
+
*
|
|
81
|
+
* A plugin can mark a field as a secret after connections already exist (for
|
|
82
|
+
* example a field renamed from a plaintext key to a password), leaving legacy
|
|
83
|
+
* rows with plaintext where the store now expects ciphertext — which makes
|
|
84
|
+
* `decryptConfigFields` fail authentication for that field. This one-time,
|
|
85
|
+
* idempotent migration encrypts any declared secret field that does not look
|
|
86
|
+
* like our ciphertext; values that do look encrypted (including genuine
|
|
87
|
+
* ciphertext written under a different key) are left untouched, so no real
|
|
88
|
+
* ciphertext is ever destroyed.
|
|
89
|
+
*/
|
|
90
|
+
export const reencryptLegacyConnectionSecrets: Migration = {
|
|
91
|
+
version: 191,
|
|
92
|
+
name: "reencrypt_legacy_connection_secrets",
|
|
93
|
+
up: async (client: PoolClient) => {
|
|
94
|
+
const result = await client.query<{
|
|
95
|
+
id: string;
|
|
96
|
+
tenant_id: string;
|
|
97
|
+
config: Record<string, unknown> | null;
|
|
98
|
+
secret_fields: unknown;
|
|
99
|
+
}>(
|
|
100
|
+
`SELECT id, tenant_id, config, secret_fields
|
|
101
|
+
FROM lattice_connection_configs
|
|
102
|
+
WHERE secret_fields IS NOT NULL`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
for (const row of result.rows) {
|
|
106
|
+
const secretFields = Array.isArray(row.secret_fields)
|
|
107
|
+
? row.secret_fields.filter((field): field is string => typeof field === "string")
|
|
108
|
+
: [];
|
|
109
|
+
if (secretFields.length === 0) continue;
|
|
110
|
+
|
|
111
|
+
const config = row.config && typeof row.config === "object" ? { ...row.config } : {};
|
|
112
|
+
let changed = false;
|
|
113
|
+
for (const field of secretFields) {
|
|
114
|
+
const value = config[field];
|
|
115
|
+
if (typeof value === "string" && value.length > 0 && !isEncryptedValue(value)) {
|
|
116
|
+
config[field] = encrypt(value);
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (changed) {
|
|
122
|
+
await client.query(
|
|
123
|
+
`UPDATE lattice_connection_configs
|
|
124
|
+
SET config = $1::jsonb, updated_at = CURRENT_TIMESTAMP
|
|
125
|
+
WHERE tenant_id = $2 AND id = $3`,
|
|
126
|
+
[JSON.stringify(config), row.tenant_id, row.id],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
@@ -155,3 +155,77 @@ export const addModelProviderModelStale: Migration = {
|
|
|
155
155
|
`);
|
|
156
156
|
},
|
|
157
157
|
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Per-model LLM config.
|
|
161
|
+
*
|
|
162
|
+
* An upstream advertises an id, an owner and a creation time — nothing about
|
|
163
|
+
* how the model wants to be called. Temperature, token limits, whether it reads
|
|
164
|
+
* images and provider-specific request fields are things only the tenant knows,
|
|
165
|
+
* so they are stored against the model rather than guessed.
|
|
166
|
+
*
|
|
167
|
+
* JSON rather than a column per setting: the set of knobs is the upstream
|
|
168
|
+
* SDK's, not ours, and growing it should not cost a migration. The values are
|
|
169
|
+
* merged *under* the provider's own address and credential at registration, so
|
|
170
|
+
* a config here can tune behaviour but cannot redirect the request.
|
|
171
|
+
*
|
|
172
|
+
* `NOT NULL DEFAULT '{}'` lets existing rows mean "no per-model config" without
|
|
173
|
+
* a backfill. `IF NOT EXISTS` matches v183/v184 and keeps a re-run a no-op.
|
|
174
|
+
*/
|
|
175
|
+
export const addModelProviderModelExtra: Migration = {
|
|
176
|
+
version: 191,
|
|
177
|
+
name: 'add_model_provider_model_extra',
|
|
178
|
+
up: async (client: PoolClient) => {
|
|
179
|
+
await client.query(`
|
|
180
|
+
ALTER TABLE lattice_model_provider_models
|
|
181
|
+
ADD COLUMN IF NOT EXISTS extra JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
182
|
+
`);
|
|
183
|
+
},
|
|
184
|
+
down: async (client: PoolClient) => {
|
|
185
|
+
await client.query(`
|
|
186
|
+
ALTER TABLE lattice_model_provider_models DROP COLUMN IF EXISTS extra
|
|
187
|
+
`);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The tenant's default model.
|
|
193
|
+
*
|
|
194
|
+
* A model key of `"default"` is what every execution path falls back to — an
|
|
195
|
+
* agent with no model configured, the assistant, the eval judge, reading an
|
|
196
|
+
* image. It used to name a model the *deployment* registered at boot; this
|
|
197
|
+
* column lets a tenant name their own instead, and the deployment's remains the
|
|
198
|
+
* fallback for tenants that have not chosen one.
|
|
199
|
+
*
|
|
200
|
+
* The partial unique index is the point: **at most one default per tenant, per
|
|
201
|
+
* database**, rather than by application convention. `WHERE is_default` indexes
|
|
202
|
+
* only the true rows, so the many non-default rows are unconstrained.
|
|
203
|
+
*
|
|
204
|
+
* Note the index spans *providers* within a tenant, while the store writes one
|
|
205
|
+
* provider at a time — which is why `replaceProviderModels` clears the flag
|
|
206
|
+
* tenant-wide before setting a new one.
|
|
207
|
+
*/
|
|
208
|
+
export const addModelProviderModelDefault: Migration = {
|
|
209
|
+
version: 192,
|
|
210
|
+
name: 'add_model_provider_model_default',
|
|
211
|
+
up: async (client: PoolClient) => {
|
|
212
|
+
await client.query(`
|
|
213
|
+
ALTER TABLE lattice_model_provider_models
|
|
214
|
+
ADD COLUMN IF NOT EXISTS is_default BOOLEAN NOT NULL DEFAULT FALSE
|
|
215
|
+
`);
|
|
216
|
+
|
|
217
|
+
await client.query(`
|
|
218
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uk_lattice_model_provider_models_tenant_default
|
|
219
|
+
ON lattice_model_provider_models (tenant_id)
|
|
220
|
+
WHERE is_default
|
|
221
|
+
`);
|
|
222
|
+
},
|
|
223
|
+
down: async (client: PoolClient) => {
|
|
224
|
+
await client.query(`
|
|
225
|
+
DROP INDEX IF EXISTS uk_lattice_model_provider_models_tenant_default
|
|
226
|
+
`);
|
|
227
|
+
await client.query(`
|
|
228
|
+
ALTER TABLE lattice_model_provider_models DROP COLUMN IF EXISTS is_default
|
|
229
|
+
`);
|
|
230
|
+
},
|
|
231
|
+
};
|
|
@@ -11,7 +11,7 @@ import { MigrationManager } from "../migrations/migration";
|
|
|
11
11
|
import { createA2AApiKeysTable } from "../migrations/a2a_api_key_migration";
|
|
12
12
|
import { addA2AKeyAssistantIds } from "../migrations/add_a2a_key_assistant_ids";
|
|
13
13
|
import { addApiKeyGrants } from "../migrations/add_api_key_grants";
|
|
14
|
-
import { encrypt, decrypt } from "@axiom-lattice/core";
|
|
14
|
+
import { encrypt, decrypt, safeDecrypt } from "@axiom-lattice/core";
|
|
15
15
|
import { randomUUID } from "crypto";
|
|
16
16
|
|
|
17
17
|
export interface PostgreSQLA2AApiKeyStoreOptions {
|
|
@@ -40,7 +40,7 @@ function generateApiKey(): string {
|
|
|
40
40
|
function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
|
|
41
41
|
return {
|
|
42
42
|
id: row.id,
|
|
43
|
-
key:
|
|
43
|
+
key: safeDecrypt(row.key_value),
|
|
44
44
|
tenantId: row.tenant_id,
|
|
45
45
|
projectId: row.project_id,
|
|
46
46
|
assistantIds: row.assistant_ids ?? undefined,
|
|
@@ -116,19 +116,6 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
116
116
|
if (!this.initialized) await this.initialize();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/** Map rows that decrypt cleanly; skip rows under an unknown key. */
|
|
120
|
-
private safeMap(rows: KeyRow[]): A2AApiKeyRecord[] {
|
|
121
|
-
const out: A2AApiKeyRecord[] = [];
|
|
122
|
-
for (const row of rows) {
|
|
123
|
-
try {
|
|
124
|
-
out.push(mapRowToRecord(row));
|
|
125
|
-
} catch {
|
|
126
|
-
// Skip undecryptable rows.
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
119
|
async findByKey(key: string): Promise<A2AApiKeyRecord | null> {
|
|
133
120
|
await this.ensureInitialized();
|
|
134
121
|
const all = await this.pool.query<KeyRow>(
|
|
@@ -168,13 +155,13 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
168
155
|
`SELECT * FROM lattice_a2a_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
169
156
|
[params.tenantId, limit, offset],
|
|
170
157
|
);
|
|
171
|
-
return
|
|
158
|
+
return result.rows.map((row) => mapRowToRecord(row));
|
|
172
159
|
}
|
|
173
160
|
const result = await this.pool.query<KeyRow>(
|
|
174
161
|
`SELECT * FROM lattice_a2a_api_keys ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
175
162
|
[limit, offset],
|
|
176
163
|
);
|
|
177
|
-
return
|
|
164
|
+
return result.rows.map((row) => mapRowToRecord(row));
|
|
178
165
|
}
|
|
179
166
|
|
|
180
167
|
async create(input: CreateA2AApiKeyInput): Promise<A2AApiKeyRecord> {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { MigrationManager } from "../migrations/migration";
|
|
11
11
|
import { createChannelInstallationsTable } from "../migrations/channel_installation_migrations";
|
|
12
12
|
import { alterChannelInstallationsTable } from "../migrations/channel_installations_alter_migration";
|
|
13
|
-
import {
|
|
13
|
+
import { safeDecrypt, encrypt } from "@axiom-lattice/core";
|
|
14
14
|
|
|
15
15
|
export interface PostgreSQLChannelInstallationStoreOptions {
|
|
16
16
|
pool?: Pool;
|
|
@@ -333,9 +333,9 @@ export class PostgreSQLChannelInstallationStore
|
|
|
333
333
|
): Record<string, unknown> {
|
|
334
334
|
return {
|
|
335
335
|
...config,
|
|
336
|
-
...(typeof config.appSecret === "string" ? { appSecret:
|
|
337
|
-
...(typeof config.verificationToken === "string" ? { verificationToken:
|
|
338
|
-
...(typeof config.encryptKey === "string" ? { encryptKey:
|
|
336
|
+
...(typeof config.appSecret === "string" ? { appSecret: safeDecrypt(config.appSecret) } : {}),
|
|
337
|
+
...(typeof config.verificationToken === "string" ? { verificationToken: safeDecrypt(config.verificationToken) } : {}),
|
|
338
|
+
...(typeof config.encryptKey === "string" ? { encryptKey: safeDecrypt(config.encryptKey) } : {}),
|
|
339
339
|
};
|
|
340
340
|
}
|
|
341
341
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '@axiom-lattice/protocols';
|
|
14
14
|
import { MigrationManager } from '../migrations/migration';
|
|
15
15
|
import { createDatabaseConfigsTable } from '../migrations/database_config_migrations';
|
|
16
|
-
import { encrypt,
|
|
16
|
+
import { encrypt, safeDecrypt } from '@axiom-lattice/core';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* PostgreSQL DatabaseConfigStore options
|
|
@@ -419,12 +419,7 @@ export class PostgreSQLDatabaseConfigStore implements DatabaseConfigStore {
|
|
|
419
419
|
|
|
420
420
|
// Decrypt password if present
|
|
421
421
|
if (config.password) {
|
|
422
|
-
|
|
423
|
-
config.password = decrypt(config.password);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error('Failed to decrypt database password:', error);
|
|
426
|
-
throw new Error('Failed to decrypt database configuration');
|
|
427
|
-
}
|
|
422
|
+
config.password = safeDecrypt(config.password);
|
|
428
423
|
}
|
|
429
424
|
|
|
430
425
|
return {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '@axiom-lattice/protocols';
|
|
14
14
|
import { MigrationManager } from '../migrations/migration';
|
|
15
15
|
import { createMcpServerConfigsTable } from '../migrations/mcp_server_config_migrations';
|
|
16
|
-
import { encrypt,
|
|
16
|
+
import { encrypt, safeDecrypt } from '@axiom-lattice/core';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* PostgreSQL McpServerConfigStore options
|
|
@@ -452,30 +452,20 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
452
452
|
|
|
453
453
|
// Decrypt env values if present and encrypted
|
|
454
454
|
if (config.env && row.is_env_encrypted) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
decryptedEnv[key] = decrypt(value);
|
|
459
|
-
}
|
|
460
|
-
config.env = decryptedEnv;
|
|
461
|
-
} catch (error) {
|
|
462
|
-
console.error('Failed to decrypt MCP server env:', error);
|
|
463
|
-
throw new Error('Failed to decrypt MCP server configuration');
|
|
455
|
+
const decryptedEnv: Record<string, string> = {};
|
|
456
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
457
|
+
decryptedEnv[key] = safeDecrypt(value);
|
|
464
458
|
}
|
|
459
|
+
config.env = decryptedEnv;
|
|
465
460
|
}
|
|
466
461
|
|
|
467
462
|
// Decrypt header values if present and encrypted
|
|
468
463
|
if (config.headers && row.is_env_encrypted) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
decryptedHeaders[key] = decrypt(value);
|
|
473
|
-
}
|
|
474
|
-
config.headers = decryptedHeaders;
|
|
475
|
-
} catch (error) {
|
|
476
|
-
console.error('Failed to decrypt MCP server headers:', error);
|
|
477
|
-
throw new Error('Failed to decrypt MCP server configuration');
|
|
464
|
+
const decryptedHeaders: Record<string, string> = {};
|
|
465
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
466
|
+
decryptedHeaders[key] = safeDecrypt(value);
|
|
478
467
|
}
|
|
468
|
+
config.headers = decryptedHeaders;
|
|
479
469
|
}
|
|
480
470
|
|
|
481
471
|
return {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '@axiom-lattice/protocols';
|
|
14
14
|
import { MigrationManager } from '../migrations/migration';
|
|
15
15
|
import { createMetricsConfigsTable } from '../migrations/metrics_config_migrations';
|
|
16
|
-
import { encrypt,
|
|
16
|
+
import { encrypt, safeDecrypt } from '@axiom-lattice/core';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* PostgreSQL MetricsServerConfigStore options
|
|
@@ -419,22 +419,12 @@ export class PostgreSQLMetricsServerConfigStore implements MetricsServerConfigSt
|
|
|
419
419
|
|
|
420
420
|
// Decrypt apiKey if present
|
|
421
421
|
if (config.apiKey) {
|
|
422
|
-
|
|
423
|
-
config.apiKey = decrypt(config.apiKey);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error('Failed to decrypt apiKey:', error);
|
|
426
|
-
throw new Error('Failed to decrypt metrics server configuration');
|
|
427
|
-
}
|
|
422
|
+
config.apiKey = safeDecrypt(config.apiKey);
|
|
428
423
|
}
|
|
429
424
|
|
|
430
425
|
// Decrypt password if present
|
|
431
426
|
if (config.password) {
|
|
432
|
-
|
|
433
|
-
config.password = decrypt(config.password);
|
|
434
|
-
} catch (error) {
|
|
435
|
-
console.error('Failed to decrypt password:', error);
|
|
436
|
-
throw new Error('Failed to decrypt metrics server configuration');
|
|
437
|
-
}
|
|
427
|
+
config.password = safeDecrypt(config.password);
|
|
438
428
|
}
|
|
439
429
|
|
|
440
430
|
return {
|