taurusdb-core 0.1.0

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 (170) hide show
  1. package/README.md +21 -0
  2. package/dist/auth/secret-resolver.d.ts +16 -0
  3. package/dist/auth/secret-resolver.js +64 -0
  4. package/dist/auth/sql-profile-loader/env-source.d.ts +3 -0
  5. package/dist/auth/sql-profile-loader/env-source.js +94 -0
  6. package/dist/auth/sql-profile-loader/file-source.d.ts +6 -0
  7. package/dist/auth/sql-profile-loader/file-source.js +40 -0
  8. package/dist/auth/sql-profile-loader/loader.d.ts +16 -0
  9. package/dist/auth/sql-profile-loader/loader.js +81 -0
  10. package/dist/auth/sql-profile-loader/parsing.d.ts +14 -0
  11. package/dist/auth/sql-profile-loader/parsing.js +216 -0
  12. package/dist/auth/sql-profile-loader/runtime-override.d.ts +14 -0
  13. package/dist/auth/sql-profile-loader/runtime-override.js +52 -0
  14. package/dist/auth/sql-profile-loader/types.d.ts +64 -0
  15. package/dist/auth/sql-profile-loader/types.js +1 -0
  16. package/dist/auth/sql-profile-loader.d.ts +4 -0
  17. package/dist/auth/sql-profile-loader.js +3 -0
  18. package/dist/capability/feature-matrix.d.ts +5 -0
  19. package/dist/capability/feature-matrix.js +237 -0
  20. package/dist/capability/probe.d.ts +19 -0
  21. package/dist/capability/probe.js +139 -0
  22. package/dist/capability/types.d.ts +49 -0
  23. package/dist/capability/types.js +16 -0
  24. package/dist/capability/version.d.ts +3 -0
  25. package/dist/capability/version.js +47 -0
  26. package/dist/cloud/auth.d.ts +26 -0
  27. package/dist/cloud/auth.js +198 -0
  28. package/dist/cloud/instances.d.ts +46 -0
  29. package/dist/cloud/instances.js +224 -0
  30. package/dist/config/env.d.ts +1 -0
  31. package/dist/config/env.js +194 -0
  32. package/dist/config/index.d.ts +6 -0
  33. package/dist/config/index.js +21 -0
  34. package/dist/config/redaction.d.ts +2 -0
  35. package/dist/config/redaction.js +19 -0
  36. package/dist/config/schema.d.ts +417 -0
  37. package/dist/config/schema.js +100 -0
  38. package/dist/context/datasource-resolver.d.ts +19 -0
  39. package/dist/context/datasource-resolver.js +71 -0
  40. package/dist/context/session-context.d.ts +26 -0
  41. package/dist/context/session-context.js +1 -0
  42. package/dist/diagnostics/metrics-source.d.ts +65 -0
  43. package/dist/diagnostics/metrics-source.js +280 -0
  44. package/dist/diagnostics/slow-sql-source/das-source.d.ts +43 -0
  45. package/dist/diagnostics/slow-sql-source/das-source.js +170 -0
  46. package/dist/diagnostics/slow-sql-source/factory.d.ts +5 -0
  47. package/dist/diagnostics/slow-sql-source/factory.js +87 -0
  48. package/dist/diagnostics/slow-sql-source/parsers.d.ts +7 -0
  49. package/dist/diagnostics/slow-sql-source/parsers.js +125 -0
  50. package/dist/diagnostics/slow-sql-source/taurus-api-source.d.ts +42 -0
  51. package/dist/diagnostics/slow-sql-source/taurus-api-source.js +149 -0
  52. package/dist/diagnostics/slow-sql-source/types.d.ts +40 -0
  53. package/dist/diagnostics/slow-sql-source/types.js +1 -0
  54. package/dist/diagnostics/slow-sql-source/utils.d.ts +20 -0
  55. package/dist/diagnostics/slow-sql-source/utils.js +170 -0
  56. package/dist/diagnostics/slow-sql-source.d.ts +4 -0
  57. package/dist/diagnostics/slow-sql-source.js +3 -0
  58. package/dist/diagnostics/types.d.ts +189 -0
  59. package/dist/diagnostics/types.js +39 -0
  60. package/dist/engine/data-access/locks.d.ts +8 -0
  61. package/dist/engine/data-access/locks.js +146 -0
  62. package/dist/engine/data-access/processlist.d.ts +4 -0
  63. package/dist/engine/data-access/processlist.js +56 -0
  64. package/dist/engine/data-access/statements.d.ts +10 -0
  65. package/dist/engine/data-access/statements.js +203 -0
  66. package/dist/engine/data-access/storage.d.ts +6 -0
  67. package/dist/engine/data-access/storage.js +96 -0
  68. package/dist/engine/data-access.d.ts +4 -0
  69. package/dist/engine/data-access.js +4 -0
  70. package/dist/engine/diagnostics.d.ts +7 -0
  71. package/dist/engine/diagnostics.js +7 -0
  72. package/dist/engine/helper-modules/diagnostics.d.ts +57 -0
  73. package/dist/engine/helper-modules/diagnostics.js +322 -0
  74. package/dist/engine/helper-modules/parsers.d.ts +13 -0
  75. package/dist/engine/helper-modules/parsers.js +283 -0
  76. package/dist/engine/helper-modules/sql.d.ts +12 -0
  77. package/dist/engine/helper-modules/sql.js +119 -0
  78. package/dist/engine/helper-modules/types.d.ts +103 -0
  79. package/dist/engine/helper-modules/types.js +1 -0
  80. package/dist/engine/helpers.d.ts +4 -0
  81. package/dist/engine/helpers.js +4 -0
  82. package/dist/engine/runtime.d.ts +20 -0
  83. package/dist/engine/runtime.js +385 -0
  84. package/dist/engine/types.d.ts +125 -0
  85. package/dist/engine/types.js +1 -0
  86. package/dist/engine/workflows/connection-spike.d.ts +4 -0
  87. package/dist/engine/workflows/connection-spike.js +316 -0
  88. package/dist/engine/workflows/db-hotspot.d.ts +4 -0
  89. package/dist/engine/workflows/db-hotspot.js +182 -0
  90. package/dist/engine/workflows/lock-contention-helpers/entities.d.ts +9 -0
  91. package/dist/engine/workflows/lock-contention-helpers/entities.js +58 -0
  92. package/dist/engine/workflows/lock-contention-helpers/no-match.d.ts +3 -0
  93. package/dist/engine/workflows/lock-contention-helpers/no-match.js +65 -0
  94. package/dist/engine/workflows/lock-contention-helpers/report.d.ts +21 -0
  95. package/dist/engine/workflows/lock-contention-helpers/report.js +104 -0
  96. package/dist/engine/workflows/lock-contention-helpers/root-cause.d.ts +4 -0
  97. package/dist/engine/workflows/lock-contention-helpers/root-cause.js +79 -0
  98. package/dist/engine/workflows/lock-contention-helpers/signals.d.ts +22 -0
  99. package/dist/engine/workflows/lock-contention-helpers/signals.js +34 -0
  100. package/dist/engine/workflows/lock-contention-helpers.d.ts +5 -0
  101. package/dist/engine/workflows/lock-contention-helpers.js +5 -0
  102. package/dist/engine/workflows/lock-contention.d.ts +4 -0
  103. package/dist/engine/workflows/lock-contention.js +67 -0
  104. package/dist/engine/workflows/service-latency.d.ts +4 -0
  105. package/dist/engine/workflows/service-latency.js +262 -0
  106. package/dist/engine/workflows/slow-query-helpers.d.ts +41 -0
  107. package/dist/engine/workflows/slow-query-helpers.js +253 -0
  108. package/dist/engine/workflows/slow-query.d.ts +4 -0
  109. package/dist/engine/workflows/slow-query.js +156 -0
  110. package/dist/engine/workflows/storage-pressure-helpers.d.ts +12 -0
  111. package/dist/engine/workflows/storage-pressure-helpers.js +281 -0
  112. package/dist/engine/workflows/storage-pressure.d.ts +4 -0
  113. package/dist/engine/workflows/storage-pressure.js +27 -0
  114. package/dist/engine/workflows/top-slow-sql.d.ts +4 -0
  115. package/dist/engine/workflows/top-slow-sql.js +222 -0
  116. package/dist/engine.d.ts +77 -0
  117. package/dist/engine.js +240 -0
  118. package/dist/executor/adapters/mysql.d.ts +2 -0
  119. package/dist/executor/adapters/mysql.js +114 -0
  120. package/dist/executor/connection-pool.d.ts +105 -0
  121. package/dist/executor/connection-pool.js +236 -0
  122. package/dist/executor/explain.d.ts +5 -0
  123. package/dist/executor/explain.js +119 -0
  124. package/dist/executor/query-tracker.d.ts +45 -0
  125. package/dist/executor/query-tracker.js +83 -0
  126. package/dist/executor/result-normalizer.d.ts +6 -0
  127. package/dist/executor/result-normalizer.js +47 -0
  128. package/dist/executor/sql-executor.d.ts +32 -0
  129. package/dist/executor/sql-executor.js +250 -0
  130. package/dist/executor/types.d.ts +70 -0
  131. package/dist/executor/types.js +1 -0
  132. package/dist/index.d.ts +40 -0
  133. package/dist/index.js +21 -0
  134. package/dist/safety/confirmation-store.d.ts +44 -0
  135. package/dist/safety/confirmation-store.js +130 -0
  136. package/dist/safety/guardrail.d.ts +39 -0
  137. package/dist/safety/guardrail.js +99 -0
  138. package/dist/safety/parser/adapter.d.ts +10 -0
  139. package/dist/safety/parser/adapter.js +72 -0
  140. package/dist/safety/parser/ast-utils.d.ts +10 -0
  141. package/dist/safety/parser/ast-utils.js +167 -0
  142. package/dist/safety/parser/features.d.ts +12 -0
  143. package/dist/safety/parser/features.js +113 -0
  144. package/dist/safety/parser/index.d.ts +2 -0
  145. package/dist/safety/parser/index.js +1 -0
  146. package/dist/safety/parser/types.d.ts +76 -0
  147. package/dist/safety/parser/types.js +1 -0
  148. package/dist/safety/redaction.d.ts +34 -0
  149. package/dist/safety/redaction.js +186 -0
  150. package/dist/safety/sql-classifier.d.ts +19 -0
  151. package/dist/safety/sql-classifier.js +43 -0
  152. package/dist/safety/sql-validator.d.ts +19 -0
  153. package/dist/safety/sql-validator.js +143 -0
  154. package/dist/schema/adapters/mysql.d.ts +16 -0
  155. package/dist/schema/adapters/mysql.js +287 -0
  156. package/dist/schema/introspector.d.ts +70 -0
  157. package/dist/schema/introspector.js +40 -0
  158. package/dist/taurus/flashback.d.ts +36 -0
  159. package/dist/taurus/flashback.js +149 -0
  160. package/dist/taurus/recycle-bin.d.ts +14 -0
  161. package/dist/taurus/recycle-bin.js +61 -0
  162. package/dist/utils/formatter.d.ts +70 -0
  163. package/dist/utils/formatter.js +60 -0
  164. package/dist/utils/hash.d.ts +2 -0
  165. package/dist/utils/hash.js +247 -0
  166. package/dist/utils/id.d.ts +2 -0
  167. package/dist/utils/id.js +11 -0
  168. package/dist/utils/logger.d.ts +9 -0
  169. package/dist/utils/logger.js +39 -0
  170. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # taurusdb-core
