whale-code 6.4.0 → 6.5.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.
- package/bin/swagmanager-mcp.js +7 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +66 -2
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +15 -3
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +71 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +45 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +61 -15
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +7 -4
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +5 -2
- package/dist/shared/agent-core.js +30 -4
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +1 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
package/dist/node/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NodeConfig } from "./config.js";
|
|
2
|
-
import type { MessagePayload } from "./adapters/base.js";
|
|
2
|
+
import type { BaseAdapter, MessagePayload } from "./adapters/base.js";
|
|
3
3
|
export type NodeStatus = "starting" | "connected" | "degraded" | "disconnected";
|
|
4
4
|
export interface NodeRuntimeStats {
|
|
5
5
|
status: NodeStatus;
|
|
@@ -32,10 +32,17 @@ export declare class NodeRuntime {
|
|
|
32
32
|
private heartbeatsFailed;
|
|
33
33
|
private messagesRelayed;
|
|
34
34
|
private messagesFailed;
|
|
35
|
+
private gatewayClient;
|
|
36
|
+
private screenCapture;
|
|
37
|
+
private portalManager;
|
|
35
38
|
constructor(config: NodeConfig);
|
|
36
39
|
getStats(): NodeRuntimeStats;
|
|
37
40
|
start(): Promise<void>;
|
|
38
41
|
stop(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Get a channel adapter by ID.
|
|
44
|
+
*/
|
|
45
|
+
getAdapter(channelId: string): BaseAdapter | undefined;
|
|
39
46
|
private startAdapter;
|
|
40
47
|
/** Fetch with retry and exponential backoff */
|
|
41
48
|
private fetchWithRetry;
|
package/dist/node/runtime.js
CHANGED
|
@@ -8,6 +8,9 @@ import { WhatsAppAdapter } from "./adapters/whatsapp.js";
|
|
|
8
8
|
import { SmsAdapter } from "./adapters/sms.js";
|
|
9
9
|
import { EmailAdapter } from "./adapters/email.js";
|
|
10
10
|
import { captureError, addBreadcrumb } from "../cli/services/error-logger.js";
|
|
11
|
+
import { GatewayClient } from "./gateway-client.js";
|
|
12
|
+
import { ScreenCaptureService } from "./remote-desktop/index.js";
|
|
13
|
+
import { PortalManager } from "./portal/index.js";
|
|
11
14
|
const HEARTBEAT_INTERVAL_MS = 60_000;
|
|
12
15
|
const POLL_INTERVAL_MS = 2_000;
|
|
13
16
|
const VERSION = "1.1.0";
|
|
@@ -30,6 +33,12 @@ export class NodeRuntime {
|
|
|
30
33
|
heartbeatsFailed = 0;
|
|
31
34
|
messagesRelayed = 0;
|
|
32
35
|
messagesFailed = 0;
|
|
36
|
+
// Server gateway connection
|
|
37
|
+
gatewayClient = null;
|
|
38
|
+
// Screen capture service (macOS only)
|
|
39
|
+
screenCapture = null;
|
|
40
|
+
// Portal manager (node-to-node connectivity)
|
|
41
|
+
portalManager = null;
|
|
33
42
|
constructor(config) {
|
|
34
43
|
this.config = config;
|
|
35
44
|
}
|
|
@@ -71,10 +80,79 @@ export class NodeRuntime {
|
|
|
71
80
|
for (const ch of this.config.channels) {
|
|
72
81
|
await this.startAdapter(ch);
|
|
73
82
|
}
|
|
83
|
+
// Connect to server gateway for receiving commands
|
|
84
|
+
this.gatewayClient = new GatewayClient({
|
|
85
|
+
serverUrl: this.config.server_url,
|
|
86
|
+
apiKey: this.config.api_key,
|
|
87
|
+
version: VERSION,
|
|
88
|
+
});
|
|
89
|
+
// Register exec command handler
|
|
90
|
+
this.gatewayClient.onCommand("exec", async (msg) => {
|
|
91
|
+
const { execSync } = await import("node:child_process");
|
|
92
|
+
try {
|
|
93
|
+
const stdout = execSync(msg.command, {
|
|
94
|
+
timeout: msg.timeout || 30_000,
|
|
95
|
+
cwd: process.env.HOME,
|
|
96
|
+
encoding: "utf-8",
|
|
97
|
+
maxBuffer: 512 * 1024,
|
|
98
|
+
});
|
|
99
|
+
return { stdout, exit_code: 0 };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return {
|
|
103
|
+
stdout: err.stdout || "",
|
|
104
|
+
stderr: err.stderr || err.message,
|
|
105
|
+
exit_code: err.status || 1,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// Register remote desktop command handler on gateway — forward input events
|
|
110
|
+
this.gatewayClient.onCommand("remote_desktop", async (msg) => {
|
|
111
|
+
if (!this.screenCapture) {
|
|
112
|
+
return { error: "Screen capture not enabled" };
|
|
113
|
+
}
|
|
114
|
+
if (msg.type === "input") {
|
|
115
|
+
this.screenCapture.injectInput(msg);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Return capture status for session requests
|
|
119
|
+
return {
|
|
120
|
+
type: "remote_desktop_response",
|
|
121
|
+
success: true,
|
|
122
|
+
...this.screenCapture.getStatus(),
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
this.gatewayClient.start();
|
|
126
|
+
// Start screen capture service (macOS only)
|
|
127
|
+
if (process.platform === "darwin" && this.config.remote_desktop?.enabled !== false) {
|
|
128
|
+
try {
|
|
129
|
+
this.screenCapture = new ScreenCaptureService(this.config.remote_desktop || {});
|
|
130
|
+
await this.screenCapture.start();
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.warn("[node] Screen capture failed to start:", err.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Initialize portal manager
|
|
137
|
+
try {
|
|
138
|
+
const portalConfig = this.config.portal || {};
|
|
139
|
+
this.portalManager = new PortalManager({
|
|
140
|
+
nodeConfig: this.config,
|
|
141
|
+
gateway: this.gatewayClient,
|
|
142
|
+
screenCapture: this.screenCapture,
|
|
143
|
+
receiveDir: portalConfig.receive_dir,
|
|
144
|
+
autoAcceptAdmins: portalConfig.auto_accept_admins !== false,
|
|
145
|
+
maxSessions: portalConfig.max_sessions || 5,
|
|
146
|
+
});
|
|
147
|
+
console.log("[node] Portal handler registered");
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.warn("[node] Portal handler failed to initialize:", err.message);
|
|
151
|
+
}
|
|
74
152
|
// Handle graceful shutdown
|
|
75
153
|
process.on("SIGTERM", () => this.stop());
|
|
76
154
|
process.on("SIGINT", () => this.stop());
|
|
77
|
-
console.log(`[node] Running with ${this.adapters.size} channel adapter(s).
|
|
155
|
+
console.log(`[node] Running with ${this.adapters.size} channel adapter(s). Gateway: connecting. Press Ctrl+C to stop.`);
|
|
78
156
|
}
|
|
79
157
|
async stop() {
|
|
80
158
|
console.log(`[node] Shutting down...`);
|
|
@@ -84,6 +162,19 @@ export class NodeRuntime {
|
|
|
84
162
|
clearInterval(this.heartbeatTimer);
|
|
85
163
|
for (const [, timer] of this.pollTimers)
|
|
86
164
|
clearInterval(timer);
|
|
165
|
+
// Stop portal sessions
|
|
166
|
+
if (this.portalManager) {
|
|
167
|
+
this.portalManager.closeAll();
|
|
168
|
+
this.portalManager = null;
|
|
169
|
+
}
|
|
170
|
+
// Stop screen capture
|
|
171
|
+
if (this.screenCapture) {
|
|
172
|
+
await this.screenCapture.stop();
|
|
173
|
+
this.screenCapture = null;
|
|
174
|
+
}
|
|
175
|
+
// Disconnect from server gateway
|
|
176
|
+
if (this.gatewayClient)
|
|
177
|
+
this.gatewayClient.stop();
|
|
87
178
|
for (const [name, adapter] of this.adapters) {
|
|
88
179
|
console.log(`[node] Stopping adapter: ${name}`);
|
|
89
180
|
await adapter.stop();
|
|
@@ -91,6 +182,12 @@ export class NodeRuntime {
|
|
|
91
182
|
console.log(`[node] Stopped.`);
|
|
92
183
|
process.exit(0);
|
|
93
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Get a channel adapter by ID.
|
|
187
|
+
*/
|
|
188
|
+
getAdapter(channelId) {
|
|
189
|
+
return this.adapters.get(channelId);
|
|
190
|
+
}
|
|
94
191
|
async startAdapter(ch) {
|
|
95
192
|
let adapter;
|
|
96
193
|
switch (ch.type) {
|
|
@@ -282,12 +379,21 @@ export class NodeRuntime {
|
|
|
282
379
|
}
|
|
283
380
|
async sendHeartbeat() {
|
|
284
381
|
try {
|
|
285
|
-
const channelStatuses = Array.from(this.adapters.entries()).map(([, adapter]) =>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
382
|
+
const channelStatuses = Array.from(this.adapters.entries()).map(([, adapter]) => {
|
|
383
|
+
const base = {
|
|
384
|
+
type: adapter.type,
|
|
385
|
+
name: adapter.name,
|
|
386
|
+
status: adapter.isRunning() ? "active" : "error",
|
|
387
|
+
stats: adapter.getStats(),
|
|
388
|
+
};
|
|
389
|
+
// Include diagnostics for iMessage adapters
|
|
390
|
+
if (adapter.type === "imessage" && "getLastError" in adapter) {
|
|
391
|
+
const imsg = adapter;
|
|
392
|
+
base.last_error = imsg.getLastError();
|
|
393
|
+
base.restart_attempts = imsg.getRestartAttempts();
|
|
394
|
+
}
|
|
395
|
+
return base;
|
|
396
|
+
});
|
|
291
397
|
const res = await this.fetchWithRetry(`${this.config.server_url}/nodes/heartbeat`, {
|
|
292
398
|
method: "POST",
|
|
293
399
|
headers: {
|
|
@@ -344,8 +450,10 @@ function getHardwareInfo() {
|
|
|
344
450
|
};
|
|
345
451
|
}
|
|
346
452
|
function getCapabilities() {
|
|
347
|
-
const caps = ["messaging", "discord", "slack", "telegram", "webchat", "whatsapp", "sms", "email"];
|
|
348
|
-
if (process.platform === "darwin")
|
|
453
|
+
const caps = ["messaging", "discord", "slack", "telegram", "webchat", "whatsapp", "sms", "email", "portal"];
|
|
454
|
+
if (process.platform === "darwin") {
|
|
349
455
|
caps.push("imessage");
|
|
456
|
+
caps.push("remote_desktop");
|
|
457
|
+
}
|
|
350
458
|
return caps;
|
|
351
459
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Supabase test helper for handler tests.
|
|
3
|
+
* Replaces mock-supabase.ts — all operations hit the real database.
|
|
4
|
+
*
|
|
5
|
+
* When SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / TEST_STORE_ID are missing,
|
|
6
|
+
* exports are stubbed so test files can skip gracefully via:
|
|
7
|
+
* describe.skipIf(!HAS_TEST_DB)("...", () => { ... })
|
|
8
|
+
*/
|
|
9
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
10
|
+
/** true when all required env vars are present — use with describe.skipIf(!HAS_TEST_DB) */
|
|
11
|
+
export declare const HAS_TEST_DB: boolean;
|
|
12
|
+
export declare const TEST_STORE: string;
|
|
13
|
+
/** Unique prefix per test run */
|
|
14
|
+
export declare const RUN_PREFIX: string;
|
|
15
|
+
/** Get the real Supabase client (pass to handlers as `sb`) */
|
|
16
|
+
export declare function getTestClient(): SupabaseClient;
|
|
17
|
+
/** Insert rows and return them with DB-generated fields */
|
|
18
|
+
export declare function seed(table: string, rows: Record<string, unknown>[]): Promise<any[]>;
|
|
19
|
+
/** Delete rows by IDs */
|
|
20
|
+
export declare function deleteByIds(table: string, ids: string[]): Promise<void>;
|
|
21
|
+
/** Delete rows matching column=value */
|
|
22
|
+
export declare function deleteWhere(table: string, column: string, value: unknown): Promise<void>;
|
|
23
|
+
export declare function seedTracked(table: string, rows: Record<string, unknown>[]): Promise<any[]>;
|
|
24
|
+
/** Delete all tracked rows in FK-safe order */
|
|
25
|
+
export declare function cleanup(): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Supabase test helper for handler tests.
|
|
3
|
+
* Replaces mock-supabase.ts — all operations hit the real database.
|
|
4
|
+
*
|
|
5
|
+
* When SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / TEST_STORE_ID are missing,
|
|
6
|
+
* exports are stubbed so test files can skip gracefully via:
|
|
7
|
+
* describe.skipIf(!HAS_TEST_DB)("...", () => { ... })
|
|
8
|
+
*/
|
|
9
|
+
import { createClient } from "@supabase/supabase-js";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
const url = process.env.SUPABASE_URL;
|
|
12
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
13
|
+
const storeId = process.env.TEST_STORE_ID;
|
|
14
|
+
/** true when all required env vars are present — use with describe.skipIf(!HAS_TEST_DB) */
|
|
15
|
+
export const HAS_TEST_DB = !!(url && key && storeId);
|
|
16
|
+
const sb = HAS_TEST_DB ? createClient(url, key) : null;
|
|
17
|
+
export const TEST_STORE = storeId ?? "missing-test-store";
|
|
18
|
+
/** Unique prefix per test run */
|
|
19
|
+
export const RUN_PREFIX = `T${randomUUID().slice(0, 6)}`;
|
|
20
|
+
/** Get the real Supabase client (pass to handlers as `sb`) */
|
|
21
|
+
export function getTestClient() {
|
|
22
|
+
return sb;
|
|
23
|
+
}
|
|
24
|
+
/** Insert rows and return them with DB-generated fields */
|
|
25
|
+
export async function seed(table, rows) {
|
|
26
|
+
if (!rows.length)
|
|
27
|
+
return [];
|
|
28
|
+
const { data, error } = await sb.from(table).insert(rows).select();
|
|
29
|
+
if (error)
|
|
30
|
+
throw new Error(`seed(${table}): ${error.message}`);
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
/** Delete rows by IDs */
|
|
34
|
+
export async function deleteByIds(table, ids) {
|
|
35
|
+
if (!ids.length)
|
|
36
|
+
return;
|
|
37
|
+
const { error } = await sb.from(table).delete().in("id", ids);
|
|
38
|
+
if (error)
|
|
39
|
+
throw new Error(`deleteByIds(${table}): ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
/** Delete rows matching column=value */
|
|
42
|
+
export async function deleteWhere(table, column, value) {
|
|
43
|
+
const { error } = await sb.from(table).delete().eq(column, value);
|
|
44
|
+
if (error)
|
|
45
|
+
throw new Error(`deleteWhere(${table}): ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
/** Track seeded data for automatic cleanup */
|
|
48
|
+
const tracked = new Map();
|
|
49
|
+
export async function seedTracked(table, rows) {
|
|
50
|
+
const data = await seed(table, rows);
|
|
51
|
+
const ids = data.map((r) => r.id);
|
|
52
|
+
tracked.set(table, [...(tracked.get(table) || []), ...ids]);
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
/** Delete all tracked rows in FK-safe order */
|
|
56
|
+
export async function cleanup() {
|
|
57
|
+
const fkOrder = [
|
|
58
|
+
"ai_messages",
|
|
59
|
+
"ai_conversation_checkpoints",
|
|
60
|
+
"ai_conversations",
|
|
61
|
+
"ai_agent_config",
|
|
62
|
+
"platform_secrets",
|
|
63
|
+
"business_audit",
|
|
64
|
+
"workflow_step_runs",
|
|
65
|
+
"workflow_dlq",
|
|
66
|
+
"workflow_runs",
|
|
67
|
+
"workflow_versions",
|
|
68
|
+
"workflow_steps",
|
|
69
|
+
"workflows",
|
|
70
|
+
"error_events",
|
|
71
|
+
"payment_intents",
|
|
72
|
+
"order_items",
|
|
73
|
+
"cart_items",
|
|
74
|
+
"webhook_events",
|
|
75
|
+
"inventory_levels",
|
|
76
|
+
"inventory",
|
|
77
|
+
"email_sends",
|
|
78
|
+
"orders",
|
|
79
|
+
"carts",
|
|
80
|
+
"products",
|
|
81
|
+
"categories",
|
|
82
|
+
"catalogs",
|
|
83
|
+
"creation_collections",
|
|
84
|
+
"customers",
|
|
85
|
+
"store_customer_profiles",
|
|
86
|
+
"user_creation_relationships",
|
|
87
|
+
"creations",
|
|
88
|
+
"locations",
|
|
89
|
+
"webhook_configs",
|
|
90
|
+
"webhook_endpoints",
|
|
91
|
+
"api_keys",
|
|
92
|
+
"platform_users",
|
|
93
|
+
"store_media",
|
|
94
|
+
"purchase_orders",
|
|
95
|
+
"suppliers",
|
|
96
|
+
"stock_transfers",
|
|
97
|
+
"v_daily_sales",
|
|
98
|
+
"meta_ads",
|
|
99
|
+
"meta_ad_sets",
|
|
100
|
+
"meta_campaigns",
|
|
101
|
+
"meta_integrations",
|
|
102
|
+
"channel_messages",
|
|
103
|
+
"channels",
|
|
104
|
+
"node_events",
|
|
105
|
+
"nodes",
|
|
106
|
+
"store_members",
|
|
107
|
+
"store_plans",
|
|
108
|
+
"store_usage",
|
|
109
|
+
"email_templates",
|
|
110
|
+
"email_threads",
|
|
111
|
+
"customer_risk_scores",
|
|
112
|
+
"customer_exposures",
|
|
113
|
+
"customer_breach_records",
|
|
114
|
+
"customer_enrichment_profiles",
|
|
115
|
+
"stores",
|
|
116
|
+
];
|
|
117
|
+
const orderedTables = [
|
|
118
|
+
...fkOrder.filter((t) => tracked.has(t)),
|
|
119
|
+
...[...tracked.keys()].filter((t) => !fkOrder.includes(t)),
|
|
120
|
+
];
|
|
121
|
+
for (const table of orderedTables) {
|
|
122
|
+
const ids = tracked.get(table);
|
|
123
|
+
if (ids?.length) {
|
|
124
|
+
await deleteByIds(table, ids).catch(() => { });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
tracked.clear();
|
|
128
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// server/handlers/api-keys.ts — API Key management: create, list, get, revoke, update
|
|
2
2
|
// Supports linking keys to creations (TV menus, displays, landing pages)
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
const KEY_COLS = "id, name, key_prefix, key_type, scope, is_active, rate_limit_per_minute, rate_limit_per_day, last_used_at, request_count, expires_at, revoked_at, revoked_reason, creation_id, created_at, updated_at";
|
|
4
|
+
const KEY_COLS = "id, name, key_prefix, key_type, scope, is_active, rate_limit_per_minute, rate_limit_per_day, last_used_at, request_count, expires_at, revoked_at, revoked_reason, creation_id, client_store_id, created_at, updated_at";
|
|
5
5
|
export async function handleAPIKeys(sb, args, storeId) {
|
|
6
6
|
const sid = storeId;
|
|
7
7
|
const action = args.action;
|
|
@@ -20,6 +20,7 @@ export async function handleAPIKeys(sb, args, storeId) {
|
|
|
20
20
|
const rateLimitPerDay = args.rate_limit_per_day || 10000;
|
|
21
21
|
const expiresAt = args.expires_at;
|
|
22
22
|
const creationId = args.creation_id;
|
|
23
|
+
const clientStoreId = args.client_store_id;
|
|
23
24
|
// Resolve owner — use provided owner_user_id or find store owner
|
|
24
25
|
let ownerUserId = args.owner_user_id;
|
|
25
26
|
if (!ownerUserId) {
|
|
@@ -65,6 +66,7 @@ export async function handleAPIKeys(sb, args, storeId) {
|
|
|
65
66
|
rate_limit_per_day: rateLimitPerDay,
|
|
66
67
|
expires_at: expiresAt || null,
|
|
67
68
|
creation_id: creationId || null,
|
|
69
|
+
client_store_id: clientStoreId || null,
|
|
68
70
|
}).select(KEY_COLS).single();
|
|
69
71
|
if (error)
|
|
70
72
|
return { success: false, error: error.message };
|
|
@@ -74,6 +76,17 @@ export async function handleAPIKeys(sb, args, storeId) {
|
|
|
74
76
|
...data,
|
|
75
77
|
key_value: keyValue,
|
|
76
78
|
warning: "Copy this key now. The full key value will NOT be returned again — only the hash is stored.",
|
|
79
|
+
api_gateway: {
|
|
80
|
+
base_url: "https://whale-gateway.fly.dev",
|
|
81
|
+
docs_url: "https://whale-gateway.fly.dev/docs",
|
|
82
|
+
openapi_spec: "https://whale-gateway.fly.dev/openapi.json",
|
|
83
|
+
usage: `curl -H "x-api-key: ${keyValue}" https://whale-gateway.fly.dev/v1/stores/${sid}/products`,
|
|
84
|
+
scopes: scopes,
|
|
85
|
+
rate_limits: {
|
|
86
|
+
per_minute: rateLimitPerMinute,
|
|
87
|
+
per_day: rateLimitPerDay,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
77
90
|
},
|
|
78
91
|
};
|
|
79
92
|
}
|
|
@@ -95,7 +108,18 @@ export async function handleAPIKeys(sb, args, storeId) {
|
|
|
95
108
|
const { data, error } = await q;
|
|
96
109
|
if (error)
|
|
97
110
|
return { success: false, error: error.message };
|
|
98
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
data: {
|
|
114
|
+
count: data?.length || 0,
|
|
115
|
+
keys: data,
|
|
116
|
+
api_gateway: {
|
|
117
|
+
base_url: "https://whale-gateway.fly.dev",
|
|
118
|
+
docs_url: "https://whale-gateway.fly.dev/docs",
|
|
119
|
+
openapi_spec: "https://whale-gateway.fly.dev/openapi.json",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
99
123
|
}
|
|
100
124
|
// ---- get: Get a single API key by ID ----
|
|
101
125
|
case "get": {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
export declare function generatePdfFromHtml(html: string, options?: {
|
|
3
|
-
format?: string;
|
|
4
|
-
landscape?: boolean;
|
|
5
|
-
}): Promise<Buffer>;
|
|
6
2
|
export declare function handleBrowser(_sb: SupabaseClient, args: Record<string, unknown>, _storeId?: string): Promise<{
|
|
7
3
|
success: boolean;
|
|
8
4
|
data?: unknown;
|
|
@@ -274,52 +274,6 @@ async function actionPdf(url, waitFor) {
|
|
|
274
274
|
};
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
|
-
// ============================================================================
|
|
278
|
-
// PDF FROM HTML (reusable by documents handler)
|
|
279
|
-
// ============================================================================
|
|
280
|
-
export async function generatePdfFromHtml(html, options) {
|
|
281
|
-
// P0 FIX: Strip dangerous tags from user HTML before rendering to prevent
|
|
282
|
-
// script execution, SSRF via iframes, and local file reads
|
|
283
|
-
const sanitizedHtml = html
|
|
284
|
-
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
285
|
-
.replace(/<script[^>]*\/?>/gi, "")
|
|
286
|
-
.replace(/<iframe[\s\S]*?<\/iframe>/gi, "")
|
|
287
|
-
.replace(/<iframe[^>]*\/?>/gi, "")
|
|
288
|
-
.replace(/<object[\s\S]*?<\/object>/gi, "")
|
|
289
|
-
.replace(/<object[^>]*\/?>/gi, "")
|
|
290
|
-
.replace(/<embed[^>]*\/?>/gi, "")
|
|
291
|
-
.replace(/<link[^>]*\/?>/gi, "");
|
|
292
|
-
const browser = await getBrowser();
|
|
293
|
-
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
294
|
-
const page = await context.newPage();
|
|
295
|
-
try {
|
|
296
|
-
// P0 FIX: Block all network requests to prevent SSRF from headless Chromium.
|
|
297
|
-
// Only allow data: URIs (inline images/fonts) and about:blank.
|
|
298
|
-
await page.route('**/*', (route) => {
|
|
299
|
-
const url = route.request().url();
|
|
300
|
-
if (url.startsWith('data:') || url === 'about:blank') {
|
|
301
|
-
route.continue();
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
route.abort();
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
// P0 FIX: Use domcontentloaded instead of networkidle to avoid waiting for
|
|
308
|
-
// (now-blocked) network requests and reduce attack surface
|
|
309
|
-
await page.setContent(sanitizedHtml, { waitUntil: "domcontentloaded", timeout: PAGE_TIMEOUT_MS });
|
|
310
|
-
const buffer = await page.pdf({
|
|
311
|
-
format: options?.format || "A4",
|
|
312
|
-
printBackground: true,
|
|
313
|
-
landscape: options?.landscape || false,
|
|
314
|
-
margin: { top: "1cm", bottom: "1cm", left: "1cm", right: "1cm" },
|
|
315
|
-
});
|
|
316
|
-
return Buffer.from(buffer);
|
|
317
|
-
}
|
|
318
|
-
finally {
|
|
319
|
-
await page.close().catch(() => { });
|
|
320
|
-
await context.close().catch(() => { });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
277
|
async function callCapMonster(taskType, params) {
|
|
324
278
|
const apiKey = process.env.CAPMONSTER_API_KEY;
|
|
325
279
|
if (!apiKey)
|
|
@@ -150,7 +150,11 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
150
150
|
if (args.pricing_data)
|
|
151
151
|
insert.pricing_data = args.pricing_data;
|
|
152
152
|
// custom_fields NOT inserted directly — schema is the source of truth
|
|
153
|
-
// Agent-provided
|
|
153
|
+
// Agent-provided custom_fields are filtered against schema keys post-insert
|
|
154
|
+
if (args.featured_image !== undefined)
|
|
155
|
+
insert.featured_image = args.featured_image;
|
|
156
|
+
if (args.image_gallery !== undefined)
|
|
157
|
+
insert.image_gallery = args.image_gallery;
|
|
154
158
|
if (args.is_wholesale !== undefined)
|
|
155
159
|
insert.is_wholesale = args.is_wholesale;
|
|
156
160
|
if (args.wholesale_only !== undefined)
|
|
@@ -203,7 +207,7 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
209
|
// Only accept agent values for keys that exist in the schema
|
|
206
|
-
const agentValues = args.
|
|
210
|
+
const agentValues = args.custom_fields || {};
|
|
207
211
|
for (const [k, v] of Object.entries(agentValues)) {
|
|
208
212
|
if (schemaKeys.has(k))
|
|
209
213
|
fieldValues[k] = v;
|
|
@@ -279,8 +283,8 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
279
283
|
if (args.pricing_data !== undefined)
|
|
280
284
|
updates.pricing_data = args.pricing_data;
|
|
281
285
|
// custom_fields filtered to schema keys only (schema = source of truth)
|
|
282
|
-
if (args.
|
|
283
|
-
const agentFV = args.
|
|
286
|
+
if (args.custom_fields !== undefined) {
|
|
287
|
+
const agentFV = args.custom_fields;
|
|
284
288
|
// Look up product's linked field schema to get allowed keys
|
|
285
289
|
const { data: pfs } = await sb.from("product_field_schemas").select("field_schema_id").eq("product_id", pid).limit(1);
|
|
286
290
|
if (pfs?.length) {
|
|
@@ -312,6 +316,8 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
312
316
|
updates.minimum_wholesale_quantity = args.minimum_wholesale_quantity;
|
|
313
317
|
if (args.featured_image !== undefined)
|
|
314
318
|
updates.featured_image = args.featured_image;
|
|
319
|
+
if (args.image_gallery !== undefined)
|
|
320
|
+
updates.image_gallery = args.image_gallery;
|
|
315
321
|
const updateCatArg = (args.category ?? args.primary_category_id ?? args.category_id);
|
|
316
322
|
if (updateCatArg !== undefined) {
|
|
317
323
|
if (!updateCatArg) {
|
|
@@ -494,6 +500,14 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
494
500
|
return { success: false, error: "name is required" };
|
|
495
501
|
if (!args.fields || !Array.isArray(args.fields))
|
|
496
502
|
return { success: false, error: "fields array is required" };
|
|
503
|
+
// Auto-resolve catalog_id: use provided value, or fall back to store's default catalog
|
|
504
|
+
let catalogId = args.catalog_id;
|
|
505
|
+
if (!catalogId) {
|
|
506
|
+
const { data: defaultCatalog } = await sb.from("catalogs")
|
|
507
|
+
.select("id").eq("store_id", sid).eq("is_default", true).single();
|
|
508
|
+
if (defaultCatalog)
|
|
509
|
+
catalogId = defaultCatalog.id;
|
|
510
|
+
}
|
|
497
511
|
const insert = {
|
|
498
512
|
store_id: sid,
|
|
499
513
|
name,
|
|
@@ -504,12 +518,12 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
504
518
|
insert.description = args.description;
|
|
505
519
|
if (args.icon)
|
|
506
520
|
insert.icon = args.icon;
|
|
507
|
-
if (
|
|
508
|
-
insert.catalog_id =
|
|
521
|
+
if (catalogId)
|
|
522
|
+
insert.catalog_id = catalogId;
|
|
509
523
|
if (args.is_public !== undefined)
|
|
510
524
|
insert.is_public = args.is_public;
|
|
511
525
|
const { data, error } = await sb.from("field_schemas").insert(insert)
|
|
512
|
-
.select("id, name, slug, fields, icon, is_active, created_at").single();
|
|
526
|
+
.select("id, name, slug, fields, icon, is_active, catalog_id, created_at").single();
|
|
513
527
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
514
528
|
}
|
|
515
529
|
case "update_field_schema": {
|
|
@@ -534,7 +548,7 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
534
548
|
// Only allow modification of own schemas (prevent IDOR)
|
|
535
549
|
const { data, error } = await sb.from("field_schemas")
|
|
536
550
|
.update(updates).eq("id", fsId).eq("store_id", sid)
|
|
537
|
-
.select("id, name, slug, fields, icon, is_active, updated_at").single();
|
|
551
|
+
.select("id, name, slug, fields, icon, is_active, catalog_id, updated_at").single();
|
|
538
552
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
539
553
|
}
|
|
540
554
|
case "delete_field_schema": {
|
|
@@ -585,6 +599,14 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
585
599
|
return { success: false, error: "name is required" };
|
|
586
600
|
if (!args.tiers || !Array.isArray(args.tiers))
|
|
587
601
|
return { success: false, error: "tiers array is required" };
|
|
602
|
+
// Auto-resolve catalog_id: use provided value, or fall back to store's default catalog
|
|
603
|
+
let catalogId = args.catalog_id;
|
|
604
|
+
if (!catalogId) {
|
|
605
|
+
const { data: defaultCatalog } = await sb.from("catalogs")
|
|
606
|
+
.select("id").eq("store_id", sid).eq("is_default", true).single();
|
|
607
|
+
if (defaultCatalog)
|
|
608
|
+
catalogId = defaultCatalog.id;
|
|
609
|
+
}
|
|
588
610
|
const insert = {
|
|
589
611
|
store_id: sid,
|
|
590
612
|
name,
|
|
@@ -595,12 +617,12 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
595
617
|
insert.description = args.description;
|
|
596
618
|
if (args.quality_tier)
|
|
597
619
|
insert.quality_tier = args.quality_tier;
|
|
598
|
-
if (
|
|
599
|
-
insert.catalog_id =
|
|
620
|
+
if (catalogId)
|
|
621
|
+
insert.catalog_id = catalogId;
|
|
600
622
|
if (args.is_public !== undefined)
|
|
601
623
|
insert.is_public = args.is_public;
|
|
602
624
|
const { data, error } = await sb.from("pricing_schemas").insert(insert)
|
|
603
|
-
.select("id, name, slug, tiers, quality_tier, is_active, created_at").single();
|
|
625
|
+
.select("id, name, slug, tiers, quality_tier, is_active, catalog_id, created_at").single();
|
|
604
626
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
605
627
|
}
|
|
606
628
|
case "update_pricing_schema": {
|
|
@@ -625,7 +647,7 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
625
647
|
// Only allow modification of own schemas (prevent IDOR)
|
|
626
648
|
const { data, error } = await sb.from("pricing_schemas")
|
|
627
649
|
.update(updates).eq("id", psId).eq("store_id", sid)
|
|
628
|
-
.select("id, name, slug, tiers, quality_tier, is_active, updated_at").single();
|
|
650
|
+
.select("id, name, slug, tiers, quality_tier, is_active, catalog_id, updated_at").single();
|
|
629
651
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
630
652
|
}
|
|
631
653
|
case "delete_pricing_schema": {
|
|
@@ -916,7 +938,7 @@ export async function handleProducts(sb, args, storeId) {
|
|
|
916
938
|
.update({ product_id: primaryId }).eq("product_id", secondaryId);
|
|
917
939
|
reassignResults.product_reviews = prErr ? `error: ${prErr.message}` : `moved ${prCount ?? 0} rows`;
|
|
918
940
|
// 6. Fill in missing fields on primary from secondary
|
|
919
|
-
const fillFields = ["description", "short_description", "cost_price", "wholesale_price", "weight", "featured_image"];
|
|
941
|
+
const fillFields = ["description", "short_description", "cost_price", "wholesale_price", "weight", "featured_image", "image_gallery"];
|
|
920
942
|
const fills = {};
|
|
921
943
|
for (const field of fillFields) {
|
|
922
944
|
if (!primary[field] && secondary[field])
|
|
@@ -966,8 +988,9 @@ export async function handleCollections(sb, args, storeId) {
|
|
|
966
988
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
967
989
|
}
|
|
968
990
|
case "create": {
|
|
991
|
+
const slug = args.slug || (args.name || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
969
992
|
const { data, error } = await sb.from("creation_collections")
|
|
970
|
-
.insert({ store_id: sid, name: args.name }).select().single();
|
|
993
|
+
.insert({ store_id: sid, name: args.name, slug }).select().single();
|
|
971
994
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
972
995
|
}
|
|
973
996
|
default:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
/**
|
|
3
|
+
* ClickHouse observability handler — platform-level metrics via direct HTTP queries.
|
|
4
|
+
* Replaces FDW RPCs with direct ClickHouse queries for lower latency.
|
|
5
|
+
*/
|
|
6
|
+
export declare function handleClickHouse(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
|
|
7
|
+
success: boolean;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
error?: string;
|
|
10
|
+
}>;
|