@synapsor/client 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/bin/synapsor.mjs +294 -9
- package/package.json +1 -1
- package/synapsor.mjs +234 -0
package/README.md
CHANGED
|
@@ -101,6 +101,30 @@ await run.complete({ decision: "waiver_proposed" }, { status: "waiting_approval"
|
|
|
101
101
|
const graph = await run.explain();
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
Find the persisted run/evidence/proposal later by business object, workflow,
|
|
105
|
+
tenant, query fingerprint, or time window:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const activity = await db.agentActivity.search({
|
|
109
|
+
tenantId: "acme",
|
|
110
|
+
businessObjectId: "T-1042",
|
|
111
|
+
workflow: "support.ticket_refund_flow",
|
|
112
|
+
timeRange: { from: 123, to: 456 },
|
|
113
|
+
limit: 50,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This helper sends the native lookup form:
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
SELECT *
|
|
121
|
+
FROM AGENT ACTIVITY
|
|
122
|
+
WHERE tenant_id = 'acme'
|
|
123
|
+
AND business_object_id = 'T-1042'
|
|
124
|
+
ORDER BY created_at DESC
|
|
125
|
+
LIMIT 50;
|
|
126
|
+
```
|
|
127
|
+
|
|
104
128
|
Replay a stored capability run by using the numeric `agent_run_id` returned by
|
|
105
129
|
Synapsor. Deterministic replay returns the captured persisted run; comparison
|
|
106
130
|
modes can inspect the original snapshot, current state, a commit version, a
|
|
@@ -157,12 +181,33 @@ The worker validates Synapsor-provided mapping metadata, only writes allowlisted
|
|
|
157
181
|
columns, adds tenant and primary-key guards, checks optional conflict columns,
|
|
158
182
|
and reports `applied`, `conflict`, or `failed` back to Synapsor idempotently.
|
|
159
183
|
|
|
184
|
+
## CLI MCP Risk Review
|
|
185
|
+
|
|
186
|
+
The package installs a `synapsor` CLI. Use `synapsor mcp audit <target>` for a
|
|
187
|
+
static MCP database risk review of exported tool manifests, remote `tools/list`
|
|
188
|
+
endpoints, or stdio MCP servers.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
synapsor mcp audit ./tools-manifest.json
|
|
192
|
+
synapsor mcp audit https://mcp.example.com --bearer-env MCP_AUDIT_TOKEN --json
|
|
193
|
+
synapsor mcp audit 'stdio:node ./server.mjs' --timeout-ms 5000
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The audit flags patterns such as generic `execute_sql` tools, model-controlled
|
|
197
|
+
tenant/table/column inputs, model-callable approval or commit tools, missing
|
|
198
|
+
proposal boundaries, missing structured output schemas, and missing idempotency
|
|
199
|
+
or conflict-guard metadata. It never calls business tools during the audit.
|
|
200
|
+
|
|
201
|
+
This is a static MCP database risk review, not proof that an MCP server is
|
|
202
|
+
secure. MCP annotations are treated as descriptive hints, not enforcement.
|
|
203
|
+
|
|
160
204
|
## API Surface
|
|
161
205
|
|
|
162
206
|
- `execute(sql)` and `query(sql)`
|
|
163
207
|
- `setSession({...})`
|
|
164
208
|
- `invokeAgentCapability(name, args, options)`
|
|
165
209
|
- `agentRuns.start(...)`, `run.invokeCapability(...)`, `run.checkpoint(...)`, `run.complete(...)`, `run.explain(...)`
|
|
210
|
+
- `agentActivity.search({ tenantId, businessObjectId, workflow, timeRange, limit })`
|
|
166
211
|
- `replayAgentRun(id, { mode, version, timestamp, branchName })`
|
|
167
212
|
- `externalActions.claim(...)` and `externalActions.confirm(...)`
|
|
168
213
|
- `createAgentEval(...)` / `evals.create(...)` for run-history or `sourceTable` dataset evals
|
package/bin/synapsor.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
@@ -11,6 +12,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const { version: PACKAGE_VERSION } = require("../package.json");
|
|
13
14
|
const DEFAULT_BASE_URL = "https://synapsor.ai";
|
|
15
|
+
const MCP_AUDIT_DISCLAIMER = "This is a static MCP database risk review, not proof that an MCP server is secure.";
|
|
14
16
|
const ERROR_HINTS = new Map([
|
|
15
17
|
["AUTH_REQUIRED", "Set SYNAPSOR_API_KEY or run `synapsor config set api-key`."],
|
|
16
18
|
["BILLING_REQUIRED", "Builder resources require active or trialing Builder billing."],
|
|
@@ -172,6 +174,17 @@ function csvFlag(args, name) {
|
|
|
172
174
|
return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : [];
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
function boolFlag(args, name) {
|
|
178
|
+
return args.includes(name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function intFlag(args, name, fallback) {
|
|
182
|
+
const value = flagValue(args, name, "");
|
|
183
|
+
if (!value) return fallback;
|
|
184
|
+
const parsed = Number.parseInt(value, 10);
|
|
185
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
186
|
+
}
|
|
187
|
+
|
|
175
188
|
function resolveSecretRef(value) {
|
|
176
189
|
const text = String(value || "").trim();
|
|
177
190
|
if (text.startsWith("env:")) {
|
|
@@ -217,6 +230,250 @@ async function readStdinSecret() {
|
|
|
217
230
|
return value.trim();
|
|
218
231
|
}
|
|
219
232
|
|
|
233
|
+
async function loadMcpAuditTarget(target, args) {
|
|
234
|
+
if (!target) throw new Error("MCP_AUDIT_TARGET_REQUIRED: usage `synapsor mcp audit <target>`");
|
|
235
|
+
const timeoutMs = intFlag(args, "--timeout-ms", 5000);
|
|
236
|
+
if (/^https?:\/\//i.test(target)) {
|
|
237
|
+
return { target, kind: "remote_tools_list", payload: await fetchRemoteMcpTools(target, args, timeoutMs) };
|
|
238
|
+
}
|
|
239
|
+
if (target.startsWith("stdio:")) {
|
|
240
|
+
const command = target.slice("stdio:".length).trim();
|
|
241
|
+
if (!command) throw new Error("MCP_STDIO_COMMAND_REQUIRED: usage `synapsor mcp audit 'stdio:node server.js'`");
|
|
242
|
+
return { target, kind: "stdio_tools_list", payload: await fetchStdioMcpTools(command, timeoutMs) };
|
|
243
|
+
}
|
|
244
|
+
const raw = await readFile(target, "utf8");
|
|
245
|
+
return { target, kind: "manifest", payload: JSON.parse(raw) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function fetchRemoteMcpTools(url, args, timeoutMs) {
|
|
249
|
+
const bearerEnv = flagValue(args, "--bearer-env", "SYNAPSOR_MCP_AUDIT_BEARER");
|
|
250
|
+
const bearer = bearerEnv ? process.env[bearerEnv] || "" : "";
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
signal: controller.signal,
|
|
257
|
+
headers: {
|
|
258
|
+
accept: "application/json",
|
|
259
|
+
"content-type": "application/json",
|
|
260
|
+
...(bearer ? { authorization: `Bearer ${bearer}` } : {}),
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
263
|
+
});
|
|
264
|
+
const text = await response.text();
|
|
265
|
+
const payload = text.trim() ? JSON.parse(text) : {};
|
|
266
|
+
if (!response.ok) throw new Error(`MCP_AUDIT_REMOTE_FAILED: HTTP ${response.status}`);
|
|
267
|
+
return payload;
|
|
268
|
+
} finally {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function splitCommand(command) {
|
|
274
|
+
const parts = [];
|
|
275
|
+
const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
276
|
+
let match;
|
|
277
|
+
while ((match = matcher.exec(command)) !== null) {
|
|
278
|
+
parts.push(match[1] ?? match[2] ?? match[3]);
|
|
279
|
+
}
|
|
280
|
+
return parts;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function fetchStdioMcpTools(command, timeoutMs) {
|
|
284
|
+
const [bin, ...args] = splitCommand(command);
|
|
285
|
+
if (!bin) throw new Error("MCP_STDIO_COMMAND_REQUIRED: empty stdio command");
|
|
286
|
+
return await new Promise((resolve, reject) => {
|
|
287
|
+
const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
288
|
+
let stdout = "";
|
|
289
|
+
let stderr = "";
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
child.kill("SIGTERM");
|
|
292
|
+
reject(new Error(`MCP_AUDIT_TIMEOUT: tools/list did not complete within ${timeoutMs}ms`));
|
|
293
|
+
}, timeoutMs);
|
|
294
|
+
child.stdout.on("data", (chunk) => {
|
|
295
|
+
stdout += String(chunk);
|
|
296
|
+
const response = parseJsonRpcLine(stdout, 2);
|
|
297
|
+
if (response) {
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
child.kill("SIGTERM");
|
|
300
|
+
resolve(response);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
child.stderr.on("data", (chunk) => {
|
|
304
|
+
stderr += String(chunk);
|
|
305
|
+
});
|
|
306
|
+
child.on("error", (error) => {
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
reject(error);
|
|
309
|
+
});
|
|
310
|
+
child.on("close", () => {
|
|
311
|
+
const response = parseJsonRpcLine(stdout, 2);
|
|
312
|
+
if (response) return;
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
reject(new Error(`MCP_AUDIT_STDIO_FAILED: tools/list response not found${stderr ? `: ${stderr.slice(0, 240)}` : ""}`));
|
|
315
|
+
});
|
|
316
|
+
child.stdin.write(`${JSON.stringify({
|
|
317
|
+
jsonrpc: "2.0",
|
|
318
|
+
id: 1,
|
|
319
|
+
method: "initialize",
|
|
320
|
+
params: {
|
|
321
|
+
protocolVersion: "2025-11-25",
|
|
322
|
+
capabilities: {},
|
|
323
|
+
clientInfo: { name: "synapsor-mcp-audit", version: PACKAGE_VERSION },
|
|
324
|
+
},
|
|
325
|
+
})}\n`);
|
|
326
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })}\n`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseJsonRpcLine(text, id) {
|
|
331
|
+
for (const line of text.split(/\r?\n/)) {
|
|
332
|
+
if (!line.trim()) continue;
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(line);
|
|
335
|
+
if (parsed.id === id) return parsed;
|
|
336
|
+
} catch {
|
|
337
|
+
// Ignore non-JSON logs from a local stdio server.
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function extractMcpTools(payload) {
|
|
344
|
+
if (Array.isArray(payload)) return { tools: payload, source: "array_manifest" };
|
|
345
|
+
if (!payload || typeof payload !== "object") return { tools: [], source: "unknown" };
|
|
346
|
+
if (Array.isArray(payload.tools)) return { tools: payload.tools, source: "tool_manifest" };
|
|
347
|
+
if (Array.isArray(payload.result?.tools)) return { tools: payload.result.tools, source: "json_rpc_tools_list" };
|
|
348
|
+
if (Array.isArray(payload.tool_manifest?.tools)) return { tools: payload.tool_manifest.tools, source: "tool_manifest" };
|
|
349
|
+
if (payload.mcpServers && typeof payload.mcpServers === "object") {
|
|
350
|
+
return { tools: [], source: "mcp_client_config", serverCount: Object.keys(payload.mcpServers).length };
|
|
351
|
+
}
|
|
352
|
+
return { tools: [], source: "unknown" };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function propNames(schema) {
|
|
356
|
+
if (!schema || typeof schema !== "object") return [];
|
|
357
|
+
const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
|
|
358
|
+
return Object.keys(properties);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function schemaText(schema) {
|
|
362
|
+
try {
|
|
363
|
+
return JSON.stringify(schema || {}).toLowerCase();
|
|
364
|
+
} catch {
|
|
365
|
+
return "";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function hasAny(text, patterns) {
|
|
370
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildMcpAuditReport(targetInfo) {
|
|
374
|
+
const { tools, source, serverCount } = extractMcpTools(targetInfo.payload);
|
|
375
|
+
const findings = [];
|
|
376
|
+
const add = (severity, code, message, details = {}) => findings.push({ severity, code, message, ...details });
|
|
377
|
+
if (source === "mcp_client_config") {
|
|
378
|
+
add(
|
|
379
|
+
"LOW",
|
|
380
|
+
"MCP_CLIENT_CONFIG_ONLY",
|
|
381
|
+
"MCP client config found; export or query tools/list for schema-level database risk review.",
|
|
382
|
+
{ evidence: `${serverCount || 0} configured MCP server(s)` },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (!tools.length && source !== "mcp_client_config") {
|
|
386
|
+
add("MEDIUM", "NO_TOOLS_FOUND", "No MCP tools were found in the target manifest or tools/list response.");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const tool of tools) {
|
|
390
|
+
const name = String(tool.name || tool.id || tool.title || "unnamed_tool");
|
|
391
|
+
const description = String(tool.description || "");
|
|
392
|
+
const inputSchema = tool.inputSchema || tool.input_schema || tool.parameters || {};
|
|
393
|
+
const outputSchema = tool.outputSchema || tool.output_schema || tool.resultSchema || tool.result_schema || null;
|
|
394
|
+
const props = propNames(inputSchema);
|
|
395
|
+
const propsLower = props.map((prop) => prop.toLowerCase());
|
|
396
|
+
const text = `${name} ${description} ${schemaText(inputSchema)} ${schemaText(outputSchema)}`.toLowerCase();
|
|
397
|
+
const writeTool = hasAny(text, [
|
|
398
|
+
/\b(writes?|written|updates?|updated|deletes?|deleted|inserts?|inserted|upserts?|upserted|mutates?|mutated|refunds?|refunded|waives?|waived|charges?|charged|credits?|credited|commits?|committed|settles?|settled|merges?|merged)\b/,
|
|
399
|
+
/(^|[._-])(approve|close|resolve|cancel)([._-]|$)/,
|
|
400
|
+
]);
|
|
401
|
+
const readTool = hasAny(text, [/\b(read|get|list|query|inspect|fetch|search)\b/]);
|
|
402
|
+
const proposalBoundary = hasAny(text, [/\b(proposal|propose|approval|review_required|writeback|change[-_ ]?set|diff|evidence)\b/]);
|
|
403
|
+
const idempotency = hasAny(text, [/\b(idempotency|idempotent|request_id|requestid|idempotency_key|idempotencykey)\b/]);
|
|
404
|
+
const conflictGuard = hasAny(text, [/\b(expected_version|row_version|updated_at|etag|version|conflict_guard|conflict guard|optimistic)\b/]);
|
|
405
|
+
|
|
406
|
+
if (/(^|[._-])(execute_sql|exec_sql|run_query|raw_sql|sql_query|query_sql|execute_query)([._-]|$)/.test(name.toLowerCase())) {
|
|
407
|
+
add("HIGH", "GENERIC_SQL_TOOL", "Generic execute_sql or run_query-style database tool exposed.", { tool: name });
|
|
408
|
+
}
|
|
409
|
+
if (propsLower.some((prop) => ["sql", "query", "statement", "raw_sql"].includes(prop)) && writeTool) {
|
|
410
|
+
add("HIGH", "WRITE_ACCEPTS_SQL", "Write-capable tool accepts arbitrary SQL/query/statement input.", { tool: name, evidence: props.join(", ") });
|
|
411
|
+
}
|
|
412
|
+
if (propsLower.some((prop) => ["schema", "table", "table_name", "tablename", "column", "columns", "database", "db"].includes(prop))) {
|
|
413
|
+
add("HIGH", "MODEL_CONTROLLED_IDENTIFIER", "Tool accepts schema/table/column identifiers as ordinary model input.", { tool: name, evidence: props.join(", ") });
|
|
414
|
+
}
|
|
415
|
+
if (propsLower.some((prop) => ["tenant", "tenant_id", "tenantid"].includes(prop))) {
|
|
416
|
+
add("HIGH", "MODEL_CONTROLLED_TENANT", "Tool accepts tenant_id as ordinary model input instead of trusted session context.", { tool: name, evidence: props.join(", ") });
|
|
417
|
+
}
|
|
418
|
+
if (/(^|[._-])(approve|commit|settle|merge)[._-]?(proposal|write|change)|(^|[._-])(proposal|write|change)[._-]?(approve|commit|settle|merge)([._-]|$)/.test(name.toLowerCase())) {
|
|
419
|
+
add("HIGH", "MODEL_CALLABLE_APPROVAL", "Approval or commit operation is exposed as a model-callable MCP tool.", { tool: name });
|
|
420
|
+
}
|
|
421
|
+
if (writeTool && !proposalBoundary) {
|
|
422
|
+
add("HIGH", "NO_VISIBLE_PROPOSAL_BOUNDARY", "Write-capable tool has no visible proposal, approval, evidence, or writeback boundary.", { tool: name });
|
|
423
|
+
}
|
|
424
|
+
if (!outputSchema) {
|
|
425
|
+
add("MEDIUM", "NO_STRUCTURED_OUTPUT_SCHEMA", "Tool has no structured output schema visible in the manifest.", { tool: name });
|
|
426
|
+
}
|
|
427
|
+
if (writeTool && !idempotency) {
|
|
428
|
+
add("MEDIUM", "NO_IDEMPOTENCY_KEY", "Write/proposal tool has no visible idempotency or request-key metadata.", { tool: name });
|
|
429
|
+
}
|
|
430
|
+
if (writeTool && !conflictGuard) {
|
|
431
|
+
add("MEDIUM", "NO_CONFLICT_GUARD", "Write/proposal tool has no visible row-version or conflict-guard metadata.", { tool: name });
|
|
432
|
+
}
|
|
433
|
+
if (readTool && writeTool) {
|
|
434
|
+
add("MEDIUM", "AMBIGUOUS_READ_WRITE_TOOL", "Tool mixes read and write behavior ambiguously.", { tool: name });
|
|
435
|
+
}
|
|
436
|
+
if (!tool.annotations) {
|
|
437
|
+
add("LOW", "MISSING_TOOL_ANNOTATIONS", "Tool is missing descriptive MCP annotations. Annotations are hints, not enforcement.", { tool: name });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const severityRank = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
442
|
+
findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || String(a.tool || "").localeCompare(String(b.tool || "")) || a.code.localeCompare(b.code));
|
|
443
|
+
const summary = { high: 0, medium: 0, low: 0 };
|
|
444
|
+
for (const finding of findings) summary[finding.severity.toLowerCase()] += 1;
|
|
445
|
+
return {
|
|
446
|
+
ok: true,
|
|
447
|
+
target: targetInfo.target,
|
|
448
|
+
source,
|
|
449
|
+
inspected_at: new Date().toISOString(),
|
|
450
|
+
disclaimer: MCP_AUDIT_DISCLAIMER,
|
|
451
|
+
tool_count: tools.length,
|
|
452
|
+
summary,
|
|
453
|
+
findings,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function printMcpAudit(report, globals) {
|
|
458
|
+
if (globals.json) return print(report, globals);
|
|
459
|
+
console.log("Static MCP database risk review");
|
|
460
|
+
console.log(`Target: ${report.target}`);
|
|
461
|
+
console.log(`Tools inspected: ${report.tool_count}`);
|
|
462
|
+
console.log(`Summary: HIGH=${report.summary.high} MEDIUM=${report.summary.medium} LOW=${report.summary.low}`);
|
|
463
|
+
console.log(MCP_AUDIT_DISCLAIMER);
|
|
464
|
+
if (!report.findings.length) {
|
|
465
|
+
console.log("No matching database MCP risk patterns found. This is not a security guarantee.");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log("\nFindings:");
|
|
469
|
+
for (const finding of report.findings) {
|
|
470
|
+
const tool = finding.tool ? ` tool=${finding.tool}` : "";
|
|
471
|
+
const evidence = finding.evidence ? ` evidence=${finding.evidence}` : "";
|
|
472
|
+
console.log(`${finding.severity.padEnd(6)} ${finding.code}${tool}${evidence}`);
|
|
473
|
+
console.log(` ${finding.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
220
477
|
async function main(argv = process.argv.slice(2)) {
|
|
221
478
|
const { globals, args } = parse(argv);
|
|
222
479
|
const [cmd, sub, third, ...rest] = args;
|
|
@@ -282,24 +539,35 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
282
539
|
const envHint = kind === "mysql" ? "APP_MYSQL_URL" : "APP_POSTGRES_URL";
|
|
283
540
|
const name = flagValue(rest, "--name", defaultName);
|
|
284
541
|
const url = resolveSecretRef(flagValue(rest, "--url", ""));
|
|
542
|
+
const cdcUrl = resolveSecretRef(flagValue(rest, "--cdc-url", ""));
|
|
285
543
|
const ssl = flagValue(rest, "--ssl", "require");
|
|
286
|
-
const mode = flagValue(rest, "--mode", "
|
|
544
|
+
const mode = flagValue(rest, "--mode", "live_read");
|
|
287
545
|
const projectId = scopedProject(globals);
|
|
288
546
|
const databaseId = scopedDatabase(globals);
|
|
289
547
|
if (!projectId || !databaseId) throw new Error("PROJECT_AND_DATABASE_REQUIRED: use --project <project_id> --db <database_id> or SYNAPSOR_PROJECT_ID/SYNAPSOR_DATABASE_ID");
|
|
290
548
|
if (!url) throw new Error(`${kind.toUpperCase()}_URL_REQUIRED: usage \`synapsor sources create ${kind} --name ${defaultName} --url env:${envHint} --project <project> --db <database>\``);
|
|
291
|
-
|
|
549
|
+
const body = {
|
|
292
550
|
project_id: projectId,
|
|
293
551
|
database_id: databaseId,
|
|
294
552
|
kind,
|
|
295
553
|
name,
|
|
296
554
|
connection: { url, ssl_mode: ssl },
|
|
297
555
|
mode,
|
|
298
|
-
}
|
|
556
|
+
};
|
|
557
|
+
if (cdcUrl) body.cdc_connection = { url: cdcUrl, ssl_mode: ssl };
|
|
558
|
+
if (mode === "cdc_mirror" || boolFlag(rest, "--cdc-ack")) body.cdc_acknowledged = true;
|
|
559
|
+
return print(await request(globals, "POST", "/v1/control/external-sources", body), globals);
|
|
299
560
|
}
|
|
300
|
-
if (["test", "inspect", "generate", "doctor", "disable"].includes(sub)) {
|
|
561
|
+
if (["test", "inspect", "generate", "doctor", "disable", "cdc-status", "cdc-snapshot", "cdc-poll"].includes(sub)) {
|
|
301
562
|
const sourceId = await resolveSourceId(globals, third);
|
|
302
563
|
if (sub === "test") return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/test`, {}), globals);
|
|
564
|
+
if (sub === "cdc-status") return print(await request(globals, "GET", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/cdc/status`), globals);
|
|
565
|
+
if (sub === "cdc-snapshot" || sub === "cdc-poll") {
|
|
566
|
+
const tables = csvFlag(rest, "--tables");
|
|
567
|
+
const body = { cdc_acknowledged: true };
|
|
568
|
+
if (tables.length) body.tables = tables;
|
|
569
|
+
return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/cdc/${sub === "cdc-snapshot" ? "snapshot" : "poll"}`, body), globals);
|
|
570
|
+
}
|
|
303
571
|
if (sub === "inspect") {
|
|
304
572
|
const schema = flagValue(rest, "--schema", "public");
|
|
305
573
|
const database = flagValue(rest, "--database", "");
|
|
@@ -340,10 +608,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
340
608
|
const database = flagValue(rest, "--database", "");
|
|
341
609
|
const tables = csvFlag(rest, "--tables");
|
|
342
610
|
const tenantColumn = flagValue(rest, "--tenant-column", "");
|
|
343
|
-
const mode = flagValue(rest, "--mode", "
|
|
611
|
+
const mode = flagValue(rest, "--mode", "live_read");
|
|
344
612
|
if (!tables.length) throw new Error("TABLES_REQUIRED: pass --tables tickets,customers,policy_chunks");
|
|
345
613
|
if (!tenantColumn && !rest.includes("--single-tenant")) throw new Error("TENANT_COLUMN_REQUIRED: pass --tenant-column tenant_id or --single-tenant");
|
|
346
|
-
|
|
614
|
+
const body = {
|
|
347
615
|
tables: tables.map((table) => ({
|
|
348
616
|
...(database ? { database } : {}),
|
|
349
617
|
schema,
|
|
@@ -354,10 +622,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
354
622
|
single_tenant: rest.includes("--single-tenant"),
|
|
355
623
|
mode,
|
|
356
624
|
})),
|
|
357
|
-
}
|
|
625
|
+
};
|
|
626
|
+
if (mode === "cdc_mirror" || boolFlag(rest, "--cdc-ack")) body.cdc_acknowledged = true;
|
|
627
|
+
return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/import`, body), globals);
|
|
358
628
|
}
|
|
359
629
|
}
|
|
360
630
|
|
|
631
|
+
if (cmd === "mcp" && sub === "audit") {
|
|
632
|
+
const targetInfo = await loadMcpAuditTarget(third, rest);
|
|
633
|
+
return printMcpAudit(buildMcpAuditReport(targetInfo), globals);
|
|
634
|
+
}
|
|
635
|
+
|
|
361
636
|
if (cmd === "sql") {
|
|
362
637
|
const db = globals.db || "";
|
|
363
638
|
let sql = args.slice(1).join(" ").trim();
|
|
@@ -408,8 +683,8 @@ Usage:
|
|
|
408
683
|
synapsor projects create <name>
|
|
409
684
|
synapsor db list --project <project>
|
|
410
685
|
synapsor db create <name> --project <project>
|
|
411
|
-
synapsor sources create postgres --name app_postgres --url env:APP_POSTGRES_URL --ssl require --mode
|
|
412
|
-
synapsor sources create mysql --name app_mysql --url env:APP_MYSQL_URL --ssl require --mode
|
|
686
|
+
synapsor sources create postgres --name app_postgres --url env:APP_POSTGRES_URL --ssl require --mode live_read --project <project> --db <database>
|
|
687
|
+
synapsor sources create mysql --name app_mysql --url env:APP_MYSQL_URL --ssl require --mode live_read --project <project> --db <database>
|
|
413
688
|
synapsor sources test app_postgres --project <project>
|
|
414
689
|
synapsor sources inspect app_postgres --schema public --project <project>
|
|
415
690
|
synapsor sources inspect app_mysql --database shopdb --project <project>
|
|
@@ -421,6 +696,9 @@ Usage:
|
|
|
421
696
|
synapsor sources show app_postgres --project <project>
|
|
422
697
|
synapsor sources doctor app_postgres --project <project>
|
|
423
698
|
synapsor sources disable app_postgres --project <project> --yes
|
|
699
|
+
synapsor mcp audit ./tools-manifest.json
|
|
700
|
+
synapsor mcp audit https://mcp.example.com --bearer-env MCP_AUDIT_TOKEN --json
|
|
701
|
+
synapsor mcp audit 'stdio:node ./server.mjs' --timeout-ms 5000
|
|
424
702
|
synapsor sql --db <database> "SELECT 1;"
|
|
425
703
|
synapsor sql --db <database> --file ./query.sql
|
|
426
704
|
synapsor invoke <capability> --db <database> --json '{"ticket_id":"TICK-1001"}'
|
|
@@ -430,6 +708,13 @@ Usage:
|
|
|
430
708
|
synapsor proposal reject <proposal-handle> --db <database> --yes
|
|
431
709
|
synapsor replay <run-id> --db <database>
|
|
432
710
|
|
|
711
|
+
Preview/dev CDC commands:
|
|
712
|
+
These are not the current public customer connector path. Use live_read unless you are running a reviewed dev/test CDC smoke.
|
|
713
|
+
synapsor sources import app_postgres --schema public --tables tickets --tenant-column tenant_id --mode cdc_mirror --cdc-ack --project <project>
|
|
714
|
+
synapsor sources cdc-snapshot app_postgres --tables external_support.tickets --project <project>
|
|
715
|
+
synapsor sources cdc-poll app_postgres --tables external_support.tickets --project <project>
|
|
716
|
+
synapsor sources cdc-status app_postgres --project <project>
|
|
717
|
+
|
|
433
718
|
Environment:
|
|
434
719
|
SYNAPSOR_API_KEY, SYNAPSOR_BASE_URL
|
|
435
720
|
|
package/package.json
CHANGED
package/synapsor.mjs
CHANGED
|
@@ -19,6 +19,17 @@ export {
|
|
|
19
19
|
PROTOCOL_VERSION,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
export const WRITEBACK_LEASE_EXPIRED = "WRITEBACK_LEASE_EXPIRED";
|
|
23
|
+
export const WRITEBACK_ALREADY_COMPLETED = "WRITEBACK_ALREADY_COMPLETED";
|
|
24
|
+
export const WRITEBACK_CONFLICT = "WRITEBACK_CONFLICT";
|
|
25
|
+
export const WRITEBACK_SAFETY_FAILURE = "WRITEBACK_SAFETY_FAILURE";
|
|
26
|
+
export const WRITEBACK_ERROR_CODES = Object.freeze([
|
|
27
|
+
WRITEBACK_LEASE_EXPIRED,
|
|
28
|
+
WRITEBACK_ALREADY_COMPLETED,
|
|
29
|
+
WRITEBACK_CONFLICT,
|
|
30
|
+
WRITEBACK_SAFETY_FAILURE,
|
|
31
|
+
]);
|
|
32
|
+
|
|
22
33
|
export class SynapsorError extends Error {
|
|
23
34
|
constructor(
|
|
24
35
|
message,
|
|
@@ -80,6 +91,7 @@ export class Synapsor {
|
|
|
80
91
|
this.apiKey = apiKey;
|
|
81
92
|
this.process = process;
|
|
82
93
|
this.timeoutMs = timeoutMs;
|
|
94
|
+
this.agentActivity = new AgentActivityNamespace(this);
|
|
83
95
|
this.agentRuns = new AgentRunsNamespace(this);
|
|
84
96
|
this.externalActions = new ExternalActionsNamespace(this);
|
|
85
97
|
this.evals = new AgentEvalsNamespace(this);
|
|
@@ -268,6 +280,13 @@ export class Synapsor {
|
|
|
268
280
|
return results.at(-1).result?.rows ?? [];
|
|
269
281
|
}
|
|
270
282
|
|
|
283
|
+
async searchAgentActivity(filters = {}, options = {}) {
|
|
284
|
+
return this.query(buildAgentActivitySearchSql(filters, options), {
|
|
285
|
+
session: options.session,
|
|
286
|
+
asOf: options.asOf ?? options.as_of,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
271
290
|
async invokeAgentCapability(capability, args = {}, {
|
|
272
291
|
session = undefined,
|
|
273
292
|
traceId = undefined,
|
|
@@ -636,6 +655,10 @@ export class Synapsor {
|
|
|
636
655
|
});
|
|
637
656
|
}
|
|
638
657
|
|
|
658
|
+
async listAdapterTools(adapter, options = {}) {
|
|
659
|
+
return this.adapterTools(adapter, options);
|
|
660
|
+
}
|
|
661
|
+
|
|
639
662
|
async callAdapterTool(adapter, tool, {
|
|
640
663
|
arguments: args = {},
|
|
641
664
|
runId = undefined,
|
|
@@ -661,6 +684,138 @@ export class Synapsor {
|
|
|
661
684
|
return this.request("POST", "/v1/agent/adapters/call-tool", payload);
|
|
662
685
|
}
|
|
663
686
|
|
|
687
|
+
async createWritebackRunnerToken(sourceId, {
|
|
688
|
+
runnerId = undefined,
|
|
689
|
+
runner_id = undefined,
|
|
690
|
+
tokenId = undefined,
|
|
691
|
+
token_id = undefined,
|
|
692
|
+
permissions = undefined,
|
|
693
|
+
expiresAt = undefined,
|
|
694
|
+
expires_at = undefined,
|
|
695
|
+
} = {}) {
|
|
696
|
+
const payload = {};
|
|
697
|
+
const runner = runnerId ?? runner_id;
|
|
698
|
+
const token = tokenId ?? token_id;
|
|
699
|
+
const expiry = expiresAt ?? expires_at;
|
|
700
|
+
if (runner !== undefined) payload.runner_id = runner;
|
|
701
|
+
if (token !== undefined) payload.token_id = token;
|
|
702
|
+
if (permissions !== undefined) payload.permissions = permissions;
|
|
703
|
+
if (expiry !== undefined) payload.expires_at = expiry;
|
|
704
|
+
return this.request("POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runner-tokens`, payload);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async createRunnerToken(sourceId, options = {}) {
|
|
708
|
+
return this.createWritebackRunnerToken(sourceId, options);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async listWritebackRunnerTokens(sourceId) {
|
|
712
|
+
return this.request("GET", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runner-tokens`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async listWritebackRunners(sourceId) {
|
|
716
|
+
return this.request("GET", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runners`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async listRunners(sourceId) {
|
|
720
|
+
return this.listWritebackRunners(sourceId);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async revokeWritebackRunnerToken(sourceId, tokenId, { reason = undefined } = {}) {
|
|
724
|
+
const payload = {};
|
|
725
|
+
if (reason !== undefined) payload.reason = reason;
|
|
726
|
+
return this.request("POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runner-tokens/${encodeURIComponent(tokenId)}/revoke`, payload);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async listExternalWritebackProposals({
|
|
730
|
+
projectId = undefined,
|
|
731
|
+
project_id = undefined,
|
|
732
|
+
sourceId = undefined,
|
|
733
|
+
source_id = undefined,
|
|
734
|
+
status = undefined,
|
|
735
|
+
limit = undefined,
|
|
736
|
+
} = {}) {
|
|
737
|
+
return this.request("GET", withQuery("/v1/control/external-writebacks/proposals", {
|
|
738
|
+
project_id: projectId ?? project_id,
|
|
739
|
+
source_id: sourceId ?? source_id,
|
|
740
|
+
status,
|
|
741
|
+
limit,
|
|
742
|
+
}));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async getExternalWritebackProposal(proposalId) {
|
|
746
|
+
return this.request("GET", `/v1/control/external-writebacks/proposals/${encodeURIComponent(proposalId)}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async getProposal(proposalId) {
|
|
750
|
+
return this.getExternalWritebackProposal(proposalId);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async createExternalWritebackProposal(payload) {
|
|
754
|
+
return this.request("POST", "/v1/control/external-writebacks/proposals", payload);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async transitionExternalWritebackProposal(proposalId, action, payload = {}) {
|
|
758
|
+
return this.request("POST", `/v1/control/external-writebacks/proposals/${encodeURIComponent(proposalId)}/${encodeURIComponent(action)}`, payload);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async approveExternalWritebackProposal(proposalId, payload = {}) {
|
|
762
|
+
return this.transitionExternalWritebackProposal(proposalId, "approve", payload);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async rejectExternalWritebackProposal(proposalId, payload = {}) {
|
|
766
|
+
return this.transitionExternalWritebackProposal(proposalId, "reject", payload);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async recordExternalWritebackApplyResult(proposalId, payload) {
|
|
770
|
+
return this.request("POST", `/v1/control/external-writebacks/proposals/${encodeURIComponent(proposalId)}/apply-result`, payload);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async doctorWritebackRunner() {
|
|
774
|
+
return this.request("GET", "/v1/writeback/runner/doctor");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async registerRunner(payload) {
|
|
778
|
+
return this.request("POST", "/v1/runner/register", payload);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async heartbeatRunner(payload) {
|
|
782
|
+
return this.request("POST", "/v1/runner/heartbeat", payload);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async claimWritebackJob({
|
|
786
|
+
sourceId = undefined,
|
|
787
|
+
source_id = undefined,
|
|
788
|
+
limit = undefined,
|
|
789
|
+
leaseSeconds = undefined,
|
|
790
|
+
lease_seconds = undefined,
|
|
791
|
+
} = {}) {
|
|
792
|
+
const payload = {};
|
|
793
|
+
const source = sourceId ?? source_id;
|
|
794
|
+
const lease = leaseSeconds ?? lease_seconds;
|
|
795
|
+
if (source !== undefined) payload.source_id = source;
|
|
796
|
+
if (limit !== undefined) payload.limit = Number(limit);
|
|
797
|
+
if (lease !== undefined) payload.lease_seconds = Number(lease);
|
|
798
|
+
return this.request("POST", "/v1/writeback/jobs/claim", payload);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async renewWritebackLease(jobId, {
|
|
802
|
+
leaseSeconds = undefined,
|
|
803
|
+
lease_seconds = undefined,
|
|
804
|
+
} = {}) {
|
|
805
|
+
const payload = {};
|
|
806
|
+
const lease = leaseSeconds ?? lease_seconds;
|
|
807
|
+
if (lease !== undefined) payload.lease_seconds = Number(lease);
|
|
808
|
+
return this.request("POST", `/v1/writeback/jobs/${encodeURIComponent(jobId)}/heartbeat`, payload);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async completeWritebackJob(jobId, result) {
|
|
812
|
+
return this.request("POST", `/v1/writeback/jobs/${encodeURIComponent(jobId)}/result`, result);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async recordWritebackJobResult(jobId, result) {
|
|
816
|
+
return this.completeWritebackJob(jobId, result);
|
|
817
|
+
}
|
|
818
|
+
|
|
664
819
|
async traceAgentRun(runId, { session = undefined } = {}) {
|
|
665
820
|
return this.request("POST", "/v1/agent/runs/trace", {
|
|
666
821
|
run_id: runId,
|
|
@@ -1045,6 +1200,10 @@ export class Synapsor {
|
|
|
1045
1200
|
return this.request("POST", "/v1/agent/runs/replay", payload);
|
|
1046
1201
|
}
|
|
1047
1202
|
|
|
1203
|
+
async getReplay(runId, options = {}) {
|
|
1204
|
+
return this.replayAgentRun(runId, options);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1048
1207
|
async proposalLifecycle(action, proposal, { session = undefined, promoteBranch = undefined, promote_branch = undefined, targetBranch = undefined, target_branch = undefined, settlementPolicy = undefined, settlement_policy = undefined } = {}) {
|
|
1049
1208
|
const payload = {
|
|
1050
1209
|
proposal,
|
|
@@ -1131,6 +1290,16 @@ export class Synapsor {
|
|
|
1131
1290
|
}
|
|
1132
1291
|
}
|
|
1133
1292
|
|
|
1293
|
+
class AgentActivityNamespace {
|
|
1294
|
+
constructor(client) {
|
|
1295
|
+
this.client = client;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async search(filters = {}, options = {}) {
|
|
1299
|
+
return this.client.searchAgentActivity(filters, options);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1134
1303
|
class AgentRunsNamespace {
|
|
1135
1304
|
constructor(client) {
|
|
1136
1305
|
this.client = client;
|
|
@@ -1396,6 +1565,60 @@ function typedSql(value) {
|
|
|
1396
1565
|
return `${identifier(ref.type)}(${sqlLiteral(String(ref.id ?? ""))})`;
|
|
1397
1566
|
}
|
|
1398
1567
|
|
|
1568
|
+
function buildAgentActivitySearchSql(filters = {}, options = {}) {
|
|
1569
|
+
const where = [];
|
|
1570
|
+
const addFilter = (column, ...keys) => {
|
|
1571
|
+
for (const key of keys) {
|
|
1572
|
+
if (filters[key] !== undefined && filters[key] !== null && filters[key] !== "") {
|
|
1573
|
+
where.push(`${column} = ${sqlScalar(filters[key])}`);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
addFilter("activity_kind", "activityKind", "activity_kind");
|
|
1579
|
+
addFilter("tenant_id", "tenantId", "tenant_id");
|
|
1580
|
+
addFilter("principal", "principal");
|
|
1581
|
+
addFilter("workflow", "workflow");
|
|
1582
|
+
addFilter("capability", "capability");
|
|
1583
|
+
addFilter("run_id", "runId", "run_id");
|
|
1584
|
+
addFilter("evidence_bundle_id", "evidenceBundleId", "evidence_bundle_id");
|
|
1585
|
+
addFilter("proposal_id", "proposalId", "proposal_id");
|
|
1586
|
+
addFilter("replay_id", "replayId", "replay_id");
|
|
1587
|
+
addFilter("business_object_type", "businessObjectType", "business_object_type");
|
|
1588
|
+
addFilter("business_object_id", "businessObjectId", "business_object_id");
|
|
1589
|
+
addFilter("query_fingerprint", "queryFingerprint", "query_fingerprint");
|
|
1590
|
+
|
|
1591
|
+
const timeRange = filters.timeRange ?? filters.time_range ?? {};
|
|
1592
|
+
const from = timeRange.from ?? filters.createdFrom ?? filters.created_from;
|
|
1593
|
+
const to = timeRange.to ?? filters.createdTo ?? filters.created_to;
|
|
1594
|
+
if (from !== undefined && from !== null && from !== "") {
|
|
1595
|
+
where.push(`created_at >= ${sqlScalar(from)}`);
|
|
1596
|
+
}
|
|
1597
|
+
if (to !== undefined && to !== null && to !== "") {
|
|
1598
|
+
where.push(`created_at <= ${sqlScalar(to)}`);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const requestedLimit = options.limit ?? filters.limit ?? 50;
|
|
1602
|
+
const limit = Number.isInteger(Number(requestedLimit)) && Number(requestedLimit) > 0
|
|
1603
|
+
? Math.min(Number(requestedLimit), 500)
|
|
1604
|
+
: 50;
|
|
1605
|
+
const whereSql = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
|
|
1606
|
+
return `SELECT * FROM AGENT ACTIVITY${whereSql} ORDER BY created_at DESC LIMIT ${limit};`;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function sqlScalar(value) {
|
|
1610
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1611
|
+
return String(Math.trunc(value));
|
|
1612
|
+
}
|
|
1613
|
+
if (typeof value === "bigint") {
|
|
1614
|
+
return String(value);
|
|
1615
|
+
}
|
|
1616
|
+
if (typeof value === "boolean") {
|
|
1617
|
+
return value ? "TRUE" : "FALSE";
|
|
1618
|
+
}
|
|
1619
|
+
return sqlLiteral(String(value));
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1399
1622
|
function sqlLiteral(value) {
|
|
1400
1623
|
return `'${String(value).replaceAll("'", "''")}'`;
|
|
1401
1624
|
}
|
|
@@ -1407,6 +1630,17 @@ function identifier(value) {
|
|
|
1407
1630
|
return value;
|
|
1408
1631
|
}
|
|
1409
1632
|
|
|
1633
|
+
function withQuery(path, params) {
|
|
1634
|
+
const query = new URLSearchParams();
|
|
1635
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1636
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
1637
|
+
query.set(key, String(value));
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const queryText = query.toString();
|
|
1641
|
+
return queryText ? `${path}?${queryText}` : path;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1410
1644
|
function repoRoot() {
|
|
1411
1645
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
1412
1646
|
}
|