2
+
3
+ Shared TaurusDB data-plane engine for:
4
+
5
+ - schema discovery
6
+ - readonly and mutation SQL execution
7
+ - SQL guardrails and confirmation flow
8
+ - TaurusDB capability detection
9
+ - diagnostics workflows
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install taurusdb-core
15
+ ```
16
+
17
+ ## Notes
18
+
19
+ - Requires Node.js `>= 20`
20
+ - This is the shared engine package
21
+ - Most end users should install `taurusdb-mcp` instead
@@ -0,0 +1,16 @@
1
+ import type { CredentialRef } from "./sql-profile-loader.js";
2
+ export interface SecretResolver {
3
+ resolve(ref: CredentialRef): Promise<string>;
4
+ }
5
+ export type UriSecretResolver = (uri: string) => Promise<string>;
6
+ export type SecretResolverOptions = {
7
+ env?: NodeJS.ProcessEnv;
8
+ uriResolvers?: Record<string, UriSecretResolver>;
9
+ };
10
+ export declare class DefaultSecretResolver implements SecretResolver {
11
+ private readonly env;
12
+ private readonly uriResolvers;
13
+ constructor(options?: SecretResolverOptions);
14
+ resolve(ref: CredentialRef): Promise<string>;
15
+ }
16
+ export declare function createSecretResolver(options?: SecretResolverOptions): SecretResolver;
@@ -0,0 +1,64 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ function resolveFilePath(rawPath) {
5
+ if (rawPath === "~") {
6
+ return os.homedir();
7
+ }
8
+ if (rawPath.startsWith("~/")) {
9
+ return path.join(os.homedir(), rawPath.slice(2));
10
+ }
11
+ if (path.isAbsolute(rawPath)) {
12
+ return rawPath;
13
+ }
14
+ return path.resolve(process.cwd(), rawPath);
15
+ }
16
+ function sanitizeFileSecret(raw) {
17
+ return raw.replace(/\uFEFF/g, "").replace(/\r?\n$/, "");
18
+ }
19
+ function parseUri(input) {
20
+ const trimmed = input.trim();
21
+ const normalizedUri = trimmed.startsWith("uri:") ? trimmed.slice("uri:".length) : trimmed;
22
+ const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(normalizedUri);
23
+ if (!match) {
24
+ throw new Error(`Invalid URI credential ref: "${input}".`);
25
+ }
26
+ return {
27
+ scheme: match[1].toLowerCase(),
28
+ normalizedUri,
29
+ };
30
+ }
31
+ export class DefaultSecretResolver {
32
+ env;
33
+ uriResolvers;
34
+ constructor(options = {}) {
35
+ this.env = options.env ?? process.env;
36
+ this.uriResolvers = options.uriResolvers ?? {};
37
+ }
38
+ async resolve(ref) {
39
+ if (ref.type === "plain") {
40
+ return ref.value;
41
+ }
42
+ if (ref.type === "env") {
43
+ const value = this.env[ref.key];
44
+ if (value === undefined) {
45
+ throw new Error(`Environment variable not found for credential ref: ${ref.key}`);
46
+ }
47
+ return value;
48
+ }
49
+ if (ref.type === "file") {
50
+ const targetPath = resolveFilePath(ref.path);
51
+ const raw = await readFile(targetPath, "utf-8");
52
+ return sanitizeFileSecret(raw);
53
+ }
54
+ const { scheme, normalizedUri } = parseUri(ref.uri);
55
+ const resolver = this.uriResolvers[scheme];
56
+ if (!resolver) {
57
+ throw new Error(`Unsupported credential URI scheme: ${scheme}. Add a uri resolver for "${scheme}" in SecretResolver.`);
58
+ }
59
+ return resolver(normalizedUri);
60
+ }
61
+ }
62
+ export function createSecretResolver(options) {
63
+ return new DefaultSecretResolver(options);
64
+ }
@@ -0,0 +1,3 @@
1
+ import type { DataSourceProfile, DatabaseEngine } from "./types.js";
2
+ export declare function parseEngineFromDsnProtocol(protocol: string): DatabaseEngine;
3
+ export declare function parseEnvProfile(env: NodeJS.ProcessEnv): DataSourceProfile | undefined;
@@ -0,0 +1,94 @@
1
+ import { asInteger, asString, defaultPortForEngine, parseCredentialRef, parseEngine, withRedactedToString } from "./parsing.js";
2
+ export function parseEngineFromDsnProtocol(protocol) {
3
+ const normalized = protocol.replace(/:$/, "").toLowerCase();
4
+ if (normalized === "mysql" || normalized === "mysql2") {
5
+ return "mysql";
6
+ }
7
+ if (normalized === "postgres" || normalized === "postgresql") {
8
+ return "postgresql";
9
+ }
10
+ throw new Error(`Unsupported DSN protocol: ${protocol}`);
11
+ }
12
+ export function parseEnvProfile(env) {
13
+ const dsn = asString(env.TAURUSDB_SQL_DSN);
14
+ const explicitHost = asString(env.TAURUSDB_SQL_HOST);
15
+ const profileName = asString(env.TAURUSDB_SQL_DATASOURCE) ?? "taurus_mcp";
16
+ let engine;
17
+ let host;
18
+ let port;
19
+ let database;
20
+ let readonlyUsername;
21
+ let readonlyPasswordRef;
22
+ if (dsn) {
23
+ const url = new URL(dsn);
24
+ engine = parseEngineFromDsnProtocol(url.protocol);
25
+ host = url.hostname;
26
+ port = url.port ? Number.parseInt(url.port, 10) : defaultPortForEngine(engine);
27
+ database = asString(url.pathname.replace(/^\//, ""));
28
+ readonlyUsername = asString(decodeURIComponent(url.username));
29
+ const dsnPassword = asString(decodeURIComponent(url.password));
30
+ if (dsnPassword) {
31
+ readonlyPasswordRef = parseCredentialRef(dsnPassword, "TAURUSDB_SQL_DSN.password");
32
+ }
33
+ }
34
+ else {
35
+ engine = parseEngine(asString(env.TAURUSDB_SQL_ENGINE) ?? "mysql", "TAURUSDB_SQL_ENGINE");
36
+ host = explicitHost;
37
+ const explicitPort = asInteger(env.TAURUSDB_SQL_PORT);
38
+ port = explicitPort ?? defaultPortForEngine(engine);
39
+ database = asString(env.TAURUSDB_SQL_DATABASE);
40
+ }
41
+ const mutationUserName = asString(env.TAURUSDB_SQL_MUTATION_USER);
42
+ const mutationPasswordRaw = asString(env.TAURUSDB_SQL_MUTATION_PASSWORD);
43
+ if (!dsn &&
44
+ !explicitHost &&
45
+ !asString(env.TAURUSDB_SQL_USER) &&
46
+ !asString(env.TAURUSDB_SQL_PASSWORD) &&
47
+ !database &&
48
+ !mutationUserName &&
49
+ !mutationPasswordRaw) {
50
+ return undefined;
51
+ }
52
+ if (!port || !Number.isFinite(port) || port <= 0) {
53
+ throw new Error("Failed to resolve SQL port from environment.");
54
+ }
55
+ readonlyUsername = readonlyUsername ?? asString(env.TAURUSDB_SQL_USER);
56
+ if (!readonlyUsername) {
57
+ throw new Error("Missing readonly username in environment. Set TAURUSDB_SQL_USER or include it in DSN.");
58
+ }
59
+ readonlyPasswordRef =
60
+ readonlyPasswordRef ??
61
+ (Object.hasOwn(env, "TAURUSDB_SQL_PASSWORD")
62
+ ? parseCredentialRef(env.TAURUSDB_SQL_PASSWORD, "TAURUSDB_SQL_PASSWORD")
63
+ : undefined);
64
+ if (!readonlyPasswordRef) {
65
+ throw new Error("Missing readonly password in environment. Set TAURUSDB_SQL_PASSWORD or include it in DSN.");
66
+ }
67
+ let mutationUser;
68
+ if (mutationUserName || mutationPasswordRaw) {
69
+ if (!mutationUserName || !mutationPasswordRaw) {
70
+ throw new Error("Invalid mutation credentials in environment: TAURUSDB_SQL_MUTATION_USER and TAURUSDB_SQL_MUTATION_PASSWORD must be set together.");
71
+ }
72
+ mutationUser = {
73
+ username: mutationUserName,
74
+ password: parseCredentialRef(mutationPasswordRaw, "TAURUSDB_SQL_MUTATION_PASSWORD"),
75
+ };
76
+ }
77
+ const poolSize = asInteger(env.TAURUSDB_SQL_POOL_SIZE);
78
+ if (poolSize !== undefined && poolSize <= 0) {
79
+ throw new Error("Invalid TAURUSDB_SQL_POOL_SIZE: must be positive.");
80
+ }
81
+ return withRedactedToString({
82
+ name: profileName,
83
+ engine,
84
+ host,
85
+ port,
86
+ database,
87
+ readonlyUser: {
88
+ username: readonlyUsername,
89
+ password: readonlyPasswordRef,
90
+ },
91
+ mutationUser,
92
+ poolSize,
93
+ });
94
+ }
@@ -0,0 +1,6 @@
1
+ import type { DataSourceProfile } from "./types.js";
2
+ export type FileProfilesPayload = {
3
+ profiles: Map<string, DataSourceProfile>;
4
+ defaultDatasource?: string;
5
+ };
6
+ export declare function parseProfilesFile(raw: string, filePath: string): FileProfilesPayload;
@@ -0,0 +1,40 @@
1
+ import { asString, isObject, parseProfileRecord } from "./parsing.js";
2
+ export function parseProfilesFile(raw, filePath) {
3
+ let parsed;
4
+ try {
5
+ parsed = JSON.parse(raw);
6
+ }
7
+ catch (error) {
8
+ throw new Error(`Invalid JSON in profiles file: ${filePath}`, { cause: error });
9
+ }
10
+ if (!isObject(parsed)) {
11
+ throw new Error(`Invalid profiles file root in ${filePath}: expected object.`);
12
+ }
13
+ const root = parsed;
14
+ const defaultDatasource = asString(root.defaultDatasource ?? root.default);
15
+ const profileNode = root.dataSources ?? root.datasources ?? root.profiles;
16
+ const profiles = new Map();
17
+ if (profileNode === undefined) {
18
+ return { profiles, defaultDatasource };
19
+ }
20
+ if (Array.isArray(profileNode)) {
21
+ for (const item of profileNode) {
22
+ if (!isObject(item)) {
23
+ throw new Error(`Invalid profile item in ${filePath}: expected object.`);
24
+ }
25
+ const name = asString(item.name);
26
+ if (!name) {
27
+ throw new Error(`Invalid profile item in ${filePath}: missing name.`);
28
+ }
29
+ profiles.set(name, parseProfileRecord(name, item, filePath));
30
+ }
31
+ return { profiles, defaultDatasource };
32
+ }
33
+ if (!isObject(profileNode)) {
34
+ throw new Error(`Invalid profiles node in ${filePath}: expected object or array.`);
35
+ }
36
+ for (const [name, profileValue] of Object.entries(profileNode)) {
37
+ profiles.set(name, parseProfileRecord(name, profileValue, filePath));
38
+ }
39
+ return { profiles, defaultDatasource };
40
+ }
@@ -0,0 +1,16 @@
1
+ import type { Config } from "../../config/index.js";
2
+ import type { DataSourceProfile, ProfileLoader, SqlProfileLoaderOptions } from "./types.js";
3
+ export declare function resolveDefaultProfilePath(config: Config): string;
4
+ export declare class SqlProfileLoader implements ProfileLoader {
5
+ private readonly config;
6
+ private readonly env;
7
+ private cache;
8
+ private pending;
9
+ constructor(options: SqlProfileLoaderOptions);
10
+ load(): Promise<Map<string, DataSourceProfile>>;
11
+ getDefault(): Promise<string | undefined>;
12
+ get(name: string): Promise<DataSourceProfile | undefined>;
13
+ private ensureLoaded;
14
+ private loadInternal;
15
+ }
16
+ export declare function createSqlProfileLoader(options: SqlProfileLoaderOptions): ProfileLoader;
@@ -0,0 +1,81 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { parseEnvProfile } from "./env-source.js";
5
+ import { parseProfilesFile } from "./file-source.js";
6
+ export function resolveDefaultProfilePath(config) {
7
+ if (config.profilesPath) {
8
+ return config.profilesPath;
9
+ }
10
+ return path.join(os.homedir(), ".config", "taurusdb-mcp", "profiles.json");
11
+ }
12
+ async function exists(filePath) {
13
+ try {
14
+ await access(filePath);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export class SqlProfileLoader {
22
+ config;
23
+ env;
24
+ cache;
25
+ pending;
26
+ constructor(options) {
27
+ this.config = options.config;
28
+ this.env = options.env ?? process.env;
29
+ }
30
+ async load() {
31
+ const loaded = await this.ensureLoaded();
32
+ return new Map(loaded.profiles);
33
+ }
34
+ async getDefault() {
35
+ const loaded = await this.ensureLoaded();
36
+ return loaded.defaultDatasource;
37
+ }
38
+ async get(name) {
39
+ const loaded = await this.ensureLoaded();
40
+ return loaded.profiles.get(name);
41
+ }
42
+ async ensureLoaded() {
43
+ if (this.cache) {
44
+ return this.cache;
45
+ }
46
+ if (!this.pending) {
47
+ this.pending = this.loadInternal();
48
+ }
49
+ const loaded = await this.pending;
50
+ this.cache = loaded;
51
+ this.pending = undefined;
52
+ return loaded;
53
+ }
54
+ async loadInternal() {
55
+ const mergedProfiles = new Map();
56
+ const envProfile = parseEnvProfile(this.env);
57
+ if (envProfile) {
58
+ mergedProfiles.set(envProfile.name, envProfile);
59
+ }
60
+ const profilePath = resolveDefaultProfilePath(this.config);
61
+ let fileDefaultDatasource;
62
+ if (await exists(profilePath)) {
63
+ const content = await readFile(profilePath, "utf-8");
64
+ const parsed = parseProfilesFile(content, profilePath);
65
+ fileDefaultDatasource = parsed.defaultDatasource;
66
+ for (const [name, profile] of parsed.profiles.entries()) {
67
+ mergedProfiles.set(name, profile);
68
+ }
69
+ }
70
+ const defaultDatasource = this.config.defaultDatasource ??
71
+ fileDefaultDatasource ??
72
+ (mergedProfiles.size === 1 ? mergedProfiles.keys().next().value : undefined);
73
+ return {
74
+ profiles: mergedProfiles,
75
+ defaultDatasource,
76
+ };
77
+ }
78
+ }
79
+ export function createSqlProfileLoader(options) {
80
+ return new SqlProfileLoader(options);
81
+ }
@@ -0,0 +1,14 @@
1
+ import type { CredentialRef, DataSourceProfile, DatabaseEngine, TlsOptions, UserCredential } from "./types.js";
2
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
3
+ export declare function asString(value: unknown): string | undefined;
4
+ export declare function asInteger(value: unknown): number | undefined;
5
+ export declare function asBoolean(value: unknown): boolean | undefined;
6
+ export declare function defaultPortForEngine(engine: DatabaseEngine): number;
7
+ export declare function parseEngine(value: unknown, context: string): DatabaseEngine;
8
+ export declare function parseCredentialRef(value: unknown, context: string): CredentialRef;
9
+ export declare function parseUserCredential(value: unknown, context: string): UserCredential;
10
+ export declare function parseTlsOptions(value: unknown, context: string): TlsOptions | undefined;
11
+ export declare function deepRedact(value: unknown): unknown;
12
+ export declare function redactDataSourceProfile(profile: DataSourceProfile): Record<string, unknown>;
13
+ export declare function withRedactedToString(profile: Omit<DataSourceProfile, "toString">): DataSourceProfile;
14
+ export declare function parseProfileRecord(name: string, value: unknown, context: string): DataSourceProfile;
@@ -0,0 +1,216 @@
1
+ const CREDENTIAL_URI_PREFIXES = ["aws-sm:", "hw-kms:", "uri:"];
2
+ const SENSITIVE_KEY_PATTERN = /(password|secret|token|credential|apikey|api_key)/i;
3
+ export function isObject(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+ export function asString(value) {
7
+ if (typeof value !== "string") {
8
+ return undefined;
9
+ }
10
+ const trimmed = value.trim();
11
+ return trimmed.length > 0 ? trimmed : undefined;
12
+ }
13
+ export function asInteger(value) {
14
+ if (typeof value === "number" && Number.isInteger(value)) {
15
+ return value;
16
+ }
17
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
18
+ return Number.parseInt(value, 10);
19
+ }
20
+ return undefined;
21
+ }
22
+ export function asBoolean(value) {
23
+ if (typeof value === "boolean") {
24
+ return value;
25
+ }
26
+ if (typeof value === "string") {
27
+ const normalized = value.trim().toLowerCase();
28
+ if (["1", "true", "yes", "on"].includes(normalized)) {
29
+ return true;
30
+ }
31
+ if (["0", "false", "no", "off"].includes(normalized)) {
32
+ return false;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+ export function defaultPortForEngine(engine) {
38
+ return engine === "postgresql" ? 5432 : 3306;
39
+ }
40
+ export function parseEngine(value, context) {
41
+ const normalized = asString(value)?.toLowerCase();
42
+ if (normalized === "mysql") {
43
+ return "mysql";
44
+ }
45
+ if (normalized === "postgresql" || normalized === "postgres" || normalized === "pg") {
46
+ return "postgresql";
47
+ }
48
+ throw new Error(`Invalid engine in ${context}. Expected mysql or postgresql.`);
49
+ }
50
+ export function parseCredentialRef(value, context) {
51
+ if (typeof value === "string") {
52
+ if (value.startsWith("env:")) {
53
+ const key = value.slice("env:".length).trim();
54
+ if (!key) {
55
+ throw new Error(`Invalid env credential ref in ${context}: missing key.`);
56
+ }
57
+ return { type: "env", key };
58
+ }
59
+ if (value.startsWith("file:")) {
60
+ const filePath = value.slice("file:".length).trim();
61
+ if (!filePath) {
62
+ throw new Error(`Invalid file credential ref in ${context}: missing path.`);
63
+ }
64
+ return { type: "file", path: filePath };
65
+ }
66
+ if (CREDENTIAL_URI_PREFIXES.some((prefix) => value.startsWith(prefix))) {
67
+ return { type: "uri", uri: value };
68
+ }
69
+ return { type: "plain", value };
70
+ }
71
+ if (!isObject(value)) {
72
+ throw new Error(`Invalid credential ref in ${context}: expected string or object.`);
73
+ }
74
+ const type = asString(value.type);
75
+ if (type === "plain") {
76
+ const plain = asString(value.value);
77
+ if (!plain) {
78
+ throw new Error(`Invalid plain credential ref in ${context}: missing value.`);
79
+ }
80
+ return { type: "plain", value: plain };
81
+ }
82
+ if (type === "env") {
83
+ const key = asString(value.key);
84
+ if (!key) {
85
+ throw new Error(`Invalid env credential ref in ${context}: missing key.`);
86
+ }
87
+ return { type: "env", key };
88
+ }
89
+ if (type === "file") {
90
+ const filePath = asString(value.path);
91
+ if (!filePath) {
92
+ throw new Error(`Invalid file credential ref in ${context}: missing path.`);
93
+ }
94
+ return { type: "file", path: filePath };
95
+ }
96
+ if (type === "uri") {
97
+ const uri = asString(value.uri);
98
+ if (!uri) {
99
+ throw new Error(`Invalid uri credential ref in ${context}: missing uri.`);
100
+ }
101
+ return { type: "uri", uri };
102
+ }
103
+ throw new Error(`Invalid credential ref type in ${context}.`);
104
+ }
105
+ export function parseUserCredential(value, context) {
106
+ if (!isObject(value)) {
107
+ throw new Error(`Invalid user credential in ${context}: expected object.`);
108
+ }
109
+ const username = asString(value.username ?? value.user);
110
+ if (!username) {
111
+ throw new Error(`Invalid user credential in ${context}: missing username.`);
112
+ }
113
+ if (!Object.hasOwn(value, "password") && !Object.hasOwn(value, "pass")) {
114
+ throw new Error(`Invalid user credential in ${context}: missing password.`);
115
+ }
116
+ const password = parseCredentialRef(value.password ?? value.pass, `${context}.password`);
117
+ return { username, password };
118
+ }
119
+ export function parseTlsOptions(value, context) {
120
+ if (value === undefined) {
121
+ return undefined;
122
+ }
123
+ if (typeof value === "boolean") {
124
+ return { enabled: value };
125
+ }
126
+ if (!isObject(value)) {
127
+ throw new Error(`Invalid tls config in ${context}: expected boolean or object.`);
128
+ }
129
+ const parsed = {};
130
+ const enabled = asBoolean(value.enabled);
131
+ if (enabled !== undefined) {
132
+ parsed.enabled = enabled;
133
+ }
134
+ const rejectUnauthorized = asBoolean(value.rejectUnauthorized);
135
+ if (rejectUnauthorized !== undefined) {
136
+ parsed.rejectUnauthorized = rejectUnauthorized;
137
+ }
138
+ const servername = asString(value.servername);
139
+ if (servername) {
140
+ parsed.servername = servername;
141
+ }
142
+ if (Object.hasOwn(value, "ca")) {
143
+ parsed.ca = parseCredentialRef(value.ca, `${context}.ca`);
144
+ }
145
+ if (Object.hasOwn(value, "cert")) {
146
+ parsed.cert = parseCredentialRef(value.cert, `${context}.cert`);
147
+ }
148
+ if (Object.hasOwn(value, "key")) {
149
+ parsed.key = parseCredentialRef(value.key, `${context}.key`);
150
+ }
151
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
152
+ }
153
+ export function deepRedact(value) {
154
+ if (Array.isArray(value)) {
155
+ return value.map((entry) => deepRedact(entry));
156
+ }
157
+ if (isObject(value)) {
158
+ const output = {};
159
+ for (const [key, nested] of Object.entries(value)) {
160
+ output[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[REDACTED]" : deepRedact(nested);
161
+ }
162
+ return output;
163
+ }
164
+ return value;
165
+ }
166
+ export function redactDataSourceProfile(profile) {
167
+ return deepRedact(profile);
168
+ }
169
+ export function withRedactedToString(profile) {
170
+ const instance = profile;
171
+ Object.defineProperty(instance, "toString", {
172
+ enumerable: false,
173
+ configurable: false,
174
+ writable: false,
175
+ value() {
176
+ return JSON.stringify(redactDataSourceProfile(instance));
177
+ },
178
+ });
179
+ return instance;
180
+ }
181
+ export function parseProfileRecord(name, value, context) {
182
+ if (!isObject(value)) {
183
+ throw new Error(`Invalid datasource profile ${name} in ${context}: expected object.`);
184
+ }
185
+ const engine = parseEngine(value.engine ?? "mysql", `${context}.${name}.engine`);
186
+ const host = asString(value.host ?? value.hostname);
187
+ const parsedPort = asInteger(value.port);
188
+ const port = parsedPort ?? defaultPortForEngine(engine);
189
+ if (port <= 0) {
190
+ throw new Error(`Invalid datasource profile ${name} in ${context}: port must be positive.`);
191
+ }
192
+ const database = asString(value.database);
193
+ const poolSize = asInteger(value.poolSize);
194
+ if (poolSize !== undefined && poolSize <= 0) {
195
+ throw new Error(`Invalid datasource profile ${name} in ${context}: poolSize must be positive.`);
196
+ }
197
+ const readonlyRaw = value.readonlyUser ?? value.readonly ?? value.readOnlyUser;
198
+ if (readonlyRaw === undefined) {
199
+ throw new Error(`Invalid datasource profile ${name} in ${context}: missing readonlyUser.`);
200
+ }
201
+ const readonlyUser = parseUserCredential(readonlyRaw, `${context}.${name}.readonlyUser`);
202
+ const mutationRaw = value.mutationUser ?? value.writeUser ?? value.rwUser;
203
+ const mutationUser = mutationRaw !== undefined ? parseUserCredential(mutationRaw, `${context}.${name}.mutationUser`) : undefined;
204
+ const tls = parseTlsOptions(value.tls, `${context}.${name}.tls`);
205
+ return withRedactedToString({
206
+ name,
207
+ engine,
208
+ host,
209
+ port,
210
+ database,
211
+ readonlyUser,
212
+ mutationUser,
213
+ tls,
214
+ poolSize,
215
+ });
216
+ }
@@ -0,0 +1,14 @@
1
+ import type { DataSourceProfile, ProfileLoader, RuntimeDataSourceTarget, RuntimeTargetProfileLoader } from "./types.js";
2
+ export declare function applyRuntimeTarget(profile: DataSourceProfile, target: RuntimeDataSourceTarget | undefined): DataSourceProfile;
3
+ export declare class RuntimeOverrideProfileLoader implements RuntimeTargetProfileLoader {
4
+ private readonly base;
5
+ private readonly runtimeTargets;
6
+ constructor(base: ProfileLoader);
7
+ setRuntimeTarget(name: string, target: RuntimeDataSourceTarget): void;
8
+ clearRuntimeTarget(name: string): void;
9
+ clearAllRuntimeTargets(): void;
10
+ getRuntimeTarget(name: string): RuntimeDataSourceTarget | undefined;
11
+ load(): Promise<Map<string, DataSourceProfile>>;
12
+ getDefault(): Promise<string | undefined>;
13
+ get(name: string): Promise<DataSourceProfile | undefined>;
14
+ }
@@ -0,0 +1,52 @@
1
+ import { withRedactedToString } from "./parsing.js";
2
+ export function applyRuntimeTarget(profile, target) {
3
+ if (!target) {
4
+ return profile;
5
+ }
6
+ return withRedactedToString({
7
+ ...profile,
8
+ host: target.host,
9
+ port: target.port ?? profile.port,
10
+ });
11
+ }
12
+ export class RuntimeOverrideProfileLoader {
13
+ base;
14
+ runtimeTargets = new Map();
15
+ constructor(base) {
16
+ this.base = base;
17
+ }
18
+ setRuntimeTarget(name, target) {
19
+ this.runtimeTargets.set(name, {
20
+ host: target.host,
21
+ port: target.port,
22
+ instanceId: target.instanceId,
23
+ nodeId: target.nodeId,
24
+ });
25
+ }
26
+ clearRuntimeTarget(name) {
27
+ this.runtimeTargets.delete(name);
28
+ }
29
+ clearAllRuntimeTargets() {
30
+ this.runtimeTargets.clear();
31
+ }
32
+ getRuntimeTarget(name) {
33
+ return this.runtimeTargets.get(name);
34
+ }
35
+ async load() {
36
+ const loaded = await this.base.load();
37
+ return new Map([...loaded.entries()].map(([name, profile]) => [
38
+ name,
39
+ applyRuntimeTarget(profile, this.runtimeTargets.get(name)),
40
+ ]));
41
+ }
42
+ async getDefault() {
43
+ return this.base.getDefault();
44
+ }
45
+ async get(name) {
46
+ const profile = await this.base.get(name);
47
+ if (!profile) {
48
+ return undefined;
49
+ }
50
+ return applyRuntimeTarget(profile, this.runtimeTargets.get(name));
51
+ }
52
+ }