@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.3

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +178 -175
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +29 -1
  4. package/dist/shared/wizard-local-telemetry.js +50 -0
  5. package/dist/worker/console/chat/chat-event-store.js +3 -0
  6. package/dist/worker/console/chat/db-connections.js +790 -0
  7. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  8. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +627 -0
  9. package/dist/worker/console/chat/pi-runtime.js +66 -6
  10. package/dist/worker/console/chat/routes.js +154 -3
  11. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  12. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  13. package/dist/worker/console/chat/tools.js +34 -0
  14. package/dist/worker/console/chat/turn-process.js +43 -39
  15. package/dist/worker/console/dag-confirmation.js +3 -0
  16. package/dist/worker/console/dag-execution-receipt.js +3 -0
  17. package/dist/worker/console/operator-actions.js +32 -2
  18. package/dist/worker/console/server.js +5 -1
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-COQlLTI7.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-zN3QkNaV.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-Pyoyj5f2.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-BNppA5WW.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-lyxL8nTx.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-CgLPkTw2.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-0hgjcdK4.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-D63N9d4i.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-CUw2AyB5.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-Q_kwZT30.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-C7vMiKcw.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-T9GbV5Hz.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-D8nUSUJI.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-BCH2ewdp.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-BTdDuDBN.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-Cqm2RcKF.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuSD1Mhb.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuSD1Mhb.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-pjwfpVu4.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-BvalptKA.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BU4qJ6d7.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-DVTpq8J9.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-B8s1W6Fc.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DPrFk9yH.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-CnQIQsHC.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-op0DEpCv.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-DXQamm4T.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-b1ad2C0r.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-B4cD-ZFJ.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-CrgGjlQ1.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-DovmXQNV.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-DpFyu2Jn.js} +1 -1
  51. package/dist/worker/console/static/assets/index-DHBWEwz_.css +1 -0
  52. package/dist/worker/console/static/assets/index-JRfTUgP7.js +478 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-D3MoQZIl.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-Ci6C1yjS.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-BWMo2lIP.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdthIAg3.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DuFAlSOj.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-Dqz3Slj9.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-DNVTrD09.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-CgfZPfBh.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-ClM9P7yo.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-DwnEak2J.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-rK4hxt9P.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-Cjqa6vo3.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-D3Fa250Z.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-2PdrNajj.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-BBcuGeZn.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CSJX9X6s.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cqskj0zt.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-pOUU1bic.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CM0ZIhNP.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-CAeqfQn0.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-DYPLOMVO.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-Dy2V0Cha.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-Dj9o7jrn.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -1
  78. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  79. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  80. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  81. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  82. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -7
  83. package/dist/worker/console/static-src/operator-chat/useChatStream.js +47 -5
  84. package/dist/worker/console/static-src/operator-chat/useChatThread.js +24 -2
  85. package/dist/worker/console/workspace-context.js +2 -0
  86. package/dist/worker/observe/static/console-theme.css +174 -0
  87. package/dist/worker/observe/static/console-theme.js +36 -0
  88. package/dist/worker/observe/static/operator-chrome.css +85 -0
  89. package/dist/worker/observe/static/operator-chrome.js +51 -0
  90. package/dist/worker/scheduler/scheduled-goal-evidence.js +60 -2
  91. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  92. package/package.json +3 -1
  93. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  94. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  95. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  96. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  97. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -0,0 +1,790 @@
