@axiom-lattice/local-stores 3.2.4 → 3.2.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 +20 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +49 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +61 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/LocalA2AApiKeyStore.test.ts +33 -4
- package/src/__tests__/LocalConnectionStore.test.ts +34 -4
- package/src/__tests__/LocalMcpServerConfigStore.test.ts +28 -0
- package/src/__tests__/LocalModelProviderStore.test.ts +65 -0
- package/src/stores/LocalA2AApiKeyStore.ts +2 -4
- package/src/stores/LocalConnectionStore.ts +45 -1
- package/src/stores/LocalMcpServerConfigStore.ts +9 -19
- package/src/stores/LocalModelProviderStore.ts +2 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/local-stores",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.6",
|
|
4
4
|
"description": "Local SQLite-based stores for Axiom Lattice framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@types/sql.js": "^1.4.11",
|
|
26
26
|
"sql.js": "^1.14.1",
|
|
27
27
|
"uuid": "^14.0.1",
|
|
28
|
-
"@axiom-lattice/core": "
|
|
28
|
+
"@axiom-lattice/core": "7.0.0",
|
|
29
29
|
"@axiom-lattice/protocols": "4.8.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -9,10 +9,23 @@ import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
|
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
10
|
import path from "path";
|
|
11
11
|
|
|
12
|
-
jest.mock("@axiom-lattice/core", () =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
jest.mock("@axiom-lattice/core", () => {
|
|
13
|
+
const decrypt = (value: string): string => {
|
|
14
|
+
if (!value.startsWith("enc:")) throw new Error("Failed to decrypt");
|
|
15
|
+
return value.replace(/^enc:/, "");
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
encrypt: (value: string) => `enc:${value}`,
|
|
19
|
+
decrypt,
|
|
20
|
+
safeDecrypt: (value: string) => {
|
|
21
|
+
try {
|
|
22
|
+
return decrypt(value);
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
16
29
|
|
|
17
30
|
import { initDatabase, closeDatabase, type DatabaseWrapper } from "../database";
|
|
18
31
|
import { LocalA2AApiKeyStore } from "../stores/LocalA2AApiKeyStore";
|
|
@@ -125,4 +138,20 @@ describe("LocalA2AApiKeyStore", () => {
|
|
|
125
138
|
const record = await store.findById(created.id);
|
|
126
139
|
expect(record?.grants).toEqual(grants);
|
|
127
140
|
});
|
|
141
|
+
|
|
142
|
+
it("empties an undecryptable key value but still lists the record", async () => {
|
|
143
|
+
const now = new Date().toISOString();
|
|
144
|
+
db.prepare(
|
|
145
|
+
`INSERT INTO lt_a2a_api_keys
|
|
146
|
+
(key_value, tenant_id, project_id, label, enabled, created_at, updated_at)
|
|
147
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
148
|
+
).run("plaintext-legacy-key", "t1", "p1", "broken", 1, now, now);
|
|
149
|
+
|
|
150
|
+
store = new LocalA2AApiKeyStore(db);
|
|
151
|
+
const records = await store.list({ tenantId: "t1" });
|
|
152
|
+
|
|
153
|
+
expect(records).toHaveLength(1);
|
|
154
|
+
expect(records[0].key).toBe("");
|
|
155
|
+
expect(records[0].projectId).toBe("p1");
|
|
156
|
+
});
|
|
128
157
|
});
|
|
@@ -148,7 +148,30 @@ describe("LocalConnectionStore secret field encryption", () => {
|
|
|
148
148
|
expect(JSON.parse(raw.config).apiKey).not.toBe("sk-fresh");
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it("
|
|
151
|
+
it("re-encrypts a legacy plaintext secret when the store is constructed", async () => {
|
|
152
|
+
db.run(
|
|
153
|
+
"INSERT INTO connection_configs (id, tenant_id, type, key, name, description, config, secret_fields, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
154
|
+
"legacy-secret-id", "t", "business-objects", "legacy", "Legacy", null,
|
|
155
|
+
JSON.stringify({ boStoreKey: "plain-store-key", baseUrl: "https://bo.example" }),
|
|
156
|
+
JSON.stringify(["boStoreKey"]),
|
|
157
|
+
"2024-01-01T00:00:00.000Z", "2024-01-01T00:00:00.000Z",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const upgraded = new LocalConnectionStore(db);
|
|
161
|
+
|
|
162
|
+
const raw = db
|
|
163
|
+
.prepare("SELECT config FROM connection_configs WHERE id = ?")
|
|
164
|
+
.get("legacy-secret-id") as { config: string };
|
|
165
|
+
const parsed = JSON.parse(raw.config) as Record<string, unknown>;
|
|
166
|
+
expect(parsed.boStoreKey).not.toBe("plain-store-key");
|
|
167
|
+
expect(decrypt(parsed.boStoreKey as string)).toBe("plain-store-key");
|
|
168
|
+
expect(parsed.baseUrl).toBe("https://bo.example");
|
|
169
|
+
|
|
170
|
+
const read = await upgraded.getByKey("t", "business-objects", "legacy");
|
|
171
|
+
expect(read?.config.boStoreKey).toBe("plain-store-key");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("empties an undecryptable secret field but still lists the connection", async () => {
|
|
152
175
|
const previous = process.env.LATTICE_ENCRYPTION_KEY;
|
|
153
176
|
try {
|
|
154
177
|
process.env.LATTICE_ENCRYPTION_KEY = "test-key-one";
|
|
@@ -158,15 +181,22 @@ describe("LocalConnectionStore secret field encryption", () => {
|
|
|
158
181
|
type: "semantic-metrics",
|
|
159
182
|
key: "conn",
|
|
160
183
|
name: "Conn",
|
|
161
|
-
config: { apiKey: "sk-one" },
|
|
184
|
+
config: { baseUrl: "https://metrics.example/api/v1", apiKey: "sk-one" },
|
|
162
185
|
secretFields: ["apiKey"],
|
|
163
186
|
});
|
|
164
187
|
|
|
165
188
|
process.env.LATTICE_ENCRYPTION_KEY = "test-key-two";
|
|
166
189
|
clearEncryptionKeyCache();
|
|
167
190
|
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
// A wrong key no longer throws; the secret is empty but the entry (and
|
|
192
|
+
// its non-secret fields) is still visible so connections can be listed.
|
|
193
|
+
const read = await store.getByKey("t", "semantic-metrics", "conn");
|
|
194
|
+
expect(read?.config.apiKey).toBe("");
|
|
195
|
+
expect(read?.config.baseUrl).toBe("https://metrics.example/api/v1");
|
|
196
|
+
|
|
197
|
+
const listed = await store.listByType("t", "semantic-metrics");
|
|
198
|
+
expect(listed).toHaveLength(1);
|
|
199
|
+
expect(listed[0].config.apiKey).toBe("");
|
|
170
200
|
} finally {
|
|
171
201
|
if (previous === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
172
202
|
else process.env.LATTICE_ENCRYPTION_KEY = previous;
|
|
@@ -10,6 +10,7 @@ import path from "path";
|
|
|
10
10
|
|
|
11
11
|
import { closeDatabase, initDatabase } from "../database";
|
|
12
12
|
import { LocalMcpServerConfigStore } from "../stores/LocalMcpServerConfigStore";
|
|
13
|
+
import { clearEncryptionKeyCache } from "@axiom-lattice/core";
|
|
13
14
|
|
|
14
15
|
const nextDbPath = (): string => path.join(
|
|
15
16
|
tmpdir(), `lt-mcp-config-${Date.now()}-${Math.random().toString(36).slice(2)}.db`,
|
|
@@ -74,4 +75,31 @@ describe("LocalMcpServerConfigStore secret handling", () => {
|
|
|
74
75
|
const read = await store.getConfigByKey("tenant-1", "remote");
|
|
75
76
|
expect(read?.config.headers?.Authorization).toBe("Bearer rotated-token");
|
|
76
77
|
});
|
|
78
|
+
|
|
79
|
+
it("empties undecryptable secrets but still lists and returns the config", async () => {
|
|
80
|
+
const previous = process.env.LATTICE_ENCRYPTION_KEY;
|
|
81
|
+
try {
|
|
82
|
+
process.env.LATTICE_ENCRYPTION_KEY = "test-key-one";
|
|
83
|
+
clearEncryptionKeyCache();
|
|
84
|
+
await store.createConfig("tenant-1", "cfg-4", {
|
|
85
|
+
key: "remote",
|
|
86
|
+
config: { ...httpConfig, env: { MCP_TOKEN: "env-secret" } },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
process.env.LATTICE_ENCRYPTION_KEY = "test-key-two";
|
|
90
|
+
clearEncryptionKeyCache();
|
|
91
|
+
|
|
92
|
+
const read = await store.getConfigByKey("tenant-1", "remote");
|
|
93
|
+
expect(read?.config.env?.MCP_TOKEN).toBe("");
|
|
94
|
+
expect(read?.config.headers?.Authorization).toBe("");
|
|
95
|
+
expect(read?.config.url).toBe("https://mcp.example.com/mcp");
|
|
96
|
+
|
|
97
|
+
const listed = await store.getAllConfigs("tenant-1");
|
|
98
|
+
expect(listed).toHaveLength(1);
|
|
99
|
+
} finally {
|
|
100
|
+
if (previous === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
101
|
+
else process.env.LATTICE_ENCRYPTION_KEY = previous;
|
|
102
|
+
clearEncryptionKeyCache();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
77
105
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The @axiom-lattice/core barrel cannot load under jest in this package (ESM-only
|
|
2
|
+
// transitive deps), so mock it with the real encryption module — node:crypto only.
|
|
3
|
+
jest.mock("@axiom-lattice/core", () => ({
|
|
4
|
+
...jest.requireActual("../../../core/src/util/encryption"),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
|
|
8
|
+
import { tmpdir } from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
import { closeDatabase, initDatabase, type DatabaseWrapper } from "../database";
|
|
12
|
+
import { LocalModelProviderStore } from "../stores/LocalModelProviderStore";
|
|
13
|
+
import { clearEncryptionKeyCache } from "@axiom-lattice/core";
|
|
14
|
+
|
|
15
|
+
describe("LocalModelProviderStore secret handling", () => {
|
|
16
|
+
let store: LocalModelProviderStore;
|
|
17
|
+
let db: DatabaseWrapper;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const dbPath = path.join(tmpdir(), `lt-model-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
21
|
+
db = await initDatabase({ dbPath });
|
|
22
|
+
store = new LocalModelProviderStore(db);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => closeDatabase());
|
|
26
|
+
|
|
27
|
+
async function createProvider(id: string, apiKey: string): Promise<void> {
|
|
28
|
+
await store.createProvider("t", id, {
|
|
29
|
+
name: `provider-${id}`,
|
|
30
|
+
protocol: "openai-compatible",
|
|
31
|
+
baseURL: `https://${id}.example.com/v1`,
|
|
32
|
+
apiKey,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it("decrypts the API key on read", async () => {
|
|
37
|
+
await createProvider("p1", "sk-one");
|
|
38
|
+
const read = await store.getProviderById("t", "p1");
|
|
39
|
+
expect(read?.apiKey).toBe("sk-one");
|
|
40
|
+
expect(read?.baseURL).toBe("https://p1.example.com/v1");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("empties an undecryptable API key but still lists the provider", async () => {
|
|
44
|
+
const previous = process.env.LATTICE_ENCRYPTION_KEY;
|
|
45
|
+
try {
|
|
46
|
+
process.env.LATTICE_ENCRYPTION_KEY = "test-key-one";
|
|
47
|
+
clearEncryptionKeyCache();
|
|
48
|
+
await createProvider("p1", "sk-one");
|
|
49
|
+
|
|
50
|
+
process.env.LATTICE_ENCRYPTION_KEY = "test-key-two";
|
|
51
|
+
clearEncryptionKeyCache();
|
|
52
|
+
|
|
53
|
+
const read = await store.getProviderById("t", "p1");
|
|
54
|
+
expect(read?.apiKey).toBe("");
|
|
55
|
+
expect(read?.baseURL).toBe("https://p1.example.com/v1");
|
|
56
|
+
|
|
57
|
+
const listed = await store.listProviders("t");
|
|
58
|
+
expect(listed).toHaveLength(1);
|
|
59
|
+
} finally {
|
|
60
|
+
if (previous === undefined) delete process.env.LATTICE_ENCRYPTION_KEY;
|
|
61
|
+
else process.env.LATTICE_ENCRYPTION_KEY = previous;
|
|
62
|
+
clearEncryptionKeyCache();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
OpenGrant,
|
|
13
13
|
} from "@axiom-lattice/protocols";
|
|
14
14
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
15
|
-
import { encrypt, decrypt } from "@axiom-lattice/core";
|
|
15
|
+
import { encrypt, decrypt, safeDecrypt } from "@axiom-lattice/core";
|
|
16
16
|
import { randomUUID } from "crypto";
|
|
17
17
|
|
|
18
18
|
const DDL = `
|
|
@@ -241,11 +241,9 @@ export class LocalA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
|
|
244
|
-
let key = "";
|
|
245
|
-
try { key = decrypt(row.key_value); } catch { key = row.key_value; }
|
|
246
244
|
return {
|
|
247
245
|
id: row.id,
|
|
248
|
-
key,
|
|
246
|
+
key: safeDecrypt(row.key_value),
|
|
249
247
|
tenantId: row.tenant_id,
|
|
250
248
|
projectId: row.project_id,
|
|
251
249
|
assistantIds: parseAssistantIds(row.assistant_ids),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { DatabaseWrapper, ensureTable, nowISO } from "../database";
|
|
5
5
|
import type { ConnectionEntry, ConnectionStore } from "@axiom-lattice/protocols";
|
|
6
6
|
import { v4 as uuid } from "uuid";
|
|
7
|
-
import { encryptConfigFields, decryptConfigFields } from "@axiom-lattice/core";
|
|
7
|
+
import { encryptConfigFields, decryptConfigFields, encrypt, isEncryptedValue } from "@axiom-lattice/core";
|
|
8
8
|
|
|
9
9
|
const DDL = `
|
|
10
10
|
CREATE TABLE IF NOT EXISTS connection_configs (
|
|
@@ -58,6 +58,7 @@ export class LocalConnectionStore implements ConnectionStore {
|
|
|
58
58
|
this.db = db;
|
|
59
59
|
ensureTable(db, DDL);
|
|
60
60
|
this.ensureColumn("connection_configs", "secret_fields", "secret_fields TEXT");
|
|
61
|
+
this.reencryptLegacySecrets();
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
private ensureColumn(table: string, column: string, ddl: string): void {
|
|
@@ -67,6 +68,49 @@ export class LocalConnectionStore implements ConnectionStore {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Re-encrypt declared secret fields that are still stored as plaintext.
|
|
73
|
+
*
|
|
74
|
+
* A plugin can mark a field as a secret after connections already exist (e.g.
|
|
75
|
+
* a field renamed from a plaintext key to a password), leaving legacy rows
|
|
76
|
+
* with plaintext where ciphertext is now expected. This one-time, idempotent
|
|
77
|
+
* pass encrypts any declared secret field that does not look like our
|
|
78
|
+
* ciphertext; values that do look encrypted are left untouched, so genuine
|
|
79
|
+
* ciphertext written under another key is never destroyed.
|
|
80
|
+
*/
|
|
81
|
+
private reencryptLegacySecrets(): void {
|
|
82
|
+
const rows = this.db.prepare(
|
|
83
|
+
`SELECT id, tenant_id, config, secret_fields FROM connection_configs WHERE secret_fields IS NOT NULL`,
|
|
84
|
+
).all() as unknown as Array<{ id: string; tenant_id: string; config: string; secret_fields: string | null }>;
|
|
85
|
+
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const secretFields = parseSecretFields(row.secret_fields);
|
|
88
|
+
if (!secretFields?.length) continue;
|
|
89
|
+
|
|
90
|
+
let config: Record<string, unknown>;
|
|
91
|
+
try {
|
|
92
|
+
config = JSON.parse(row.config) as Record<string, unknown>;
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let changed = false;
|
|
98
|
+
for (const field of secretFields) {
|
|
99
|
+
const value = config[field];
|
|
100
|
+
if (typeof value === "string" && value.length > 0 && !isEncryptedValue(value)) {
|
|
101
|
+
config[field] = encrypt(value);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (changed) {
|
|
107
|
+
this.db.prepare(
|
|
108
|
+
`UPDATE connection_configs SET config = ?, updated_at = ? WHERE tenant_id = ? AND id = ?`,
|
|
109
|
+
).run(JSON.stringify(config), nowISO(), row.tenant_id, row.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
70
114
|
async listByTenant(tenantId: string): Promise<ConnectionEntry[]> {
|
|
71
115
|
const rows = this.db.prepare(
|
|
72
116
|
`SELECT * FROM connection_configs WHERE tenant_id = ? ORDER BY created_at`,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
McpServerConfig,
|
|
12
12
|
} from "@axiom-lattice/protocols";
|
|
13
13
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
14
|
-
import { encrypt,
|
|
14
|
+
import { encrypt, safeDecrypt } from "@axiom-lattice/core";
|
|
15
15
|
|
|
16
16
|
const DDL = `
|
|
17
17
|
CREATE TABLE IF NOT EXISTS lt_mcp_configs (
|
|
@@ -179,29 +179,19 @@ function mapRowToEntry(row: ConfigRow): McpServerConfigEntry {
|
|
|
179
179
|
const config: McpServerConfig = JSON.parse(row.config);
|
|
180
180
|
|
|
181
181
|
if (config.env && row.is_env_encrypted) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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");
|
|
182
|
+
const decryptedEnv: Record<string, string> = {};
|
|
183
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
184
|
+
decryptedEnv[key] = safeDecrypt(value);
|
|
191
185
|
}
|
|
186
|
+
config.env = decryptedEnv;
|
|
192
187
|
}
|
|
193
188
|
|
|
194
189
|
if (config.headers && row.is_env_encrypted) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
decryptedHeaders[key] = decrypt(value);
|
|
199
|
-
}
|
|
200
|
-
config.headers = decryptedHeaders;
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error("Failed to decrypt MCP server headers:", error);
|
|
203
|
-
throw new Error("Failed to decrypt MCP server configuration");
|
|
190
|
+
const decryptedHeaders: Record<string, string> = {};
|
|
191
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
192
|
+
decryptedHeaders[key] = safeDecrypt(value);
|
|
204
193
|
}
|
|
194
|
+
config.headers = decryptedHeaders;
|
|
205
195
|
}
|
|
206
196
|
|
|
207
197
|
return {
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
ProviderModelInput,
|
|
17
17
|
} from "@axiom-lattice/protocols";
|
|
18
18
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
19
|
-
import { encrypt,
|
|
19
|
+
import { encrypt, safeDecrypt } from "@axiom-lattice/core";
|
|
20
20
|
|
|
21
21
|
const DDL = `
|
|
22
22
|
CREATE TABLE IF NOT EXISTS lt_model_providers (
|
|
@@ -457,12 +457,7 @@ function mapRowToEntry(row: ProviderRow): ModelProviderEntry {
|
|
|
457
457
|
let apiKey: string | undefined;
|
|
458
458
|
|
|
459
459
|
if (row.api_key_enc) {
|
|
460
|
-
|
|
461
|
-
apiKey = decrypt(row.api_key_enc);
|
|
462
|
-
} catch (error) {
|
|
463
|
-
console.error("Failed to decrypt model provider API key:", error);
|
|
464
|
-
throw new Error("Failed to decrypt model provider API key");
|
|
465
|
-
}
|
|
460
|
+
apiKey = safeDecrypt(row.api_key_enc);
|
|
466
461
|
}
|
|
467
462
|
|
|
468
463
|
return {
|