taurusdb-core 0.4.0 → 0.5.0-rc.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -1
  3. package/dist/audit/jsonl-writer.d.ts +47 -0
  4. package/dist/audit/jsonl-writer.js +124 -0
  5. package/dist/auth/sql-profile-loader/env-source.js +10 -0
  6. package/dist/auth/sql-profile-loader/parsing.js +10 -1
  7. package/dist/auth/sql-profile-loader/runtime-override.d.ts +1 -0
  8. package/dist/auth/sql-profile-loader/runtime-override.js +19 -1
  9. package/dist/auth/sql-profile-loader/types.d.ts +7 -0
  10. package/dist/capability/probe.js +3 -1
  11. package/dist/cloud/auth.d.ts +1 -0
  12. package/dist/cloud/auth.js +37 -0
  13. package/dist/config/env.js +14 -0
  14. package/dist/config/schema.d.ts +68 -0
  15. package/dist/config/schema.js +17 -0
  16. package/dist/context/datasource-resolver.js +7 -0
  17. package/dist/context/session-context.d.ts +7 -0
  18. package/dist/diagnostics/replication-lag.d.ts +4 -0
  19. package/dist/diagnostics/replication-lag.js +138 -0
  20. package/dist/diagnostics/types.d.ts +5 -1
  21. package/dist/diagnostics/types.js +7 -7
  22. package/dist/engine/runtime.d.ts +2 -2
  23. package/dist/engine/runtime.js +8 -14
  24. package/dist/engine/types.d.ts +3 -2
  25. package/dist/engine.d.ts +4 -3
  26. package/dist/engine.js +34 -1
  27. package/dist/executor/bounded-sql.d.ts +1 -0
  28. package/dist/executor/bounded-sql.js +10 -0
  29. package/dist/executor/concurrency-limiter.d.ts +15 -0
  30. package/dist/executor/concurrency-limiter.js +67 -0
  31. package/dist/executor/connection-pool.d.ts +3 -2
  32. package/dist/executor/connection-pool.js +36 -24
  33. package/dist/executor/sql-executor.d.ts +3 -0
  34. package/dist/executor/sql-executor.js +52 -9
  35. package/dist/executor/types.d.ts +5 -1
  36. package/dist/index.d.ts +6 -3
  37. package/dist/index.js +4 -1
  38. package/dist/safety/confirmation-store.d.ts +30 -7
  39. package/dist/safety/confirmation-store.js +154 -30
  40. package/dist/safety/guardrail.d.ts +3 -0
  41. package/dist/safety/guardrail.js +16 -6
  42. package/dist/safety/redaction.d.ts +6 -0
  43. package/dist/safety/redaction.js +47 -6
  44. package/dist/safety/sql-classifier.d.ts +1 -0
  45. package/dist/safety/sql-classifier.js +1 -0
  46. package/dist/safety/sql-validator.d.ts +1 -0
  47. package/dist/safety/sql-validator.js +24 -0
  48. package/dist/schema/adapters/mysql.js +3 -1
  49. package/dist/taurus/flashback.js +4 -10
  50. package/dist/taurus/recycle-bin.js +2 -8
  51. package/dist/utils/formatter.d.ts +6 -2
  52. package/dist/utils/formatter.js +7 -3
  53. package/dist/utils/logger.js +8 -0
  54. package/dist/utils/mysql-identifier.d.ts +1 -0
  55. package/dist/utils/mysql-identifier.js +9 -0
  56. package/package.json +13 -2
@@ -11,6 +11,9 @@ export type SqlExecutorOptions = {
11
11
  historyLimit?: number;
12
12
  queryTracker?: QueryTracker;
13
13
  resultRedactor?: ResultRedactor;
14
+ maxConcurrentQueries?: number;
15
+ maxQueuedQueries?: number;
16
+ queueTimeoutMs?: number;
14
17
  };
