@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.
@@ -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: decrypt(row.key_value),
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 this.safeMap(result.rows);
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 this.safeMap(result.rows);
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 { decrypt, encrypt } from "@axiom-lattice/core";
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: decrypt(config.appSecret) } : {}),
337
- ...(typeof config.verificationToken === "string" ? { verificationToken: decrypt(config.verificationToken) } : {}),
338
- ...(typeof config.encryptKey === "string" ? { encryptKey: decrypt(config.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, decrypt } from '@axiom-lattice/core';
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
- try {
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, decrypt } from '@axiom-lattice/core';
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
- try {
456
- const decryptedEnv: Record<string, string> = {};
457
- for (const [key, value] of Object.entries(config.env)) {
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
- try {
470
- const decryptedHeaders: Record<string, string> = {};
471
- for (const [key, value] of Object.entries(config.headers)) {
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, decrypt } from '@axiom-lattice/core';
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
- try {
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
- try {
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 {