taurusdb-core 0.1.0 → 0.3.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.
@@ -17,18 +17,18 @@ export function parseEnvProfile(env) {
17
17
  let host;
18
18
  let port;
19
19
  let database;
20
- let readonlyUsername;
21
- let readonlyPasswordRef;
20
+ let username;
21
+ let passwordRef;
22
22
  if (dsn) {
23
23
  const url = new URL(dsn);
24
24
  engine = parseEngineFromDsnProtocol(url.protocol);
25
25
  host = url.hostname;
26
26
  port = url.port ? Number.parseInt(url.port, 10) : defaultPortForEngine(engine);
27
27
  database = asString(url.pathname.replace(/^\//, ""));
28
- readonlyUsername = asString(decodeURIComponent(url.username));
28
+ username = asString(decodeURIComponent(url.username));
29
29
  const dsnPassword = asString(decodeURIComponent(url.password));
30
30
  if (dsnPassword) {
31
- readonlyPasswordRef = parseCredentialRef(dsnPassword, "TAURUSDB_SQL_DSN.password");
31
+ passwordRef = parseCredentialRef(dsnPassword, "TAURUSDB_SQL_DSN.password");
32
32
  }
33
33
  }
34
34
  else {
@@ -38,41 +38,27 @@ export function parseEnvProfile(env) {
38
38
  port = explicitPort ?? defaultPortForEngine(engine);
39
39
  database = asString(env.TAURUSDB_SQL_DATABASE);
40
40
  }
41
- const mutationUserName = asString(env.TAURUSDB_SQL_MUTATION_USER);
42
- const mutationPasswordRaw = asString(env.TAURUSDB_SQL_MUTATION_PASSWORD);
43
41
  if (!dsn &&
44
42
  !explicitHost &&
45
43
  !asString(env.TAURUSDB_SQL_USER) &&
46
44
  !asString(env.TAURUSDB_SQL_PASSWORD) &&
47
- !database &&
48
- !mutationUserName &&
49
- !mutationPasswordRaw) {
45
+ !database) {
50
46
  return undefined;
51
47
  }
52
48
  if (!port || !Number.isFinite(port) || port <= 0) {
53
49
  throw new Error("Failed to resolve SQL port from environment.");
54
50
  }
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.");
51
+ username = username ?? asString(env.TAURUSDB_SQL_USER);
52
+ if (!username) {
53
+ throw new Error("Missing SQL username in environment. Set TAURUSDB_SQL_USER or include it in DSN.");
58
54
  }
59
- readonlyPasswordRef =
60
- readonlyPasswordRef ??
55
+ passwordRef =
56
+ passwordRef ??
61
57
  (Object.hasOwn(env, "TAURUSDB_SQL_PASSWORD")
62
58
  ? parseCredentialRef(env.TAURUSDB_SQL_PASSWORD, "TAURUSDB_SQL_PASSWORD")
63
59
  : 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
- };
60
+ if (!passwordRef) {
61
+ throw new Error("Missing SQL password in environment. Set TAURUSDB_SQL_PASSWORD or include it in DSN.");
76
62
  }
77
63
  const poolSize = asInteger(env.TAURUSDB_SQL_POOL_SIZE);
78
64
  if (poolSize !== undefined && poolSize <= 0) {
@@ -84,11 +70,10 @@ export function parseEnvProfile(env) {
84
70
  host,
85
71
  port,
86
72
  database,
87
- readonlyUser: {
88
- username: readonlyUsername,
89
- password: readonlyPasswordRef,
73
+ user: {
74
+ username,
75
+ password: passwordRef,
90
76
  },
91
- mutationUser,
92
77
  poolSize,
93
78
  });
94
79
  }
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { parseEnvProfile } from "./env-source.js";
5
5
  import { parseProfilesFile } from "./file-source.js";
6
+ import { withRedactedToString } from "./parsing.js";
6
7
  export function resolveDefaultProfilePath(config) {
7
8
  if (config.profilesPath) {
8
9
  return config.profilesPath;
@@ -67,6 +68,13 @@ export class SqlProfileLoader {
67
68
  mergedProfiles.set(name, profile);
68
69
  }
69
70
  }
71
+ if (mergedProfiles.size === 0) {
72
+ mergedProfiles.set("taurus_mcp", withRedactedToString({
73
+ name: "taurus_mcp",
74
+ engine: "mysql",
75
+ port: 3306,
76
+ }));
77
+ }
70
78
  const defaultDatasource = this.config.defaultDatasource ??
71
79
  fileDefaultDatasource ??
72
80
  (mergedProfiles.size === 1 ? mergedProfiles.keys().next().value : undefined);
@@ -194,13 +194,15 @@ export function parseProfileRecord(name, value, context) {
194
194
  if (poolSize !== undefined && poolSize <= 0) {
195
195
  throw new Error(`Invalid datasource profile ${name} in ${context}: poolSize must be positive.`);
196
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;
197
+ // Backward compatibility for older profile field names. New configs should use `user`.
198
+ const userRaw = value.user ??
199
+ value.readonlyUser ??
200
+ value.readonly ??
201
+ value.readOnlyUser;
202
+ if (userRaw === undefined) {
203
+ throw new Error(`Invalid datasource profile ${name} in ${context}: missing user.`);
204
+ }
205
+ const user = parseUserCredential(userRaw, `${context}.${name}.user`);
204
206
  const tls = parseTlsOptions(value.tls, `${context}.${name}.tls`);
205
207
  return withRedactedToString({
206
208
  name,
@@ -208,8 +210,7 @@ export function parseProfileRecord(name, value, context) {
208
210
  host,
209
211
  port,
210
212
  database,
211
- readonlyUser,
212
- mutationUser,
213
+ user,
213
214
  tls,
214
215
  poolSize,
215
216
  });
@@ -5,6 +5,7 @@ export declare class RuntimeOverrideProfileLoader implements RuntimeTargetProfil
5
5
  private readonly runtimeTargets;
6
6
  constructor(base: ProfileLoader);
7
7
  setRuntimeTarget(name: string, target: RuntimeDataSourceTarget): void;
8
+ clearRuntimeUser(name: string): void;
8
9
  clearRuntimeTarget(name: string): void;
9
10
  clearAllRuntimeTargets(): void;
10
11
  getRuntimeTarget(name: string): RuntimeDataSourceTarget | undefined;
@@ -5,8 +5,10 @@ export function applyRuntimeTarget(profile, target) {
5
5
  }
6
6
  return withRedactedToString({
7
7
  ...profile,
8
- host: target.host,
8
+ host: target.host ?? profile.host,
9
9
  port: target.port ?? profile.port,
10
+ database: target.database ?? profile.database,
11
+ user: target.user ?? profile.user,
10
12
  });
11
13
  }
12
14
  export class RuntimeOverrideProfileLoader {
@@ -16,12 +18,24 @@ export class RuntimeOverrideProfileLoader {
16
18
  this.base = base;
17
19
  }
18
20
  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
- });
21
+ const current = this.runtimeTargets.get(name);
22
+ const next = {
23
+ host: target.host ?? current?.host,
24
+ port: target.port ?? current?.port,
25
+ database: target.database ?? current?.database,
26
+ user: target.user ?? current?.user,
27
+ instanceId: target.instanceId ?? current?.instanceId,
28
+ nodeId: target.nodeId ?? current?.nodeId,
29
+ };
30
+ this.runtimeTargets.set(name, next);
31
+ }
32
+ clearRuntimeUser(name) {
33
+ const current = this.runtimeTargets.get(name);
34
+ if (!current) {
35
+ return;
36
+ }
37
+ const { user: _user, ...next } = current;
38
+ this.runtimeTargets.set(name, next);
25
39
  }
26
40
  clearRuntimeTarget(name) {
27
41
  this.runtimeTargets.delete(name);
@@ -31,8 +31,7 @@ export interface DataSourceProfile {
31
31
  host?: string;
32
32
  port: number;
33
33
  database?: string;
34
- readonlyUser: UserCredential;
35
- mutationUser?: UserCredential;
34
+ user?: UserCredential;
36
35
  tls?: TlsOptions;
37
36
  poolSize?: number;
38
37
  toString(): string;
@@ -43,13 +42,16 @@ export interface ProfileLoader {
43
42
  get(name: string): Promise<DataSourceProfile | undefined>;
44
43
  }
45
44
  export interface RuntimeDataSourceTarget {
46
- host: string;
45
+ host?: string;
47
46
  port?: number;
47
+ database?: string;
48
+ user?: UserCredential;
48
49
  instanceId?: string;
49
50
  nodeId?: string;
50
51
  }
51
52
  export interface RuntimeTargetProfileLoader extends ProfileLoader {
52
53
  setRuntimeTarget(name: string, target: RuntimeDataSourceTarget): void;
54
+ clearRuntimeUser(name: string): void;
53
55
  clearRuntimeTarget(name: string): void;
54
56
  clearAllRuntimeTargets(): void;
55
57
  getRuntimeTarget(name: string): RuntimeDataSourceTarget | undefined;
@@ -41,9 +41,11 @@ export declare class UnsupportedFeatureError extends Error {
41
41
  readonly feature: TaurusFeatureName;
42
42
  readonly requiredVersion?: string;
43
43
  readonly currentVersion?: string;
44
+ readonly parameterHint?: string;
44
45
  constructor(feature: TaurusFeatureName, message: string, options?: {
45
46
  requiredVersion?: string;
46
47
  currentVersion?: string;
48
+ parameterHint?: string;
47
49
  cause?: unknown;
48
50
  });
49
51
  }
@@ -3,12 +3,14 @@ export class UnsupportedFeatureError extends Error {
3
3
  feature;
4
4
  requiredVersion;
5
5
  currentVersion;
6
+ parameterHint;
6
7
  constructor(feature, message, options = {}) {
7
8
  super(message);
8
9
  this.name = "UnsupportedFeatureError";
9
10
  this.feature = feature;
10
11
  this.requiredVersion = options.requiredVersion;
11
12
  this.currentVersion = options.currentVersion;
13
+ this.parameterHint = options.parameterHint;
12
14
  if (options.cause !== undefined) {
13
15
  this.cause = options.cause;
14
16
  }
@@ -120,7 +120,6 @@ export function buildRawConfigFromEnv(env) {
120
120
  return {
121
121
  defaultDatasource: readString(env.TAURUSDB_DEFAULT_DATASOURCE) ?? inferredDatasourceName,
122
122
  profilesPath: expandTildePath(readString(env.TAURUSDB_SQL_PROFILES)),
123
- enableMutations: parseBoolean(env.TAURUSDB_MCP_ENABLE_MUTATIONS, "TAURUSDB_MCP_ENABLE_MUTATIONS"),
124
123
  cloud: {
125
124
  provider: "huaweicloud",
126
125
  region: cloudRegion,
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  export declare const ConfigSchema: z.ZodObject<{
3
3
  defaultDatasource: z.ZodOptional<z.ZodString>;
4
4
  profilesPath: z.ZodOptional<z.ZodString>;
5
- enableMutations: z.ZodDefault<z.ZodBoolean>;
6
5
  cloud: z.ZodDefault<z.ZodObject<{
7
6
  provider: z.ZodDefault<z.ZodEnum<["huaweicloud"]>>;
8
7
  region: z.ZodOptional<z.ZodString>;
@@ -270,7 +269,6 @@ export declare const ConfigSchema: z.ZodObject<{
270
269
  } | undefined;
271
270
  }>>;
272
271
  }, "strip", z.ZodTypeAny, {
273
- enableMutations: boolean;
274
272
  cloud: {
275
273
  provider: "huaweicloud";
276
274
  domainSuffix: string;
@@ -344,7 +342,6 @@ export declare const ConfigSchema: z.ZodObject<{
344
342
  }, {
345
343
  defaultDatasource?: string | undefined;
346
344
  profilesPath?: string | undefined;
347
- enableMutations?: boolean | undefined;
348
345
  cloud?: {
349
346
  provider?: "huaweicloud" | undefined;
350
347
  region?: string | undefined;
@@ -82,7 +82,6 @@ const CesMetricsSourceSchema = z
82
82
  export const ConfigSchema = z.object({
83
83
  defaultDatasource: z.string().min(1).optional(),
84
84
  profilesPath: z.string().min(1).optional(),
85
- enableMutations: z.boolean().default(true),
86
85
  cloud: CloudSchema,
87
86
  limits: LimitsSchema,
88
87
  audit: AuditSchema,
@@ -1,6 +1,6 @@
1
1
  import { UnsupportedFeatureError } from "../capability/types.js";
2
2
  import { InMemoryConfirmationStore } from "../safety/confirmation-store.js";
3
- import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
3
+ import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, normalizeFlashbackWhereClause, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
4
4
  import { buildListRecycleBinSql, buildRestoreRecycleBinTableSql, recycleBinMutationOptions, recycleBinReadonlyOptions } from "../taurus/recycle-bin.js";
5
5
  import { normalizeSql, sqlHash } from "../utils/hash.js";
6
6
  function resolveConfirmationSql(input) {
@@ -90,7 +90,8 @@ async function buildFlashbackNoViewError(engine, ctx, input, database, requested
90
90
  }
91
91
  if (input.where?.trim()) {
92
92
  try {
93
- const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteIdentifier("updated_at")} FROM ${quoteIdentifier(database)}.${quoteIdentifier(input.table)} WHERE (${input.where.trim()}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
93
+ const where = normalizeFlashbackWhereClause(input.where);
94
+ const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteIdentifier("updated_at")} FROM ${quoteIdentifier(database)}.${quoteIdentifier(input.table)} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
94
95
  const updatedAtRow = firstRowAsObject(updatedAtResult);
95
96
  if (typeof updatedAtRow?.updated_at === "string") {
96
97
  details.current_row_updated_at = updatedAtRow.updated_at;
@@ -286,6 +287,7 @@ export async function flashbackQuery(engine, input, ctx, opts) {
286
287
  throw new UnsupportedFeatureError("flashback_query", flashbackFeature.reason ??
287
288
  `Flashback query requires kernel version >= ${flashbackFeature.minVersion ?? "unknown"}.`, {
288
289
  requiredVersion: flashbackFeature.minVersion,
290
+ parameterHint: flashbackFeature.param,
289
291
  currentVersion: (await engine.capabilityProbe.getKernelInfo(ctx))
290
292
  .kernelVersion,
291
293
  });
@@ -318,6 +320,7 @@ export async function listRecycleBin(engine, ctx, opts) {
318
320
  throw new UnsupportedFeatureError("recycle_bin", recycleBinFeature.reason ??
319
321
  `Recycle bin requires kernel version >= ${recycleBinFeature.minVersion ?? "unknown"}.`, {
320
322
  requiredVersion: recycleBinFeature.minVersion,
323
+ parameterHint: recycleBinFeature.param,
321
324
  currentVersion: (await engine.capabilityProbe.getKernelInfo(ctx))
322
325
  .kernelVersion,
323
326
  });
@@ -331,13 +334,13 @@ export async function restoreRecycleBinTable(engine, input, ctx, opts) {
331
334
  throw new UnsupportedFeatureError("recycle_bin", recycleBinFeature.reason ??
332
335
  `Recycle bin requires kernel version >= ${recycleBinFeature.minVersion ?? "unknown"}.`, {
333
336
  requiredVersion: recycleBinFeature.minVersion,
337
+ parameterHint: recycleBinFeature.param,
334
338
  currentVersion: (await engine.capabilityProbe.getKernelInfo(ctx))
335
339
  .kernelVersion,
336
340
  });
337
341
  }
338
342
  return engine.executor.executeMutation(buildRestoreRecycleBinTableSql(input), ctx, {
339
343
  ...recycleBinMutationOptions(opts),
340
- allowWithoutGlobalMutations: true,
341
344
  allowReadonlyFallbackForMutations: true,
342
345
  });
343
346
  }
@@ -17,7 +17,6 @@ export interface DataSourceInfo {
17
17
  host?: string;
18
18
  port: number;
19
19
  database?: string;
20
- hasMutationUser: boolean;
21
20
  poolSize?: number;
22
21
  isDefault: boolean;
23
22
  }
package/dist/engine.js CHANGED
@@ -22,7 +22,6 @@ function toDataSourceInfo(profile, defaultDatasource) {
22
22
  host: profile.host,
23
23
  port: profile.port,
24
24
  database: profile.database,
25
- hasMutationUser: profile.mutationUser !== undefined,
26
25
  poolSize: profile.poolSize,
27
26
  isDefault: profile.name === defaultDatasource,
28
27
  };
@@ -35,7 +35,6 @@ export interface PoolHealth {
35
35
  }
36
36
  export interface ConnectionPool {
37
37
  acquire(datasource: string, mode: PoolMode, opts?: {
38
- allowWithoutGlobalMutations?: boolean;
39
38
  allowReadonlyFallbackForMutations?: boolean;
40
39
  }): Promise<Session>;
41
40
  release(session: Session): Promise<void>;
@@ -92,7 +91,6 @@ export declare class ConnectionPoolManager implements ConnectionPool {
92
91
  private readonly activeSessions;
93
92
  constructor(options: ConnectionPoolManagerOptions);
94
93
  acquire(datasource: string, mode: PoolMode, opts?: {
95
- allowWithoutGlobalMutations?: boolean;
96
94
  allowReadonlyFallbackForMutations?: boolean;
97
95
  }): Promise<Session>;
98
96
  release(session: Session): Promise<void>;
@@ -32,22 +32,12 @@ async function resolveTls(tls, secretResolver) {
32
32
  }
33
33
  return resolved;
34
34
  }
35
- function ensureMutationAllowed(config) {
36
- if (!config.enableMutations) {
37
- throw new ConnectionPoolError("Mutation mode is disabled by configuration.");
35
+ function selectCredential(profile, mode, _opts = {}) {
36
+ void mode;
37
+ if (!profile.user) {
38
+ throw new ConnectionPoolError(`Datasource "${profile.name}" does not define SQL credentials. Call begin_sql_login and complete the secure local login form before executing SQL.`);
38
39
  }
39
- }
40
- function selectCredential(profile, mode, opts = {}) {
41
- if (mode === "ro") {
42
- return profile.readonlyUser;
43
- }
44
- if (!profile.mutationUser) {
45
- if (opts.allowReadonlyFallbackForMutations) {
46
- return profile.readonlyUser;
47
- }
48
- throw new ConnectionPoolError(`Mutation user is not configured for datasource "${profile.name}".`);
49
- }
50
- return profile.mutationUser;
40
+ return profile.user;
51
41
  }
52
42
  async function resolveCredentialValue(ref, secretResolver, context) {
53
43
  try {
@@ -75,9 +65,6 @@ export class ConnectionPoolManager {
75
65
  if (!profile) {
76
66
  throw new ConnectionPoolError(`Datasource profile not found: "${datasource}".`);
77
67
  }
78
- if (mode === "rw" && !opts.allowWithoutGlobalMutations) {
79
- ensureMutationAllowed(this.config);
80
- }
81
68
  const entry = await this.getOrCreatePool(profile, mode, opts);
82
69
  let driverSession;
83
70
  try {
@@ -122,16 +109,7 @@ export class ConnectionPoolManager {
122
109
  async healthCheck(datasource) {
123
110
  const modes = [];
124
111
  modes.push(await this.healthCheckMode(datasource, "ro"));
125
- if (!this.config.enableMutations) {
126
- modes.push({
127
- mode: "rw",
128
- status: "skipped",
129
- message: "Mutation mode disabled by config.",
130
- });
131
- }
132
- else {
133
- modes.push(await this.healthCheckMode(datasource, "rw"));
134
- }
112
+ modes.push(await this.healthCheckMode(datasource, "rw"));
135
113
  return {
136
114
  datasource,
137
115
  checkedAt: new Date().toISOString(),
@@ -166,9 +144,6 @@ export class ConnectionPoolManager {
166
144
  return { mode, status: "ok" };
167
145
  }
168
146
  catch (error) {
169
- if (mode === "rw" && error instanceof ConnectionPoolError && /not configured/.test(error.message)) {
170
- return { mode, status: "skipped", message: error.message };
171
- }
172
147
  return {
173
148
  mode,
174
149
  status: "error",
@@ -114,7 +114,6 @@ export class SqlExecutorImpl {
114
114
  const startedAt = this.now();
115
115
  const timeoutMs = opts.timeoutMs ?? ctx.limits.timeoutMs;
116
116
  const session = await this.connectionPool.acquire(ctx.datasource, "rw", {
117
- allowWithoutGlobalMutations: opts.allowWithoutGlobalMutations,
118
117
  allowReadonlyFallbackForMutations: opts.allowReadonlyFallbackForMutations,
119
118
  });
120
119
  const active = this.beginQuery(queryId, session, ctx, "rw", startedAt);
@@ -15,7 +15,6 @@ export interface ReadonlyOptions {
15
15
  }
16
16
  export interface MutationOptions {
17
17
  timeoutMs?: number;
18
- allowWithoutGlobalMutations?: boolean;
19
18
  allowReadonlyFallbackForMutations?: boolean;
20
19
  }
21
20
  export interface QueryResult {
@@ -13,6 +13,7 @@ export interface FlashbackInput {
13
13
  columns?: string[];
14
14
  limit?: number;
15
15
  }
16
+ export declare function normalizeFlashbackWhereClause(where: string): string;
16
17
  export type FlashbackNoViewDetails = {
17
18
  database?: string;
18
19
  table?: string;
@@ -1,3 +1,5 @@
1
+ import { createSqlParser } from "../safety/parser/index.js";
2
+ import { normalizeSql } from "../utils/hash.js";
1
3
  const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
2
4
  const SQL_TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.\d{1,6})?$/;
3
5
  const RELATIVE_DURATION_PATTERN = /(\d+)\s*(ms|milliseconds?|s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)/gi;
@@ -30,6 +32,15 @@ function quoteIdentifier(identifier, fieldName) {
30
32
  }
31
33
  return `\`${identifier}\``;
32
34
  }
35
+ export function normalizeFlashbackWhereClause(where) {
36
+ const parser = createSqlParser("mysql");
37
+ const candidate = parser.normalize(`SELECT 1 FROM placeholder WHERE (${where})`);
38
+ const parsed = parser.parse(candidate.normalizedSql);
39
+ if (!parsed.ok || parsed.isMultiStatement || parsed.ast.kind !== "select") {
40
+ throw new Error("Invalid flashback where clause. Provide a single SQL expression.");
41
+ }
42
+ return normalizeSql(where);
43
+ }
33
44
  export class FlashbackNoViewError extends Error {
34
45
  details;
35
46
  constructor(message, details) {
@@ -129,7 +140,7 @@ export function buildFlashbackSql(input, defaultDatabase, now = Date.now) {
129
140
  ];
130
141
  const whereClause = input.where?.trim();
131
142
  if (whereClause) {
132
- clauses.push(`WHERE (${whereClause})`);
143
+ clauses.push(`WHERE (${normalizeFlashbackWhereClause(whereClause)})`);
133
144
  }
134
145
  if (input.limit !== undefined) {
135
146
  if (!Number.isInteger(input.limit) || input.limit <= 0) {
@@ -137,7 +148,7 @@ export function buildFlashbackSql(input, defaultDatabase, now = Date.now) {
137
148
  }
138
149
  clauses.push(`LIMIT ${input.limit}`);
139
150
  }
140
- return clauses.join(" ");
151
+ return normalizeSql(clauses.join(" "));
141
152
  }
142
153
  export function flashbackReadonlyOptions(limit) {
143
154
  if (limit === undefined) {
@@ -8,6 +8,14 @@ const REDACT_PATHS = [
8
8
  "secret",
9
9
  "token",
10
10
  "*.token",
11
+ "accessKeyId",
12
+ "*.accessKeyId",
13
+ "secretAccessKey",
14
+ "*.secretAccessKey",
15
+ "securityToken",
16
+ "*.securityToken",
17
+ "authToken",
18
+ "*.authToken",
11
19
  ];
12
20
  function createDefaultDestination() {
13
21
  return pino.destination({ fd: 2, sync: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taurusdb-core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared TaurusDB data-plane engine for schema, SQL execution, guardrails, and diagnostics.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",