15
18
  export declare class SqlExecutorImpl implements SqlExecutor {
16
19
  private readonly connectionPool;
@@ -3,6 +3,22 @@ import { buildExplainRecommendations, normalizeExplainRows, summarizeExplainRows
3
3
  import { createQueryTracker, } from "./query-tracker.js";
4
4
  import { createResultRedactor, } from "../safety/redaction.js";
5
5
  import { asFiniteNumber, inferColumns, normalizeRows } from "./result-normalizer.js";
6
+ import { QueryConcurrencyLimiter } from "./concurrency-limiter.js";
7
+ import { buildServerBoundedReadonlySql } from "./bounded-sql.js";
8
+ function isTimeoutError(error) {
9
+ return /timeout|timed out/i.test(error instanceof Error ? error.message : String(error));
10
+ }
11
+ async function cancelTimedOutSession(session, error) {
12
+ if (!isTimeoutError(error)) {
13
+ return;
14
+ }
15
+ try {
16
+ await session.cancel();
17
+ }
18
+ catch {
19
+ // Keep the original timeout error.
20
+ }
21
+ }
6
22
  export class SqlExecutorImpl {
7
23
  connectionPool;
8
24
  now;
@@ -25,7 +41,9 @@ export class SqlExecutorImpl {
25
41
  async explain(sql, ctx) {
26
42
  const queryId = this.queryIdGenerator();
27
43
  const startedAt = this.now();
28
- const session = await this.connectionPool.acquire(ctx.datasource, "ro");
44
+ const session = await this.connectionPool.acquire(ctx.datasource, "ro", {
45
+ database: ctx.database,
46
+ });
29
47
  const active = this.beginQuery(queryId, session, ctx, "ro", startedAt);
30
48
  try {
31
49
  const result = await session.execute(`EXPLAIN ${sql}`, {
@@ -45,6 +63,7 @@ export class SqlExecutorImpl {
45
63
  };
46
64
  }
47
65
  catch (error) {
66
+ await cancelTimedOutSession(session, error);
48
67
  const durationMs = this.now() - startedAt;
49
68
  this.completeQuery(active.queryId, active.cancelRequested ? "cancelled" : "failed", durationMs, error);
50
69
  throw error;
@@ -59,11 +78,16 @@ export class SqlExecutorImpl {
59
78
  const maxRows = opts.maxRows ?? ctx.limits.maxRows;
60
79
  const maxColumns = opts.maxColumns ?? ctx.limits.maxColumns;
61
80
  const maxFieldChars = opts.maxFieldChars ?? ctx.limits.maxFieldChars ?? 2048;
81
+ const maxResultBytes = opts.maxResultBytes ?? ctx.limits.maxResultBytes ?? 1048576;
82
+ const maxBlobBytes = opts.maxBlobBytes ?? ctx.limits.maxBlobBytes ?? 65536;
62
83
  const timeoutMs = opts.timeoutMs ?? ctx.limits.timeoutMs;
63
- const session = await this.connectionPool.acquire(ctx.datasource, "ro");
84
+ const session = await this.connectionPool.acquire(ctx.datasource, "ro", {
85
+ database: ctx.database,
86
+ });
64
87
  const active = this.beginQuery(queryId, session, ctx, "ro", startedAt);
65
88
  try {
66
- const result = await session.execute(sql, { timeoutMs });
89
+ const executionSql = buildServerBoundedReadonlySql(sql, maxRows);
90
+ const result = await session.execute(executionSql, { timeoutMs });
67
91
  const sourceRows = Array.isArray(result.rows) ? result.rows : [];
68
92
  const columns = inferColumns(result, sourceRows);
69
93
  const normalizedRows = normalizeRows(sourceRows, columns);
@@ -76,6 +100,9 @@ export class SqlExecutorImpl {
76
100
  maxRows,
77
101
  maxColumns,
78
102
  maxFieldChars,
103
+ maxResultBytes,
104
+ maxBlobBytes,
105
+ maskAllColumns: opts.maskAllColumns,
79
106
  sensitiveColumns: opts.sensitiveColumns,
80
107
  sensitiveStrategy: opts.sensitiveStrategy,
81
108
  });
@@ -91,6 +118,8 @@ export class SqlExecutorImpl {
91
118
  rowTruncated: redacted.rowTruncated,
92
119
  columnTruncated: redacted.columnTruncated,
93
120
  fieldTruncated: redacted.fieldTruncated,
121
+ byteTruncated: redacted.byteTruncated,
122
+ returnedBytes: redacted.returnedBytes,
94
123
  redactedColumns: redacted.redactedColumns,
95
124
  droppedColumns: redacted.droppedColumns,
96
125
  truncatedColumns: redacted.truncatedColumns,
@@ -98,6 +127,7 @@ export class SqlExecutorImpl {
98
127
  };
99
128
  }
100
129
  catch (error) {
130
+ await cancelTimedOutSession(session, error);
101
131
  const durationMs = this.now() - startedAt;
102
132
  this.completeQuery(active.queryId, active.cancelRequested ? "cancelled" : "failed", durationMs, error);
103
133
  throw error;
@@ -114,7 +144,7 @@ export class SqlExecutorImpl {
114
144
  const startedAt = this.now();
115
145
  const timeoutMs = opts.timeoutMs ?? ctx.limits.timeoutMs;
116
146
  const session = await this.connectionPool.acquire(ctx.datasource, "rw", {
117
- allowReadonlyFallbackForMutations: opts.allowReadonlyFallbackForMutations,
147
+ database: ctx.database,
118
148
  });
119
149
  const active = this.beginQuery(queryId, session, ctx, "rw", startedAt);
120
150
  try {
@@ -133,11 +163,16 @@ export class SqlExecutorImpl {
133
163
  };
134
164
  }
135
165
  catch (error) {
136
- try {
137
- await session.execute("ROLLBACK", { timeoutMs });
166
+ if (isTimeoutError(error)) {
167
+ await cancelTimedOutSession(session, error);
138
168
  }
139
- catch {
140
- // Ignore rollback failure and keep original error.
169
+ else {
170
+ try {
171
+ await session.execute("ROLLBACK", { timeoutMs });
172
+ }
173
+ catch {
174
+ // Ignore rollback failure and keep original error.
175
+ }
141
176
  }
142
177
  const durationMs = this.now() - startedAt;
143
178
  this.completeQuery(active.queryId, active.cancelRequested ? "cancelled" : "failed", durationMs, error);
@@ -245,5 +280,13 @@ export class SqlExecutorImpl {
245
280
  }
246
281
  }
247
282
  export function createSqlExecutor(options) {
248
- return new SqlExecutorImpl(options);
283
+ const executor = new SqlExecutorImpl(options);
284
+ const limiter = new QueryConcurrencyLimiter(options.maxConcurrentQueries ?? 8, options.maxQueuedQueries ?? 32, options.queueTimeoutMs ?? 5000);
285
+ return {
286
+ explain: (sql, ctx) => limiter.run(() => executor.explain(sql, ctx)),
287
+ executeReadonly: (sql, ctx, opts) => limiter.run(() => executor.executeReadonly(sql, ctx, opts)),
288
+ executeMutation: (sql, ctx, opts) => limiter.run(() => executor.executeMutation(sql, ctx, opts)),
289
+ getQueryStatus: (queryId) => executor.getQueryStatus(queryId),
290
+ cancelQuery: (queryId) => executor.cancelQuery(queryId),
291
+ };
249
292
  }
@@ -9,13 +9,15 @@ export interface ReadonlyOptions {
9
9
  maxRows?: number;
10
10
  maxColumns?: number;
11
11
  maxFieldChars?: number;
12
+ maxResultBytes?: number;
13
+ maxBlobBytes?: number;
14
+ maskAllColumns?: boolean;
12
15
  timeoutMs?: number;
13
16
  sensitiveColumns?: Iterable<string>;
14
17
  sensitiveStrategy?: SensitiveStrategy;
15
18
  }
16
19
  export interface MutationOptions {
17
20
  timeoutMs?: number;
18
- allowReadonlyFallbackForMutations?: boolean;
19
21
  }
20
22
  export interface QueryResult {
21
23
  queryId: string;
@@ -27,6 +29,8 @@ export interface QueryResult {
27
29
  rowTruncated: boolean;
28
30
  columnTruncated: boolean;
29
31
  fieldTruncated: boolean;
32
+ byteTruncated: boolean;
33
+ returnedBytes: number;
30
34
  redactedColumns: string[];
31
35
  droppedColumns: string[];
32
36
  truncatedColumns: string[];
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { TaurusDBEngine } from "./engine.js";
2
2
  export type { EnhancedExplainResult, ConfirmationOutcome, DataSourceInfo, IssueConfirmationInput, TaurusDBEngineCreateOptions, TaurusDBEngineDeps, } from "./engine.js";
3
+ export { createJsonlAuditWriter, JsonlAuditWriter, type AuditDecision, type AuditEvent, type AuditWriter, type JsonlAuditWriterOptions, } from "./audit/jsonl-writer.js";
3
4
  export { createConfigFromEnv, getConfig, redactConfigForLog, resetConfigForTests, } from "./config/index.js";
4
5
  export type { Config } from "./config/index.js";
5
6
  export { createSqlProfileLoader, RuntimeOverrideProfileLoader, } from "./auth/sql-profile-loader.js";
@@ -7,13 +8,15 @@ export type { DataSourceProfile, DatabaseEngine, ProfileLoader, RuntimeDataSourc
7
8
  export { DatasourceResolutionError } from "./context/datasource-resolver.js";
8
9
  export type { DatasourceResolveInput, DatasourceResolver, RuntimeLimits, SessionContext, } from "./context/session-context.js";
9
10
  export { ConnectionPoolError } from "./executor/connection-pool.js";
11
+ export { QueryConcurrencyError } from "./executor/concurrency-limiter.js";
12
+ export { buildServerBoundedReadonlySql } from "./executor/bounded-sql.js";
10
13
  export type { CancelResult, ExplainResult, MutationOptions, MutationResult, QueryResult, QueryStatus, ReadonlyOptions, SqlExecutor, } from "./executor/sql-executor.js";
11
14
  export { createCapabilityProbe } from "./capability/probe.js";
12
15
  export type { CapabilityProbe } from "./capability/probe.js";
13
16
  export { UnsupportedFeatureError } from "./capability/types.js";
14
17
  export type { CapabilitySnapshot, FeatureMatrix, FeatureStatus, KernelInfo, TaurusFeatureName, } from "./capability/types.js";
15
- export { createConfirmationStore, InMemoryConfirmationStore, } from "./safety/confirmation-store.js";
16
- export type { ConfirmationStore, ConfirmationToken, ConfirmationValidationResult, IssueInput, } from "./safety/confirmation-store.js";
18
+ export { createConfirmationStore, InMemoryConfirmationStore, parseApprovalRequest, signApprovalRequest, } from "./safety/confirmation-store.js";
19
+ export type { ConfirmationStore, ConfirmationRequest, ConfirmationValidationResult, IssueInput, } from "./safety/confirmation-store.js";
17
20
  export { createGuardrail } from "./safety/guardrail.js";
18
21
  export type { Guardrail, GuardrailDecision, GuardrailRuntimeLimits, InspectInput, } from "./safety/guardrail.js";
19
22
  export type { ExplainRiskSummary, RiskLevel, ValidationResult } from "./safety/sql-validator.js";
@@ -41,6 +44,6 @@ export type { RecycleBinListResult, RecycleBinRestoreResult, RestoreRecycleBinTa
41
44
  export { createPlaceholderDiagnosticResult } from "./diagnostics/types.js";
42
45
  export { buildResolveSlowSqlInput, createSlowSqlSource, TaurusApiSlowSqlSource, } from "./diagnostics/slow-sql-source.js";
43
46
  export { CesMetricsSource, createMetricsSource, } from "./diagnostics/metrics-source.js";
44
- export type { DbHotspotItem, DbHotspotResult, DiagnosticBaseInput, DiagnosticConfidence, DiagnosticEvidenceItem, DiagnosticEvidenceLevel, DiagnosticNextToolInput, DiagnoseDbHotspotInput, DiagnoseServiceLatencyInput, FindTopSlowSqlInput, FindTopSlowSqlResult, DiagnosticResult, DiagnosticRootCauseCandidate, DiagnosticSeverity, DiagnosticStatus, ServiceLatencyCandidate, ServiceLatencyResult, ServiceLatencySuspectedCategory, DiagnosticSuspiciousEntities, DiagnosticToolName, DiagnosisWindow, DiagnoseConnectionSpikeInput, DiagnoseLockContentionInput, DiagnoseSlowQueryInput, DiagnoseStoragePressureInput, PlaceholderDiagnosticOptions, TopSlowSqlItem, } from "./diagnostics/types.js";
47
+ export type { DbHotspotItem, DbHotspotResult, DiagnosticBaseInput, DiagnosticConfidence, DiagnosticEvidenceItem, DiagnosticEvidenceLevel, DiagnosticNextToolInput, DiagnoseDbHotspotInput, DiagnoseServiceLatencyInput, FindTopSlowSqlInput, FindTopSlowSqlResult, DiagnosticResult, DiagnosticRootCauseCandidate, DiagnosticSeverity, DiagnosticStatus, ServiceLatencyCandidate, ServiceLatencyResult, ServiceLatencySuspectedCategory, DiagnosticSuspiciousEntities, DiagnosticToolName, DiagnosisWindow, DiagnoseConnectionSpikeInput, DiagnoseLockContentionInput, DiagnoseReplicationLagInput, DiagnoseSlowQueryInput, DiagnoseStoragePressureInput, PlaceholderDiagnosticOptions, TopSlowSqlItem, } from "./diagnostics/types.js";
45
48
  export type { ExternalSlowSqlSample, ResolveSlowSqlInput, SlowSqlSource, } from "./diagnostics/slow-sql-source.js";
46
49
  export type { MetricAlias, MetricPoint, MetricSummary, MetricsSource, QueryMetricsInput, } from "./diagnostics/metrics-source.js";
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  export { TaurusDBEngine } from "./engine.js";
2
+ export { createJsonlAuditWriter, JsonlAuditWriter, } from "./audit/jsonl-writer.js";
2
3
  export { createConfigFromEnv, getConfig, redactConfigForLog, resetConfigForTests, } from "./config/index.js";
3
4
  export { createSqlProfileLoader, RuntimeOverrideProfileLoader, } from "./auth/sql-profile-loader.js";
4
5
  export { DatasourceResolutionError } from "./context/datasource-resolver.js";
5
6
  export { ConnectionPoolError } from "./executor/connection-pool.js";
7
+ export { QueryConcurrencyError } from "./executor/concurrency-limiter.js";
8
+ export { buildServerBoundedReadonlySql } from "./executor/bounded-sql.js";
6
9
  export { createCapabilityProbe } from "./capability/probe.js";
7
10
  export { UnsupportedFeatureError } from "./capability/types.js";
8
- export { createConfirmationStore, InMemoryConfirmationStore, } from "./safety/confirmation-store.js";
11
+ export { createConfirmationStore, InMemoryConfirmationStore, parseApprovalRequest, signApprovalRequest, } from "./safety/confirmation-store.js";
9
12
  export { createGuardrail } from "./safety/guardrail.js";
10
13
  export { SchemaIntrospectionError } from "./schema/introspector.js";
11
14
  export { ErrorCode, formatBlocked, formatConfirmationRequired, formatError, formatSuccess, } from "./utils/formatter.js";
@@ -1,5 +1,20 @@
1
1
  import type { SessionContext } from "../context/session-context.js";
2
2
  import type { RiskLevel, ValidationResult } from "./sql-validator.js";
3
+ export type ApprovalRequestPayload = {
4
+ version: 1;
5
+ requestId: string;
6
+ sqlHash: string;
7
+ datasource: string;
8
+ database?: string;
9
+ host?: string;
10
+ port?: number;
11
+ projectId?: string;
12
+ instanceId?: string;
13
+ nodeId?: string;
14
+ riskLevel: RiskLevel;
15
+ issuedAt: number;
16
+ expiresAt: number;
17
+ };
3
18
  export type IssueInput = {
4
19
  sqlHash: string;
5
20
  normalizedSql: string;
@@ -7,38 +22,46 @@ export type IssueInput = {
7
22
  riskLevel: RiskLevel;
8
23
  ttlSeconds?: number;
9
24
  };
10
- export type ConfirmationToken = {
11
- token: string;
25
+ export type ConfirmationRequest = {
26
+ request: string;
27
+ requestId: string;
12
28
  issuedAt: number;
13
29
  expiresAt: number;
14
30
  };
15
31
  export type ConfirmationValidationResult = ValidationResult & {
16
32
  valid: boolean;
33
+ actor?: string;
17
34
  reason?: string;
18
35
  };
19
36
  export interface ConfirmationStore {
20
- issue(input: IssueInput): Promise<ConfirmationToken>;
37
+ issue(input: IssueInput): Promise<ConfirmationRequest>;
21
38
  validate(token: string, currentSql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
22
- revoke(token: string): Promise<void>;
39
+ revoke(requestId: string): Promise<void>;
23
40
  }
24
41
  export type ConfirmationStoreOptions = {
42
+ approvalSecret?: string | Buffer;
25
43
  ttlSeconds?: number;
26
44
  cleanupIntervalMs?: number;
45
+ maxEntries?: number;
27
46
  now?: () => number;
28
47
  randomBytesFn?: (size: number) => Buffer;
29
48
  };
49
+ export declare function parseApprovalRequest(request: string): ApprovalRequestPayload;
50
+ export declare function signApprovalRequest(request: string, actor: string, secret: string | Buffer): string;
30
51
  export declare class InMemoryConfirmationStore implements ConfirmationStore {
31
52
  private readonly entries;
32
53
  private readonly now;
33
54
  private readonly ttlSeconds;
55
+ private readonly maxEntries;
34
56
  private readonly randomBytesFn;
57
+ private readonly approvalSecret?;
35
58
  private cleanupTimer?;
36
59
  constructor(options?: ConfirmationStoreOptions);
37
- issue(input: IssueInput): Promise<ConfirmationToken>;
60
+ issue(input: IssueInput): Promise<ConfirmationRequest>;
38
61
  validate(token: string, currentSql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
39
- revoke(token: string): Promise<void>;
62
+ revoke(requestId: string): Promise<void>;
40
63
  stop(): void;
41
64
  cleanupExpired(now?: number): void;
42
- private generateUniqueToken;
65
+ private generateUniqueRequestId;
43
66
  }
44
67
  export declare function createConfirmationStore(options?: ConfirmationStoreOptions): ConfirmationStore;
@@ -1,11 +1,14 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
2
  import { normalizeSql, sqlHash } from "../utils/hash.js";
3
3
  const DEFAULT_TTL_SECONDS = 300;
4
4
  const DEFAULT_CLEANUP_INTERVAL_MS = 60_000;
5
+ const DEFAULT_MAX_ENTRIES = 1_000;
6
+ const REQUEST_PREFIX = "creq_";
5
7
  const TOKEN_PREFIX = "ctok_";
6
- function allowResult() {
8
+ function allowResult(actor) {
7
9
  return {
8
10
  valid: true,
11
+ actor,
9
12
  action: "allow",
10
13
  riskLevel: "low",
11
14
  reasonCodes: [],
@@ -36,16 +39,90 @@ function parseTtlSeconds(ttlSeconds, fallback) {
36
39
  }
37
40
  return resolved;
38
41
  }
42
+ function parseMaxEntries(value) {
43
+ const resolved = value ?? DEFAULT_MAX_ENTRIES;
44
+ if (!Number.isInteger(resolved) || resolved <= 0) {
45
+ throw new Error("maxEntries must be a positive integer.");
46
+ }
47
+ return resolved;
48
+ }
49
+ function encodePayload(payload) {
50
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
51
+ }
52
+ function decodeJson(encoded) {
53
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
54
+ }
55
+ function normalizeSecret(secret) {
56
+ if (secret === undefined) {
57
+ return undefined;
58
+ }
59
+ const normalized = Buffer.isBuffer(secret) ? Buffer.from(secret) : Buffer.from(secret, "utf8");
60
+ if (normalized.length < 32) {
61
+ throw new Error("Mutation approval secret must contain at least 32 bytes.");
62
+ }
63
+ return normalized;
64
+ }
65
+ function signEncodedPayload(encodedPayload, secret) {
66
+ return createHmac("sha256", secret).update(encodedPayload, "utf8").digest();
67
+ }
68
+ export function parseApprovalRequest(request) {
69
+ if (!request.startsWith(REQUEST_PREFIX)) {
70
+ throw new Error("Invalid approval request prefix.");
71
+ }
72
+ const payload = decodeJson(request.slice(REQUEST_PREFIX.length));
73
+ if (payload.version !== 1 ||
74
+ typeof payload.requestId !== "string" ||
75
+ typeof payload.sqlHash !== "string" ||
76
+ typeof payload.datasource !== "string" ||
77
+ typeof payload.issuedAt !== "number" ||
78
+ typeof payload.expiresAt !== "number") {
79
+ throw new Error("Invalid approval request payload.");
80
+ }
81
+ return payload;
82
+ }
83
+ export function signApprovalRequest(request, actor, secret) {
84
+ const normalizedActor = actor.trim();
85
+ if (!normalizedActor) {
86
+ throw new Error("Approval actor is required.");
87
+ }
88
+ const requestPayload = parseApprovalRequest(request);
89
+ const payload = {
90
+ ...requestPayload,
91
+ actor: normalizedActor,
92
+ };
93
+ const encodedPayload = encodePayload(payload);
94
+ const signature = signEncodedPayload(encodedPayload, normalizeSecret(secret));
95
+ return `${TOKEN_PREFIX}${encodedPayload}.${signature.toString("base64url")}`;
96
+ }
97
+ function equalRequestPayload(pending, signed) {
98
+ return (pending.version === signed.version &&
99
+ pending.requestId === signed.requestId &&
100
+ pending.sqlHash === signed.sqlHash &&
101
+ pending.datasource === signed.datasource &&
102
+ pending.database === signed.database &&
103
+ pending.host === signed.host &&
104
+ pending.port === signed.port &&
105
+ pending.projectId === signed.projectId &&
106
+ pending.instanceId === signed.instanceId &&
107
+ pending.nodeId === signed.nodeId &&
108
+ pending.riskLevel === signed.riskLevel &&
109
+ pending.issuedAt === signed.issuedAt &&
110
+ pending.expiresAt === signed.expiresAt);
111
+ }
39
112
  export class InMemoryConfirmationStore {
40
113
  entries = new Map();
41
114
  now;
42
115
  ttlSeconds;
116
+ maxEntries;
43
117
  randomBytesFn;
118
+ approvalSecret;
44
119
  cleanupTimer;
45
120
  constructor(options = {}) {
46
121
  this.now = options.now ?? Date.now;
47
122
  this.ttlSeconds = parseTtlSeconds(options.ttlSeconds, DEFAULT_TTL_SECONDS);
123
+ this.maxEntries = parseMaxEntries(options.maxEntries);
48
124
  this.randomBytesFn = options.randomBytesFn ?? randomBytes;
125
+ this.approvalSecret = normalizeSecret(options.approvalSecret);
49
126
  const cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
50
127
  if (cleanupIntervalMs > 0) {
51
128
  this.cleanupTimer = setInterval(() => this.cleanupExpired(), cleanupIntervalMs);
@@ -54,53 +131,100 @@ export class InMemoryConfirmationStore {
54
131
  }
55
132
  async issue(input) {
56
133
  this.cleanupExpired();
134
+ if (this.entries.size >= this.maxEntries) {
135
+ throw new Error("Too many pending mutation approval requests.");
136
+ }
57
137
  const ttlSeconds = parseTtlSeconds(input.ttlSeconds, this.ttlSeconds);
58
138
  const issuedAt = this.now();
59
139
  const expiresAt = issuedAt + ttlSeconds * 1000;
60
- const token = this.generateUniqueToken();
61
- this.entries.set(token, {
62
- token,
140
+ const requestId = this.generateUniqueRequestId();
141
+ const payload = {
142
+ version: 1,
143
+ requestId,
63
144
  sqlHash: input.sqlHash,
64
- normalizedSql: input.normalizedSql,
65
145
  datasource: input.context.datasource,
66
146
  database: normalizeDatabase(input.context.database),
147
+ host: input.context.host,
148
+ port: input.context.port,
149
+ projectId: input.context.projectId,
150
+ instanceId: input.context.instanceId,
151
+ nodeId: input.context.nodeId,
67
152
  riskLevel: input.riskLevel,
68
153
  issuedAt,
69
154
  expiresAt,
70
- });
155
+ };
156
+ this.entries.set(requestId, { payload });
71
157
  return {
72
- token,
158
+ request: `${REQUEST_PREFIX}${encodePayload(payload)}`,
159
+ requestId,
73
160
  issuedAt,
74
161
  expiresAt,
75
162
  };
76
163
  }
77
164
  async validate(token, currentSql, ctx) {
78
- const entry = this.entries.get(token);
165
+ if (!this.approvalSecret) {
166
+ return blockResult("CF006", "External mutation approval is not configured.");
167
+ }
168
+ if (!token.startsWith(TOKEN_PREFIX)) {
169
+ return blockResult("CF001", "Invalid approval token prefix.");
170
+ }
171
+ const [encodedPayload, encodedSignature, ...extra] = token
172
+ .slice(TOKEN_PREFIX.length)
173
+ .split(".");
174
+ if (!encodedPayload || !encodedSignature || extra.length > 0) {
175
+ return blockResult("CF001", "Invalid approval token format.");
176
+ }
177
+ let payload;
178
+ let signature;
179
+ try {
180
+ payload = decodeJson(encodedPayload);
181
+ signature = Buffer.from(encodedSignature, "base64url");
182
+ }
183
+ catch {
184
+ return blockResult("CF001", "Invalid approval token encoding.");
185
+ }
186
+ const expectedSignature = signEncodedPayload(encodedPayload, this.approvalSecret);
187
+ if (signature.length !== expectedSignature.length ||
188
+ !timingSafeEqual(signature, expectedSignature)) {
189
+ return blockResult("CF007", "Approval token signature is invalid.");
190
+ }
191
+ if (typeof payload.actor !== "string" || !payload.actor.trim()) {
192
+ return blockResult("CF008", "Approval token does not identify an actor.");
193
+ }
194
+ const entry = this.entries.get(payload.requestId);
79
195
  if (!entry) {
80
- return blockResult("CF001", "Confirmation token not found.");
196
+ return blockResult("CF001", "Approval request not found.");
81
197
  }
82
198
  const now = this.now();
83
- if (entry.expiresAt <= now) {
84
- this.entries.delete(token);
85
- return blockResult("CF002", "Confirmation token has expired.");
199
+ if (entry.payload.expiresAt <= now || payload.expiresAt <= now) {
200
+ this.entries.delete(payload.requestId);
201
+ return blockResult("CF002", "Approval request has expired.");
86
202
  }
87
203
  if (entry.usedAt !== undefined) {
88
- return blockResult("CF005", "Confirmation token has already been used.");
204
+ return blockResult("CF005", "Approval token has already been used.");
205
+ }
206
+ if (!equalRequestPayload(entry.payload, payload)) {
207
+ return blockResult("CF009", "Approval token payload does not match the pending request.");
89
208
  }
90
209
  const normalizedCurrentSql = normalizeSql(currentSql);
91
- const currentSqlHash = sqlHash(normalizedCurrentSql);
92
- if (currentSqlHash !== entry.sqlHash) {
93
- return blockResult("CF003", "SQL hash mismatch for confirmation token.");
210
+ if (sqlHash(normalizedCurrentSql) !== payload.sqlHash) {
211
+ return blockResult("CF003", "SQL hash mismatch for approval token.");
94
212
  }
95
213
  const currentDatabase = normalizeDatabase(ctx.database);
96
- if (ctx.datasource !== entry.datasource || currentDatabase !== entry.database) {
97
- return blockResult("CF004", "Datasource or database mismatch for confirmation token.");
214
+ if (ctx.datasource !== payload.datasource ||
215
+ currentDatabase !== payload.database ||
216
+ ctx.host !== payload.host ||
217
+ ctx.port !== payload.port ||
218
+ ctx.projectId !== payload.projectId ||
219
+ ctx.instanceId !== payload.instanceId ||
220
+ ctx.nodeId !== payload.nodeId) {
221
+ return blockResult("CF004", "Datasource, database, or target mismatch for approval token.");
98
222
  }
99
223
  entry.usedAt = now;
100
- return allowResult();
224
+ return allowResult(payload.actor.trim());
101
225
  }
102
- async revoke(token) {
103
- this.entries.delete(token);
226
+ async revoke(requestId) {
227
+ this.entries.delete(requestId);
104
228
  }
105
229
  stop() {
106
230
  if (this.cleanupTimer) {
@@ -109,20 +233,20 @@ export class InMemoryConfirmationStore {
109
233
  }
110
234
  }
111
235
  cleanupExpired(now = this.now()) {
112
- for (const [token, entry] of this.entries.entries()) {
113
- if (entry.expiresAt <= now) {
114
- this.entries.delete(token);
236
+ for (const [requestId, entry] of this.entries.entries()) {
237
+ if (entry.payload.expiresAt <= now) {
238
+ this.entries.delete(requestId);
115
239
  }
116
240
  }
117
241
  }
118
- generateUniqueToken() {
242
+ generateUniqueRequestId() {
119
243
  for (let i = 0; i < 5; i += 1) {
120
- const token = `${TOKEN_PREFIX}${this.randomBytesFn(32).toString("base64url")}`;
121
- if (!this.entries.has(token)) {
122
- return token;
244
+ const requestId = this.randomBytesFn(24).toString("base64url");
245
+ if (!this.entries.has(requestId)) {
246
+ return requestId;
123
247
  }
124
248
  }
125
- throw new Error("Unable to generate unique confirmation token.");
249
+ throw new Error("Unable to generate unique approval request id.");
126
250
  }
127
251
  }
128
252
  export function createConfirmationStore(options = {}) {
@@ -8,6 +8,9 @@ export interface GuardrailRuntimeLimits {
8
8
  maxRows: number;
9
9
  maxColumns: number;
10
10
  maxFieldChars: number;
11
+ maxResultBytes: number;
12
+ maxBlobBytes: number;
13
+ maskAllColumns: boolean;
11
14
  }
12
15
  export interface GuardrailDecision {
13
16
  action: "allow" | "confirm" | "block";
@@ -1,6 +1,7 @@
1
1
  import { createSqlParser } from "./parser/index.js";
2
2
  import { classifySql } from "./sql-classifier.js";
3
- import { validateStaticRules, validateToolScope, } from "./sql-validator.js";
3
+ import { hasSensitiveColumnReference } from "./redaction.js";
4
+ import { validateStaticRules, validateDatabaseScope, validateToolScope, } from "./sql-validator.js";
4
5
  function dedupeCaseInsensitive(values) {
5
6
  const output = [];
6
7
  const seen = new Set();
@@ -54,10 +55,13 @@ function buildBaseDecision(input, normalizedSql, sqlHash, action, riskLevel, rea
54
55
  maxRows: input.context.limits.maxRows,
55
56
  maxColumns: input.context.limits.maxColumns,
56
57
  maxFieldChars: input.context.limits.maxFieldChars ?? 2048,
58
+ maxResultBytes: input.context.limits.maxResultBytes ?? 1048576,
59
+ maxBlobBytes: input.context.limits.maxBlobBytes ?? 65536,
60
+ maskAllColumns: false,
57
61
  },
58
62
  };
59
63
  }
60
- function mergeDecision(input, normalizedSql, sqlHash, validations, requiresExplain) {
64
+ function mergeDecision(input, normalizedSql, sqlHash, validations, requiresExplain, maskAllColumns = false) {
61
65
  let action = "allow";
62
66
  let riskLevel = "low";
63
67
  const reasonCodes = [];
@@ -68,7 +72,9 @@ function mergeDecision(input, normalizedSql, sqlHash, validations, requiresExpla
68
72
  reasonCodes.push(...validation.reasonCodes);
69
73
  riskHints.push(...validation.riskHints);
70
74
  }
71
- return buildBaseDecision(input, normalizedSql, sqlHash, action, riskLevel, reasonCodes, riskHints, requiresExplain);
75
+ const decision = buildBaseDecision(input, normalizedSql, sqlHash, action, riskLevel, reasonCodes, riskHints, requiresExplain);
76
+ decision.runtimeLimits.maskAllColumns = maskAllColumns;
77
+ return decision;
72
78
  }
73
79
  export class GuardrailImpl {
74
80
  parserFactory;
@@ -85,13 +91,17 @@ export class GuardrailImpl {
85
91
  const cls = classifySql(parseResult.ast, normalized, input.context.engine);
86
92
  const d1 = validateToolScope(input.toolName, cls);
87
93
  if (d1.action === "block") {
88
- return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1], false);
94
+ return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1], false, hasSensitiveColumnReference(cls.referencedColumns));
95
+ }
96
+ const databaseScope = validateDatabaseScope(cls, input.context.database);
97
+ if (databaseScope.action === "block") {
98
+ return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1, databaseScope], false, hasSensitiveColumnReference(cls.referencedColumns));
89
99
  }
90
100
  const d2 = validateStaticRules(cls);
91
101
  if (d2.action === "block") {
92
- return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1, d2], false);
102
+ return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1, databaseScope, d2], false, hasSensitiveColumnReference(cls.referencedColumns));
93
103
  }
94
- return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1, d2], false);
104
+ return mergeDecision(input, normalized.normalizedSql, normalized.sqlHash, [d1, databaseScope, d2], false, hasSensitiveColumnReference(cls.referencedColumns));
95
105
  }
96
106
  }
97
107
  export function createGuardrail(options = {}) {
@@ -12,6 +12,9 @@ export interface RedactionPolicy {
12
12
  maxRows: number;
13
13
  maxColumns: number;
14
14
  maxFieldChars: number;
15
+ maxResultBytes?: number;
16
+ maxBlobBytes?: number;
17
+ maskAllColumns?: boolean;
15
18
  sensitiveColumns?: Iterable<string>;
16
19
  sensitiveStrategy?: SensitiveStrategy;
17
20
  }
@@ -24,6 +27,8 @@ export interface RedactedQueryResult {
24
27
  rowTruncated: boolean;
25
28
  columnTruncated: boolean;
26
29
  fieldTruncated: boolean;
30
+ byteTruncated: boolean;
31
+ returnedBytes: number;
27
32
  redactedColumns: string[];
28
33
  droppedColumns: string[];
29
34
  truncatedColumns: string[];
@@ -31,4 +36,5 @@ export interface RedactedQueryResult {
31
36
  export interface ResultRedactor {
32
37
  redact(raw: RawQueryResult, policy: RedactionPolicy): RedactedQueryResult;
33
38
  }
39
+ export declare function hasSensitiveColumnReference(columns: Iterable<string>): boolean;
34
40
  export declare function createResultRedactor(): ResultRedactor;