1
+ /**
2
+ * Operator Chat — governed database connection registry (2026-09-12).
3
+ *
4
+ * Manages NAMED database connection configs persisted in Console app-data
5
+ * (`db/connections.json` under the Console-global app-data namespace). Credentials NEVER live in
6
+ * the config: sqlite stores an absolute file path; postgres/mysql store safe
7
+ * host/port/database/user metadata or the NAME of a DSN environment variable.
8
+ * Password/DSN values are never persisted, echoed, listed, or exported.
9
+ *
10
+ * Engines:
11
+ * - sqlite: Node built-in `node:sqlite` (DatabaseSync) — zero npm deps.
12
+ * - postgres / mysql: packaged pg/mysql2 drivers loaded dynamically; a broken
13
+ * installation maps to DB_ENGINE_UNAVAILABLE instead of crashing Console.
14
+ */
15
+ import path from "node:path";
16
+ import os from "node:os";
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
19
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from "node:crypto";
20
+ import { writeSecureJson } from "../../../infrastructure/console/app-data.js";
21
+ export class DbConnectionError extends Error {
22
+ code;
23
+ constructor(code, message, options) {
24
+ super(message, options);
25
+ this.code = code;
26
+ this.name = "DbConnectionError";
27
+ }
28
+ }
29
+ /** Narrow a handle to the sqlite-specific surface (prepare/columns). */
30
+ export function isSqliteHandle(handle) {
31
+ return (typeof handle.prepare === "function");
32
+ }
33
+ function isRecord(value) {
34
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
35
+ }
36
+ function toDbEngine(value) {
37
+ if (value === "sqlite" || value === "postgres" || value === "mysql") {
38
+ return value;
39
+ }
40
+ throw new DbConnectionError("DB_INVALID_CONFIG", `engine must be one of sqlite|postgres|mysql (got "${value}")`);
41
+ }
42
+ /** Split a possibly multi-statement SQL script into trimmed statements. */
43
+ function normalizePersisted(raw) {
44
+ const map = new Map();
45
+ if (!Array.isArray(raw))
46
+ return map;
47
+ for (const entry of raw) {
48
+ if (!isRecord(entry))
49
+ continue;
50
+ const name = typeof entry.name === "string" ? entry.name : undefined;
51
+ const engine = typeof entry.engine === "string" ? toDbEngine(entry.engine) : undefined;
52
+ if (!name || !engine)
53
+ continue;
54
+ const createdAt = typeof entry.createdAt === "string" && entry.createdAt
55
+ ? entry.createdAt
56
+ : new Date(0).toISOString();
57
+ try {
58
+ map.set(name, normalizeConnectionInput({
59
+ name,
60
+ engine,
61
+ createdAt,
62
+ ...(typeof entry.path === "string" ? { path: entry.path } : {}),
63
+ ...(entry.mode === "parameters" ? { mode: "parameters" } : {}),
64
+ ...(typeof entry.urlEnv === "string" ? { urlEnv: entry.urlEnv } : {}),
65
+ ...(typeof entry.host === "string" ? { host: entry.host } : {}),
66
+ ...(typeof entry.port === "number" ? { port: entry.port } : {}),
67
+ ...(typeof entry.database === "string" ? { database: entry.database } : {}),
68
+ ...(typeof entry.username === "string" ? { username: entry.username } : {}),
69
+ ...(entry.passwordMode === "environment" || entry.passwordMode === "encrypted" ? { passwordMode: entry.passwordMode } : {}),
70
+ ...(typeof entry.passwordRef === "string" ? { passwordRef: entry.passwordRef } : {}),
71
+ ...(typeof entry.passwordEnv === "string" ? { passwordEnv: entry.passwordEnv } : {}),
72
+ ...(typeof entry.ssl === "boolean" ? { ssl: entry.ssl } : {}),
73
+ }));
74
+ }
75
+ catch {
76
+ // Skip malformed persisted entries without preventing other connections from loading.
77
+ }
78
+ }
79
+ return map;
80
+ }
81
+ function envName(value) {
82
+ const name = typeof value === "string" ? value.trim() : "";
83
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
84
+ throw new DbConnectionError("DB_INVALID_CONFIG", "environment variable name is invalid");
85
+ }
86
+ return name;
87
+ }
88
+ function requiredText(value, label) {
89
+ const text = typeof value === "string" ? value.trim() : "";
90
+ if (!text)
91
+ throw new DbConnectionError("DB_INVALID_CONFIG", label + " is required");
92
+ return text;
93
+ }
94
+ function normalizeConnectionInput(input) {
95
+ const name = requiredText(input.name, "connection name");
96
+ const engine = toDbEngine(input.engine);
97
+ const createdAt = typeof input.createdAt === "string" && !Number.isNaN(Date.parse(input.createdAt))
98
+ ? input.createdAt
99
+ : new Date().toISOString();
100
+ if (engine === "sqlite") {
101
+ const dbPath = typeof input.path === "string" ? input.path.trim() : "";
102
+ if (!dbPath || !path.isAbsolute(dbPath)) {
103
+ throw new DbConnectionError("DB_INVALID_CONFIG", "sqlite connection requires an absolute file path");
104
+ }
105
+ return { name, engine, path: dbPath, createdAt };
106
+ }
107
+ const mode = input.mode === "parameters" ? "parameters" : "url-env";
108
+ if (mode === "url-env") {
109
+ return { name, engine, mode, urlEnv: envName(input.urlEnv), createdAt };
110
+ }
111
+ const rawPort = input.port ?? (engine === "postgres" ? 5432 : 3306);
112
+ if (!Number.isInteger(rawPort) || rawPort < 1 || rawPort > 65535) {
113
+ throw new DbConnectionError("DB_INVALID_CONFIG", "port must be an integer between 1 and 65535");
114
+ }
115
+ const passwordMode = input.passwordMode === "environment"
116
+ ? "environment"
117
+ : input.passwordMode === "encrypted" ? "encrypted" : "runtime";
118
+ const passwordRef = passwordMode === "encrypted"
119
+ ? (typeof input.passwordRef === "string" && input.passwordRef.trim() ? input.passwordRef.trim() : credentialRefFor(name))
120
+ : undefined;
121
+ return {
122
+ name,
123
+ engine,
124
+ mode,
125
+ host: requiredText(input.host, "host"),
126
+ port: rawPort,
127
+ ...(typeof input.database === "string" && input.database.trim() ? { database: input.database.trim() } : {}),
128
+ username: requiredText(input.username, "username"),
129
+ passwordMode,
130
+ ...(passwordMode === "encrypted" ? { passwordRef } : {}),
131
+ ...(passwordMode === "environment" ? { passwordEnv: envName(input.passwordEnv) } : {}),
132
+ ssl: input.ssl === true,
133
+ createdAt,
134
+ };
135
+ }
136
+ /** Fail-closed SQL gate shared by every driver handle. */
137
+ export function assertDatabaseReadOnlySql(sql) {
138
+ let executable = sql
139
+ .replace(/--[^\r\n]*/g, " ")
140
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
141
+ .replace(/'(?:''|[^'])*'/g, "''")
142
+ .trim();
143
+ const statements = executable.split(";").map((part) => part.trim()).filter(Boolean);
144
+ if (statements.length !== 1)
145
+ throw new DbConnectionError("DB_READ_ONLY_VIOLATION", "database access is read-only; multiple SQL statements are forbidden");
146
+ executable = statements[0] ?? "";
147
+ const token = /^([A-Za-z_][A-Za-z0-9_]*)/.exec(executable)?.[1]?.toUpperCase() ?? "";
148
+ if (!["SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "WITH", "PRAGMA"].includes(token)) {
149
+ throw new DbConnectionError("DB_READ_ONLY_VIOLATION", `database access is read-only; ${token || "unknown"} statements are forbidden`);
150
+ }
151
+ const folded = executable.toUpperCase();
152
+ if (/\b(INSERT|UPDATE|DELETE|REPLACE|MERGE|CREATE|DROP|ALTER|TRUNCATE|GRANT|REVOKE|CALL|EXECUTE|COPY|LOAD|VACUUM|REINDEX|ATTACH|DETACH|BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE|SET|USE|INTO)\b/.test(folded) || /\bFOR\s+(UPDATE|SHARE)\b/.test(folded)) {
153
+ throw new DbConnectionError("DB_READ_ONLY_VIOLATION", "database access is read-only; write, DDL, transaction, locking, and privilege statements are forbidden");
154
+ }
155
+ if (token === "PRAGMA" && (/=/.test(executable) || /^PRAGMA\s+(JOURNAL_MODE|WAL_CHECKPOINT|OPTIMIZE|FOREIGN_KEYS|WRITABLE_SCHEMA)\b/i.test(executable))) {
156
+ throw new DbConnectionError("DB_READ_ONLY_VIOLATION", "database access is read-only; writable PRAGMA statements are forbidden");
157
+ }
158
+ }
159
+ /** Wrap node:sqlite DatabaseSync into the engine-neutral DbQueryHandle. */
160
+ function wrapSqlite(db) {
161
+ return {
162
+ prepare(sql) {
163
+ assertDatabaseReadOnlySql(sql);
164
+ return db.prepare(sql);
165
+ },
166
+ async query(sql) {
167
+ assertDatabaseReadOnlySql(sql);
168
+ return db.prepare(sql).all();
169
+ },
170
+ async run(sql) {
171
+ assertDatabaseReadOnlySql(sql);
172
+ const result = db.prepare(sql).run();
173
+ return typeof result.changes === "bigint"
174
+ ? Number(result.changes)
175
+ : Number(result.changes ?? 0);
176
+ },
177
+ close() {
178
+ try {
179
+ db.close();
180
+ }
181
+ catch {
182
+ // already closed — idempotent dispose
183
+ }
184
+ },
185
+ };
186
+ }
187
+ class LocalEncryptedCredentialVault {
188
+ root;
189
+ dataFile;
190
+ loaded;
191
+ constructor(root) {
192
+ this.root = root;
193
+ this.dataFile = path.join(root, "credentials.enc");
194
+ }
195
+ async key() {
196
+ await mkdir(this.root, { recursive: true });
197
+ const identity = [os.userInfo().username, os.hostname(), path.resolve(this.root)].join("\0");
198
+ return scryptSync(identity, "loop-agent-database-credentials-v1", 32);
199
+ }
200
+ async values() {
201
+ if (!this.loaded)
202
+ this.loaded = (async () => {
203
+ try {
204
+ const document = decryptCredentialDocument(await readFile(this.dataFile, "utf8"), await this.key());
205
+ return new Map(Object.entries(document.entries));
206
+ }
207
+ catch {
208
+ return new Map();
209
+ }
210
+ })();
211
+ return this.loaded;
212
+ }
213
+ async flush(values) {
214
+ const entries = Object.fromEntries(values);
215
+ const serialized = encryptCredentialDocument({ version: 1, entries }, await this.key());
216
+ const temp = this.dataFile + ".tmp";
217
+ await writeFile(temp, serialized, { mode: 0o600 });
218
+ await (await import("node:fs/promises")).rename(temp, this.dataFile);
219
+ this.loaded = Promise.resolve(new Map(values));
220
+ }
221
+ async set(ref, password) {
222
+ const values = await this.values();
223
+ values.set(ref, password);
224
+ await this.flush(values);
225
+ }
226
+ async get(ref) { return (await this.values()).get(ref); }
227
+ async delete(ref) {
228
+ const values = await this.values();
229
+ if (!values.delete(ref))
230
+ return;
231
+ await this.flush(values);
232
+ }
233
+ async clear() { this.loaded = Promise.resolve(new Map()); try {
234
+ await (await import("node:fs/promises")).unlink(this.dataFile);
235
+ }
236
+ catch { } }
237
+ }
238
+ export class DbConnectionRegistry {
239
+ root;
240
+ persistWriter;
241
+ driverLoader;
242
+ configs = new Map();
243
+ handles = new Map();
244
+ runtimePasswords = new Map();
245
+ credentialVault;
246
+ legacyCredentialVault;
247
+ legacyRoot;
248
+ loaded = false;
249
+ write;
250
+ mutationQueue = Promise.resolve();
251
+ constructor(root, persistWriter = writeSecureJson, driverLoader = (moduleId) => import(moduleId), legacyRoot) {
252
+ this.root = root;
253
+ this.persistWriter = persistWriter;
254
+ this.driverLoader = driverLoader;
255
+ this.legacyRoot = legacyRoot && path.resolve(legacyRoot) !== path.resolve(root) ? legacyRoot : undefined;
256
+ this.credentialVault = new LocalEncryptedCredentialVault(root);
257
+ this.legacyCredentialVault = this.legacyRoot ? new LocalEncryptedCredentialVault(this.legacyRoot) : undefined;
258
+ }
259
+ /** Wait for the latest persistence write to settle (tests / graceful stop). */
260
+ async flushed() {
261
+ await this.write;
262
+ }
263
+ async ensureLoaded() {
264
+ this.ensureLoadedSync();
265
+ }
266
+ /**
267
+ * Synchronous lazy loader for the name-based read APIs (list/get/add/
268
+ * remove): the persisted file is small JSON, so a sync read on first use
269
+ * keeps list()/get() synchronous. connect() keeps the async reader.
270
+ */
271
+ ensureLoadedSync(force = false) {
272
+ if (this.loaded && !force)
273
+ return;
274
+ const file = this.connectionsFile();
275
+ if (!existsSync(file) && this.legacyRoot) {
276
+ const legacyFile = path.join(this.legacyRoot, "connections.json");
277
+ if (existsSync(legacyFile)) {
278
+ try {
279
+ this.configs = normalizePersisted(JSON.parse(readFileSync(legacyFile, "utf8")));
280
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
281
+ writeFileSync(file, JSON.stringify([...this.configs.values()], null, 2) + "\n", { mode: 0o600 });
282
+ }
283
+ catch {
284
+ this.configs = new Map();
285
+ }
286
+ this.loaded = true;
287
+ return;
288
+ }
289
+ }
290
+ if (!existsSync(file)) {
291
+ this.configs = new Map();
292
+ this.loaded = true;
293
+ return;
294
+ }
295
+ try {
296
+ const raw = JSON.parse(readFileSync(file, "utf8"));
297
+ const next = normalizePersisted(raw);
298
+ for (const [name, current] of this.configs) {
299
+ const updated = next.get(name);
300
+ if (!updated || JSON.stringify(updated) !== JSON.stringify(current))
301
+ this.closeHandle(name);
302
+ }
303
+ this.configs = next;
304
+ }
305
+ catch {
306
+ // A concurrent atomic replace can make the file briefly unavailable.
307
+ // Keep the last valid snapshot instead of hiding every connection.
308
+ if (!this.loaded)
309
+ this.configs = new Map();
310
+ }
311
+ this.loaded = true;
312
+ }
313
+ connectionsFile() {
314
+ return path.join(this.root, "connections.json");
315
+ }
316
+ async persist(configs) {
317
+ const write = this.persistWriter(this.connectionsFile(), [...configs.values()]);
318
+ this.write = write;
319
+ try {
320
+ await write;
321
+ }
322
+ catch (error) {
323
+ throw new DbConnectionError("DB_PERSISTENCE_ERROR", `failed to save database connections: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
324
+ }
325
+ finally {
326
+ if (this.write === write)
327
+ this.write = undefined;
328
+ }
329
+ }
330
+ mutate(operation) {
331
+ const result = this.mutationQueue.then(operation, operation);
332
+ this.mutationQueue = result.then(() => undefined, () => undefined);
333
+ return result;
334
+ }
335
+ async add(input) {
336
+ return this.mutate(async () => {
337
+ this.ensureLoadedSync(true);
338
+ const config = normalizeConnectionInput(input);
339
+ if (config.mode === "parameters" && (config.passwordMode === "runtime" || config.passwordMode === "encrypted") && typeof input.password !== "string") {
340
+ throw new DbConnectionError("DB_INVALID_CONFIG", "password is required for a new password connection");
341
+ }
342
+ if (this.configs.has(config.name)) {
343
+ throw new DbConnectionError("DB_INVALID_CONFIG", `connection "${config.name}" already exists`);
344
+ }
345
+ const next = new Map(this.configs);
346
+ next.set(config.name, config);
347
+ if (config.mode === "parameters" && config.passwordMode === "encrypted" && typeof input.password === "string") {
348
+ await this.credentialVault.set(config.passwordRef, input.password);
349
+ }
350
+ await this.persist(next);
351
+ this.configs = next;
352
+ if (config.mode === "parameters" && config.passwordMode === "runtime" && typeof input.password === "string") {
353
+ this.runtimePasswords.set(config.name, input.password);
354
+ }
355
+ return this.summary(config);
356
+ });
357
+ }
358
+ async update(currentName, input) {
359
+ return this.mutate(async () => {
360
+ this.ensureLoadedSync(true);
361
+ const current = this.configs.get(currentName);
362
+ if (!current) {
363
+ throw new DbConnectionError("DB_CONNECTION_NOT_FOUND", `no database connection named "${currentName}"`);
364
+ }
365
+ const config = normalizeConnectionInput({
366
+ ...input,
367
+ ...(input.passwordMode === "encrypted" && typeof input.passwordRef !== "string" && current.passwordRef ? { passwordRef: current.passwordRef } : {}),
368
+ createdAt: current.createdAt,
369
+ });
370
+ if (config.name !== currentName && this.configs.has(config.name)) {
371
+ throw new DbConnectionError("DB_INVALID_CONFIG", `connection "${config.name}" already exists`);
372
+ }
373
+ if (config.mode === "parameters" && (config.passwordMode === "runtime" || config.passwordMode === "encrypted") && typeof input.password !== "string" && config.passwordMode === "runtime" && !this.runtimePasswords.has(currentName)) {
374
+ if (config.passwordMode === "runtime")
375
+ throw new DbConnectionError("DB_INVALID_CONFIG", "password must be entered again because no runtime password is available");
376
+ }
377
+ const next = new Map(this.configs);
378
+ next.delete(currentName);
379
+ next.set(config.name, config);
380
+ if (config.mode === "parameters" && config.passwordMode === "encrypted" && typeof input.password === "string") {
381
+ await this.credentialVault.set(config.passwordRef, input.password);
382
+ }
383
+ await this.persist(next);
384
+ if (current.passwordMode === "encrypted" && current.passwordRef && (config.passwordMode !== "encrypted" || config.passwordRef !== current.passwordRef)) {
385
+ await this.credentialVault.delete(current.passwordRef);
386
+ }
387
+ this.closeHandle(currentName);
388
+ const previousPassword = this.runtimePasswords.get(currentName);
389
+ this.runtimePasswords.delete(currentName);
390
+ if (config.mode === "parameters" && config.passwordMode === "runtime") {
391
+ if (typeof input.password === "string")
392
+ this.runtimePasswords.set(config.name, input.password);
393
+ else if (previousPassword !== undefined)
394
+ this.runtimePasswords.set(config.name, previousPassword);
395
+ }
396
+ this.configs = next;
397
+ return this.summary(config);
398
+ });
399
+ }
400
+ async remove(name) {
401
+ return this.mutate(async () => {
402
+ this.ensureLoadedSync(true);
403
+ const current = this.configs.get(name);
404
+ if (!current)
405
+ return false;
406
+ const next = new Map(this.configs);
407
+ next.delete(name);
408
+ await this.persist(next);
409
+ this.closeHandle(name);
410
+ this.runtimePasswords.delete(name);
411
+ if (current.passwordMode === "encrypted" && current.passwordRef)
412
+ await this.credentialVault.delete(current.passwordRef);
413
+ this.configs = next;
414
+ return true;
415
+ });
416
+ }
417
+ exportConfig() {
418
+ this.ensureLoadedSync(true);
419
+ return {
420
+ schemaVersion: 1,
421
+ exportedAt: new Date().toISOString(),
422
+ connections: [...this.configs.values()].map((config) => ({ ...config })),
423
+ };
424
+ }
425
+ async importConfig(input, conflict = "skip") {
426
+ return this.mutate(async () => {
427
+ this.ensureLoadedSync(true);
428
+ if (!isRecord(input) || input.schemaVersion !== 1 || !Array.isArray(input.connections)) {
429
+ throw new DbConnectionError("DB_INVALID_CONFIG", "invalid database connection export document");
430
+ }
431
+ const parsed = input.connections.map((entry) => {
432
+ if (!isRecord(entry) || typeof entry.engine !== "string") {
433
+ throw new DbConnectionError("DB_INVALID_CONFIG", "invalid connection entry in import document");
434
+ }
435
+ return normalizeConnectionInput({
436
+ name: typeof entry.name === "string" ? entry.name : "",
437
+ engine: toDbEngine(entry.engine),
438
+ ...(typeof entry.path === "string" ? { path: entry.path } : {}),
439
+ ...(entry.mode === "parameters" || entry.mode === "url-env" ? { mode: entry.mode } : {}),
440
+ ...(typeof entry.urlEnv === "string" ? { urlEnv: entry.urlEnv } : {}),
441
+ ...(typeof entry.host === "string" ? { host: entry.host } : {}),
442
+ ...(typeof entry.port === "number" ? { port: entry.port } : {}),
443
+ ...(typeof entry.database === "string" ? { database: entry.database } : {}),
444
+ ...(typeof entry.username === "string" ? { username: entry.username } : {}),
445
+ ...(entry.passwordMode === "runtime" || entry.passwordMode === "environment" || entry.passwordMode === "encrypted" ? { passwordMode: entry.passwordMode } : {}),
446
+ ...(typeof entry.passwordRef === "string" ? { passwordRef: entry.passwordRef } : {}),
447
+ ...(typeof entry.passwordEnv === "string" ? { passwordEnv: entry.passwordEnv } : {}),
448
+ ...(typeof entry.ssl === "boolean" ? { ssl: entry.ssl } : {}),
449
+ ...(typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {}),
450
+ });
451
+ });
452
+ const next = new Map(this.configs);
453
+ const handlesToClose = [];
454
+ let imported = 0;
455
+ let updated = 0;
456
+ let skipped = 0;
457
+ for (const config of parsed) {
458
+ if (next.has(config.name)) {
459
+ if (conflict === "skip") {
460
+ skipped += 1;
461
+ continue;
462
+ }
463
+ handlesToClose.push(config.name);
464
+ updated += 1;
465
+ }
466
+ else {
467
+ imported += 1;
468
+ }
469
+ next.set(config.name, config);
470
+ }
471
+ if (imported > 0 || updated > 0) {
472
+ await this.persist(next);
473
+ for (const name of handlesToClose) {
474
+ this.closeHandle(name);
475
+ this.runtimePasswords.delete(name);
476
+ }
477
+ this.configs = next;
478
+ }
479
+ return { imported, updated, skipped, total: parsed.length };
480
+ });
481
+ }
482
+ closeHandle(name) {
483
+ const handle = this.handles.get(name);
484
+ if (!handle)
485
+ return;
486
+ this.handles.delete(name);
487
+ handle.close();
488
+ }
489
+ summary(config) {
490
+ if (config.mode !== "parameters")
491
+ return { ...config };
492
+ const passwordStatus = config.passwordMode === "environment"
493
+ ? "environment"
494
+ : config.passwordMode === "encrypted" ? "encrypted"
495
+ : this.runtimePasswords.has(config.name) ? "runtime" : "required";
496
+ return { ...config, passwordStatus };
497
+ }
498
+ /** Sanitized summaries — never includes any env var or password value. */
499
+ list() {
500
+ this.ensureLoadedSync(true);
501
+ return [...this.configs.values()].map((config) => this.summary(config));
502
+ }
503
+ get(name) {
504
+ this.ensureLoadedSync(true);
505
+ const config = this.configs.get(name);
506
+ return config ? this.summary(config) : undefined;
507
+ }
508
+ /** Resolve configs lazily-loaded from disk before name-based lookups. */
509
+ async refresh() {
510
+ await this.ensureLoaded();
511
+ }
512
+ async listDatabases(name) {
513
+ const config = this.getConfig(name);
514
+ if (config.engine === "sqlite")
515
+ return ["main"];
516
+ // Catalog discovery must use a server-level connection. PostgreSQL servers
517
+ // commonly reject a database-less connection, so use the maintenance
518
+ // database explicitly; MySQL accepts a connection without a default schema.
519
+ const handle = await this.connect(name, config.engine === "postgres" ? "postgres" : undefined);
520
+ const rows = config.engine === "postgres"
521
+ ? await handle.query("SELECT datname AS name FROM pg_database WHERE datistemplate = false AND has_database_privilege(datname, 'CONNECT') ORDER BY datname")
522
+ : await handle.query("SHOW DATABASES");
523
+ return Array.from(new Set(rows.map((row) => String(row.name ?? row.Database ?? row.database ?? "").trim()).filter(Boolean)));
524
+ }
525
+ async listTables(name, selectedDatabase) {
526
+ const config = this.getConfig(name);
527
+ const effectiveDatabase = selectedDatabase?.trim() || config.database;
528
+ const handle = await this.connect(name, effectiveDatabase);
529
+ const sql = config.engine === "sqlite"
530
+ ? "SELECT name, type FROM sqlite_master WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name"
531
+ : config.engine === "postgres"
532
+ ? "SELECT table_name AS name, CASE WHEN table_type = 'VIEW' THEN 'view' ELSE 'table' END AS type FROM information_schema.tables WHERE table_schema = current_schema() ORDER BY table_name"
533
+ : effectiveDatabase
534
+ ? "SELECT table_name AS name, CASE WHEN table_type = 'VIEW' THEN 'view' ELSE 'table' END AS type, table_schema AS schemaName FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name"
535
+ : "SELECT table_name AS name, CASE WHEN table_type = 'VIEW' THEN 'view' ELSE 'table' END AS type, table_schema AS schemaName FROM information_schema.tables WHERE table_schema NOT IN ('information_schema','mysql','performance_schema','sys') ORDER BY table_schema, table_name";
536
+ const rows = await handle.query(sql);
537
+ return rows.map((row) => ({
538
+ name: String(row.name ?? ""),
539
+ type: String(row.type).toLowerCase() === "view" ? "view" : "table",
540
+ ...(typeof row.schemaName === "string" && !effectiveDatabase ? { schema: row.schemaName } : {}),
541
+ })).filter((table) => table.name !== "");
542
+ }
543
+ async browseTable(name, requestedTable, page = 1, pageSize = 50, selectedDatabase) {
544
+ const config = this.getConfig(name);
545
+ const tables = await this.listTables(name, selectedDatabase);
546
+ const table = tables.find((candidate) => (candidate.schema ? `${candidate.schema}.${candidate.name}` : candidate.name) === requestedTable);
547
+ if (!table)
548
+ throw new DbConnectionError("DB_INVALID_CONFIG", `table or view "${requestedTable}" does not exist`);
549
+ const safePage = Number.isInteger(page) && page > 0 ? page : 1;
550
+ const safePageSize = Number.isInteger(pageSize) ? Math.min(Math.max(pageSize, 1), 200) : 50;
551
+ const offset = (safePage - 1) * safePageSize;
552
+ const escapedName = config.engine === "mysql"
553
+ ? `\`${table.name.replaceAll("\`", "\`\`")}\``
554
+ : `"${table.name.replaceAll('"', '""')}"`;
555
+ const identifier = config.engine === "mysql" && table.schema
556
+ ? `\`${table.schema.replaceAll("\`", "\`\`")}\`.${escapedName}`
557
+ : escapedName;
558
+ const columns = await this.describeTable(config, table.name, identifier, table.schema, selectedDatabase);
559
+ const handle = await this.connect(name, selectedDatabase);
560
+ const rows = await handle.query(`SELECT * FROM ${identifier} LIMIT ${safePageSize + 1} OFFSET ${offset}`);
561
+ return {
562
+ connection: name,
563
+ table: requestedTable,
564
+ columns,
565
+ rows: rows.slice(0, safePageSize).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, typeof value === "bigint" ? value.toString() : value]))),
566
+ page: safePage,
567
+ pageSize: safePageSize,
568
+ hasNext: rows.length > safePageSize,
569
+ };
570
+ }
571
+ getConfig(name) {
572
+ this.ensureLoadedSync(true);
573
+ const config = this.configs.get(name);
574
+ if (!config)
575
+ throw new DbConnectionError("DB_CONNECTION_NOT_FOUND", `no database connection named "${name}"`);
576
+ return config;
577
+ }
578
+ async describeTable(config, table, identifier, schema, selectedDatabase) {
579
+ const handle = await this.connect(config.name, selectedDatabase);
580
+ if (config.engine === "sqlite") {
581
+ const rows = await handle.query(`PRAGMA table_info(${identifier})`);
582
+ return rows.map((row) => ({ name: String(row.name), type: String(row.type ?? ""), nullable: Number(row.notnull ?? 0) !== 1, primaryKey: Number(row.pk ?? 0) > 0 }));
583
+ }
584
+ if (config.engine === "postgres") {
585
+ const literal = table.replaceAll("'", "''");
586
+ const rows = await handle.query(`SELECT column_name AS name, data_type AS type, is_nullable AS nullable FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = '${literal}' ORDER BY ordinal_position`);
587
+ return rows.map((row) => ({ name: String(row.name), type: String(row.type ?? ""), nullable: String(row.nullable).toUpperCase() === "YES", primaryKey: false }));
588
+ }
589
+ const literal = table.replaceAll("'", "''");
590
+ const schemaFilter = schema ? `'${schema.replaceAll("'", "''")}'` : "DATABASE()";
591
+ const rows = await handle.query(`SELECT column_name AS name, column_type AS type, is_nullable AS nullable, column_key AS columnKey FROM information_schema.columns WHERE table_schema = ${schemaFilter} AND table_name = '${literal}' ORDER BY ordinal_position`);
592
+ return rows.map((row) => ({ name: String(row.name), type: String(row.type ?? ""), nullable: String(row.nullable).toUpperCase() === "YES", primaryKey: String(row.columnKey).toUpperCase() === "PRI" }));
593
+ }
594
+ async connect(name, databaseOverride) {
595
+ await this.ensureLoaded();
596
+ const cached = databaseOverride ? undefined : this.handles.get(name);
597
+ if (cached)
598
+ return cached;
599
+ const config = this.configs.get(name);
600
+ if (!config) {
601
+ throw new DbConnectionError("DB_CONNECTION_NOT_FOUND", `no database connection named "${name}"`);
602
+ }
603
+ const handle = await this.openHandle(config, databaseOverride);
604
+ if (!databaseOverride)
605
+ this.handles.set(name, handle);
606
+ return handle;
607
+ }
608
+ async openHandle(config, databaseOverride) {
609
+ if (config.engine === "sqlite") {
610
+ const { DatabaseSync } = await import("node:sqlite");
611
+ try {
612
+ // node:sqlite CREATES missing files by default — require an existing file.
613
+ if (!existsSync(config.path)) {
614
+ throw new DbConnectionError("DB_ENGINE_ERROR", `sqlite database file does not exist: ${config.path}`);
615
+ }
616
+ const db = new DatabaseSync(config.path, { timeout: 5_000 });
617
+ return wrapSqlite(db);
618
+ }
619
+ catch (error) {
620
+ if (error instanceof DbConnectionError)
621
+ throw error;
622
+ throw new DbConnectionError("DB_ENGINE_ERROR", `sqlite failed to open ${config.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
623
+ }
624
+ }
625
+ let connectionOptions;
626
+ if (config.mode === "parameters") {
627
+ let password;
628
+ if (config.passwordMode === "environment") {
629
+ password = config.passwordEnv ? process.env[config.passwordEnv] : undefined;
630
+ if (password === undefined) {
631
+ throw new DbConnectionError("DB_ENV_MISSING", `password environment variable ${config.passwordEnv} for connection "${config.name}" is not set`);
632
+ }
633
+ }
634
+ else if (config.passwordMode === "encrypted") {
635
+ password = config.passwordRef ? await this.credentialVault.get(config.passwordRef) : undefined;
636
+ if (password === undefined && this.legacyCredentialVault && config.passwordRef) {
637
+ password = await this.legacyCredentialVault.get(config.passwordRef);
638
+ if (password !== undefined)
639
+ await this.credentialVault.set(config.passwordRef, password);
640
+ }
641
+ if (password === undefined) {
642
+ throw new DbConnectionError("DB_ENV_MISSING", `encrypted password for connection "${config.name}" is not configured; edit the connection and enter it again`);
643
+ }
644
+ }
645
+ else {
646
+ password = this.runtimePasswords.get(config.name);
647
+ if (password === undefined) {
648
+ throw new DbConnectionError("DB_ENV_MISSING", `runtime password for connection "${config.name}" is required; edit the connection and enter it again`);
649
+ }
650
+ }
651
+ connectionOptions = {
652
+ host: config.host,
653
+ port: config.port,
654
+ ...(databaseOverride || config.database ? { database: databaseOverride || config.database } : {}),
655
+ user: config.username,
656
+ password,
657
+ ssl: config.ssl === true,
658
+ };
659
+ }
660
+ else {
661
+ const urlEnv = config.urlEnv;
662
+ const dsn = process.env[urlEnv];
663
+ if (!dsn || !dsn.trim()) {
664
+ throw new DbConnectionError("DB_ENV_MISSING", `environment variable ${urlEnv} (DSN for connection "${config.name}") is not set`);
665
+ }
666
+ connectionOptions = dsn;
667
+ }
668
+ const moduleId = config.engine === "postgres" ? "pg" : "mysql2/promise";
669
+ let driver;
670
+ try {
671
+ driver = await this.driverLoader(moduleId);
672
+ }
673
+ catch (error) {
674
+ throw new DbConnectionError("DB_ENGINE_UNAVAILABLE", `driver "${moduleId}" could not be loaded for connection "${config.name}"`, { cause: error });
675
+ }
676
+ try {
677
+ if (config.engine === "postgres") {
678
+ return await connectPostgres(driver, connectionOptions);
679
+ }
680
+ return await connectMysql(driver, connectionOptions);
681
+ }
682
+ catch (error) {
683
+ if (error instanceof DbConnectionError)
684
+ throw error;
685
+ throw new DbConnectionError("DB_ENGINE_ERROR", `${config.engine} connect failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
686
+ }
687
+ }
688
+ async test(name) {
689
+ const start = Date.now();
690
+ const handle = await this.connect(name);
691
+ const rows = await handle.query("SELECT 1 AS one");
692
+ if (!Array.isArray(rows) || rows.length < 1) {
693
+ throw new DbConnectionError("DB_ENGINE_ERROR", `connectivity probe for "${name}" returned no rows`);
694
+ }
695
+ return {
696
+ ok: true,
697
+ name,
698
+ engine: this.configs.get(name)?.engine ?? "sqlite",
699
+ elapsedMs: Date.now() - start,
700
+ };
701
+ }
702
+ dispose() {
703
+ for (const handle of this.handles.values())
704
+ handle.close();
705
+ this.handles.clear();
706
+ this.runtimePasswords.clear();
707
+ }
708
+ }
709
+ function credentialRefFor(name) {
710
+ return "local:" + createHash("sha256").update(name).digest("hex").slice(0, 24);
711
+ }
712
+ function encryptCredentialDocument(document, key) {
713
+ const iv = randomBytes(12);
714
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
715
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(document), "utf8"), cipher.final()]);
716
+ return JSON.stringify({ version: 1, iv: iv.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), ciphertext: ciphertext.toString("base64url") });
717
+ }
718
+ function decryptCredentialDocument(serialized, key) {
719
+ const raw = JSON.parse(serialized);
720
+ if (raw.version !== 1 || !raw.iv || !raw.tag || !raw.ciphertext)
721
+ throw new Error("invalid encrypted credential document");
722
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(raw.iv, "base64url"));
723
+ decipher.setAuthTag(Buffer.from(raw.tag, "base64url"));
724
+ const plaintext = Buffer.concat([decipher.update(Buffer.from(raw.ciphertext, "base64url")), decipher.final()]);
725
+ const document = JSON.parse(plaintext.toString("utf8"));
726
+ if (document.version !== 1 || !isRecord(document.entries))
727
+ throw new Error("invalid encrypted credential entries");
728
+ return document;
729
+ }
730
+ /** PostgreSQL driver adapter (`pg`). */
731
+ async function connectPostgres(driver, options) {
732
+ const mod = driver;
733
+ const PoolCtor = mod?.default?.Pool ?? mod?.Pool;
734
+ if (typeof PoolCtor !== "function") {
735
+ throw new DbConnectionError("DB_ENGINE_UNAVAILABLE", 'installed "pg" module does not expose a Pool constructor');
736
+ }
737
+ const poolConfig = typeof options === "string"
738
+ ? { connectionString: options }
739
+ : { host: options.host, port: options.port, ...(options.database ? { database: options.database } : {}), user: options.user, password: options.password, ssl: options.ssl };
740
+ const pool = new PoolCtor(poolConfig);
741
+ return {
742
+ async query(sql) {
743
+ assertDatabaseReadOnlySql(sql);
744
+ const result = await pool.query(sql);
745
+ return result.rows;
746
+ },
747
+ async run(sql) {
748
+ assertDatabaseReadOnlySql(sql);
749
+ const result = await pool.query(sql);
750
+ return result.rowCount ?? 0;
751
+ },
752
+ close() {
753
+ void pool.end().catch(() => undefined);
754
+ },
755
+ };
756
+ }
757
+ /** MySQL driver adapter (`mysql2/promise`). */
758
+ async function connectMysql(driver, options) {
759
+ const mod = driver;
760
+ const create = mod?.default
761
+ ?.createConnection ?? mod?.createConnection;
762
+ if (typeof create !== "function") {
763
+ throw new DbConnectionError("DB_ENGINE_UNAVAILABLE", 'installed "mysql2/promise" module does not expose createConnection');
764
+ }
765
+ const conn = await create(typeof options === "string" ? options : {
766
+ host: options.host,
767
+ port: options.port,
768
+ ...(options.database ? { database: options.database } : {}),
769
+ user: options.user,
770
+ password: options.password,
771
+ ...(options.ssl ? { ssl: {} } : {}),
772
+ });
773
+ return {
774
+ async query(sql) {
775
+ assertDatabaseReadOnlySql(sql);
776
+ const [rows] = await conn.query(sql);
777
+ return Array.isArray(rows) ? rows : [];
778
+ },
779
+ async run(sql) {
780
+ assertDatabaseReadOnlySql(sql);
781
+ const [result] = await conn.query(sql);
782
+ if (!Array.isArray(result) && typeof result.affectedRows === "number")
783
+ return result.affectedRows;
784
+ return Array.isArray(result) ? result.length : 0;
785
+ },
786
+ close() {
787
+ void conn.end().catch(() => undefined);
788
+ },
789
+ };
790
+ }