@lobu/core 3.0.13 → 3.0.19
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/dist/__tests__/fixtures/mock-redis.d.ts +3 -0
- package/dist/__tests__/fixtures/mock-redis.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mock-redis.js +12 -0
- package/dist/__tests__/fixtures/mock-redis.js.map +1 -1
- package/dist/__tests__/secret-refs.test.d.ts +2 -0
- package/dist/__tests__/secret-refs.test.d.ts.map +1 -0
- package/dist/__tests__/secret-refs.test.js +29 -0
- package/dist/__tests__/secret-refs.test.js.map +1 -0
- package/dist/agent-store.d.ts +33 -6
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js.map +1 -1
- package/dist/api-types.d.ts +1 -5
- package/dist/api-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/integration-types.d.ts +9 -1
- package/dist/integration-types.d.ts.map +1 -1
- package/dist/lobu-toml-schema.d.ts +168 -0
- package/dist/lobu-toml-schema.d.ts.map +1 -0
- package/dist/lobu-toml-schema.js +108 -0
- package/dist/lobu-toml-schema.js.map +1 -0
- package/dist/secret-refs.d.ts +11 -0
- package/dist/secret-refs.d.ts.map +1 -0
- package/dist/secret-refs.js +31 -0
- package/dist/secret-refs.js.map +1 -0
- package/dist/types.d.ts +29 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -3
- package/dist/types.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/encryption.test.ts +0 -103
- package/src/__tests__/fixtures/factories.ts +0 -76
- package/src/__tests__/fixtures/index.ts +0 -9
- package/src/__tests__/fixtures/mock-fetch.ts +0 -32
- package/src/__tests__/fixtures/mock-queue.ts +0 -50
- package/src/__tests__/fixtures/mock-redis.ts +0 -300
- package/src/__tests__/retry.test.ts +0 -134
- package/src/__tests__/sanitize.test.ts +0 -158
- package/src/agent-policy.ts +0 -207
- package/src/agent-store.ts +0 -220
- package/src/api-types.ts +0 -256
- package/src/command-registry.ts +0 -73
- package/src/constants.ts +0 -60
- package/src/errors.ts +0 -220
- package/src/index.ts +0 -131
- package/src/integration-types.ts +0 -26
- package/src/logger.ts +0 -248
- package/src/modules.ts +0 -184
- package/src/otel.ts +0 -307
- package/src/plugin-types.ts +0 -46
- package/src/provider-config-types.ts +0 -54
- package/src/redis/base-store.ts +0 -200
- package/src/sentry.ts +0 -56
- package/src/trace.ts +0 -32
- package/src/types.ts +0 -440
- package/src/utils/encryption.ts +0 -78
- package/src/utils/env.ts +0 -50
- package/src/utils/json.ts +0 -37
- package/src/utils/lock.ts +0 -75
- package/src/utils/mcp-tool-instructions.ts +0 -5
- package/src/utils/retry.ts +0 -91
- package/src/utils/sanitize.ts +0 -127
- package/src/worker/auth.ts +0 -100
- package/src/worker/transport.ts +0 -107
- package/tsconfig.json +0 -20
package/src/otel.ts
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenTelemetry tracing setup for distributed tracing.
|
|
3
|
-
* Ships traces via OTLP gRPC to any compatible collector (Tempo, Jaeger, Datadog, etc.).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Span, Tracer } from "@opentelemetry/api";
|
|
7
|
-
import { context, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
8
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
9
|
-
import { Resource } from "@opentelemetry/resources";
|
|
10
|
-
import {
|
|
11
|
-
NodeTracerProvider,
|
|
12
|
-
SimpleSpanProcessor,
|
|
13
|
-
} from "@opentelemetry/sdk-trace-node";
|
|
14
|
-
import {
|
|
15
|
-
ATTR_SERVICE_NAME,
|
|
16
|
-
ATTR_SERVICE_VERSION,
|
|
17
|
-
} from "@opentelemetry/semantic-conventions";
|
|
18
|
-
import { createLogger } from "./logger";
|
|
19
|
-
|
|
20
|
-
const logger = createLogger("otel");
|
|
21
|
-
|
|
22
|
-
let provider: NodeTracerProvider | null = null;
|
|
23
|
-
let tracer: Tracer | null = null;
|
|
24
|
-
|
|
25
|
-
export interface OtelConfig {
|
|
26
|
-
serviceName: string;
|
|
27
|
-
serviceVersion?: string;
|
|
28
|
-
otlpEndpoint?: string; // e.g., "http://collector:4317"
|
|
29
|
-
enabled?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Initialize OpenTelemetry tracing.
|
|
34
|
-
* Call this once at application startup.
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* initTracing({
|
|
38
|
-
* serviceName: "lobu-gateway",
|
|
39
|
-
* otlpEndpoint: "http://collector:4317",
|
|
40
|
-
* });
|
|
41
|
-
*/
|
|
42
|
-
export function initTracing(config: OtelConfig): void {
|
|
43
|
-
if (provider) {
|
|
44
|
-
return; // Already initialized
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const enabled = config.enabled ?? !!config.otlpEndpoint;
|
|
48
|
-
if (!enabled) {
|
|
49
|
-
logger.debug(
|
|
50
|
-
"Tracing disabled (no OTEL_EXPORTER_OTLP_ENDPOINT configured)"
|
|
51
|
-
);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const resource = new Resource({
|
|
56
|
-
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
57
|
-
[ATTR_SERVICE_VERSION]: config.serviceVersion || "1.0.0",
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
provider = new NodeTracerProvider({ resource });
|
|
61
|
-
|
|
62
|
-
const exporter = new OTLPTraceExporter({
|
|
63
|
-
url: config.otlpEndpoint,
|
|
64
|
-
timeoutMillis: 30000, // 30 second timeout for reliability
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Use SimpleSpanProcessor for immediate export (better for short-lived workers)
|
|
68
|
-
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
|
|
69
|
-
provider.register();
|
|
70
|
-
|
|
71
|
-
tracer = trace.getTracer(config.serviceName, config.serviceVersion);
|
|
72
|
-
|
|
73
|
-
logger.info(
|
|
74
|
-
`Tracing initialized: ${config.serviceName} -> ${config.otlpEndpoint}`
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get the configured tracer. Returns null if not initialized.
|
|
80
|
-
*/
|
|
81
|
-
export function getTracer(): Tracer | null {
|
|
82
|
-
return tracer;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Shutdown tracing gracefully.
|
|
87
|
-
*/
|
|
88
|
-
export async function shutdownTracing(): Promise<void> {
|
|
89
|
-
if (provider) {
|
|
90
|
-
await provider.shutdown();
|
|
91
|
-
provider = null;
|
|
92
|
-
tracer = null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Force flush all pending spans to the exporter.
|
|
98
|
-
* Call this after processing a message to ensure spans are exported promptly.
|
|
99
|
-
*/
|
|
100
|
-
export async function flushTracing(): Promise<void> {
|
|
101
|
-
if (provider) {
|
|
102
|
-
await provider.forceFlush();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Create a new span for tracing.
|
|
108
|
-
* If tracing is not initialized, returns a no-op span.
|
|
109
|
-
*
|
|
110
|
-
* @param name Span name (e.g., "queue_processing", "agent_execution")
|
|
111
|
-
* @param attributes Optional attributes to add to the span
|
|
112
|
-
* @param parentContext Optional parent context for trace correlation
|
|
113
|
-
*/
|
|
114
|
-
export function createSpan(
|
|
115
|
-
name: string,
|
|
116
|
-
attributes?: Record<string, string | number | boolean>,
|
|
117
|
-
kind: SpanKind = SpanKind.INTERNAL
|
|
118
|
-
): Span | null {
|
|
119
|
-
if (!tracer) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const span = tracer.startSpan(name, {
|
|
124
|
-
kind,
|
|
125
|
-
attributes,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return span;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Execute a function within a span context.
|
|
133
|
-
* Automatically handles span lifecycle (start, end, error recording).
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* const result = await withSpan("process_message", async (span) => {
|
|
137
|
-
* span?.setAttribute("messageId", messageId);
|
|
138
|
-
* return await processMessage();
|
|
139
|
-
* });
|
|
140
|
-
*/
|
|
141
|
-
export async function withSpan<T>(
|
|
142
|
-
name: string,
|
|
143
|
-
fn: (span: Span | null) => Promise<T>,
|
|
144
|
-
attributes?: Record<string, string | number | boolean>
|
|
145
|
-
): Promise<T> {
|
|
146
|
-
const span = createSpan(name, attributes);
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const result = await fn(span);
|
|
150
|
-
span?.setStatus({ code: SpanStatusCode.OK });
|
|
151
|
-
return result;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
if (span) {
|
|
154
|
-
span.setStatus({
|
|
155
|
-
code: SpanStatusCode.ERROR,
|
|
156
|
-
message: error instanceof Error ? error.message : String(error),
|
|
157
|
-
});
|
|
158
|
-
span.recordException(error as Error);
|
|
159
|
-
}
|
|
160
|
-
throw error;
|
|
161
|
-
} finally {
|
|
162
|
-
span?.end();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Get current active span from context.
|
|
168
|
-
*/
|
|
169
|
-
export function getCurrentSpan(): Span | undefined {
|
|
170
|
-
return trace.getActiveSpan();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Run a function within a span context, propagating the span.
|
|
175
|
-
*/
|
|
176
|
-
export function runInSpanContext<T>(span: Span, fn: () => T): T {
|
|
177
|
-
const ctx = trace.setSpan(context.active(), span);
|
|
178
|
-
return context.with(ctx, fn);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Create a root span and return traceparent header for propagation.
|
|
183
|
-
* Use this at the entry point (message ingestion) to start a trace.
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* const { span, traceparent } = createRootSpan("message_received", { messageId });
|
|
187
|
-
* // Store traceparent in message metadata for downstream propagation
|
|
188
|
-
* await queueProducer.enqueueMessage({ ...data, platformMetadata: { traceparent } });
|
|
189
|
-
* span.end();
|
|
190
|
-
*/
|
|
191
|
-
export function createRootSpan(
|
|
192
|
-
name: string,
|
|
193
|
-
attributes?: Record<string, string | number | boolean>
|
|
194
|
-
): { span: Span | null; traceparent: string | null } {
|
|
195
|
-
if (!tracer) {
|
|
196
|
-
return { span: null, traceparent: null };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const span = tracer.startSpan(name, {
|
|
200
|
-
kind: SpanKind.SERVER,
|
|
201
|
-
attributes,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Extract W3C traceparent header from span context
|
|
205
|
-
const spanContext = span.spanContext();
|
|
206
|
-
const traceparent = `00-${spanContext.traceId}-${spanContext.spanId}-01`;
|
|
207
|
-
|
|
208
|
-
return { span, traceparent };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Create a child span from a traceparent header.
|
|
213
|
-
* Use this to continue a trace in downstream services (queue consumer, worker).
|
|
214
|
-
*
|
|
215
|
-
* @example
|
|
216
|
-
* const traceparent = data.platformMetadata?.traceparent;
|
|
217
|
-
* const span = createChildSpan("queue_processing", traceparent, { jobId });
|
|
218
|
-
* // ... do work ...
|
|
219
|
-
* span?.end();
|
|
220
|
-
*/
|
|
221
|
-
export function createChildSpan(
|
|
222
|
-
name: string,
|
|
223
|
-
traceparent: string | null | undefined,
|
|
224
|
-
attributes?: Record<string, string | number | boolean>
|
|
225
|
-
): Span | null {
|
|
226
|
-
if (!tracer) {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!traceparent) {
|
|
231
|
-
// No parent context - create independent span
|
|
232
|
-
return createSpan(name, attributes);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Parse W3C traceparent: 00-traceId-parentSpanId-flags
|
|
236
|
-
const parts = traceparent.split("-");
|
|
237
|
-
if (parts.length !== 4) {
|
|
238
|
-
return createSpan(name, attributes);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const traceId = parts[1]!;
|
|
242
|
-
const parentSpanId = parts[2]!;
|
|
243
|
-
|
|
244
|
-
// Create span context from traceparent
|
|
245
|
-
const parentContext = trace.setSpanContext(context.active(), {
|
|
246
|
-
traceId,
|
|
247
|
-
spanId: parentSpanId,
|
|
248
|
-
traceFlags: 1, // sampled
|
|
249
|
-
isRemote: true,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// Start span as child of the propagated context
|
|
253
|
-
return tracer.startSpan(
|
|
254
|
-
name,
|
|
255
|
-
{ kind: SpanKind.INTERNAL, attributes },
|
|
256
|
-
parentContext
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Run a function within a child span context.
|
|
262
|
-
* Automatically handles span lifecycle and error recording.
|
|
263
|
-
*
|
|
264
|
-
* @example
|
|
265
|
-
* const result = await withChildSpan("process_job", traceparent, async (span) => {
|
|
266
|
-
* span?.setAttribute("jobId", jobId);
|
|
267
|
-
* return await processJob();
|
|
268
|
-
* });
|
|
269
|
-
*/
|
|
270
|
-
export async function withChildSpan<T>(
|
|
271
|
-
name: string,
|
|
272
|
-
traceparent: string | null | undefined,
|
|
273
|
-
fn: (span: Span | null) => Promise<T>,
|
|
274
|
-
attributes?: Record<string, string | number | boolean>
|
|
275
|
-
): Promise<T> {
|
|
276
|
-
const span = createChildSpan(name, traceparent, attributes);
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const result = await fn(span);
|
|
280
|
-
span?.setStatus({ code: SpanStatusCode.OK });
|
|
281
|
-
return result;
|
|
282
|
-
} catch (error) {
|
|
283
|
-
if (span) {
|
|
284
|
-
span.setStatus({
|
|
285
|
-
code: SpanStatusCode.ERROR,
|
|
286
|
-
message: error instanceof Error ? error.message : String(error),
|
|
287
|
-
});
|
|
288
|
-
span.recordException(error as Error);
|
|
289
|
-
}
|
|
290
|
-
throw error;
|
|
291
|
-
} finally {
|
|
292
|
-
span?.end();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Extract traceparent from span for propagation to downstream services.
|
|
298
|
-
*/
|
|
299
|
-
export function getTraceparent(span: Span | null): string | null {
|
|
300
|
-
if (!span) return null;
|
|
301
|
-
const ctx = span.spanContext();
|
|
302
|
-
return `00-${ctx.traceId}-${ctx.spanId}-01`;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export type { Span, Tracer };
|
|
306
|
-
// Re-export OpenTelemetry types for convenience
|
|
307
|
-
export { SpanKind, SpanStatusCode };
|
package/src/plugin-types.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw plugin types for Lobu.
|
|
3
|
-
*
|
|
4
|
-
* Supports loading existing OpenClaw community plugins.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/** Supported plugin slots */
|
|
8
|
-
export type PluginSlot = "tool" | "provider" | "memory";
|
|
9
|
-
|
|
10
|
-
/** Configuration for a single plugin */
|
|
11
|
-
export interface PluginConfig {
|
|
12
|
-
/** npm package name or local path (e.g., "@openclaw/voice-call", "./my-plugin") */
|
|
13
|
-
source: string;
|
|
14
|
-
/** Which slot this plugin fills */
|
|
15
|
-
slot: PluginSlot;
|
|
16
|
-
/** Whether this plugin is enabled (default: true) */
|
|
17
|
-
enabled?: boolean;
|
|
18
|
-
/** Plugin-specific configuration passed through to the plugin runtime */
|
|
19
|
-
config?: Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Top-level plugins configuration stored in agent settings */
|
|
23
|
-
export interface PluginsConfig {
|
|
24
|
-
plugins: PluginConfig[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Metadata about a loaded plugin */
|
|
28
|
-
export interface PluginManifest {
|
|
29
|
-
/** Source identifier (package name or path) */
|
|
30
|
-
source: string;
|
|
31
|
-
/** Plugin slot */
|
|
32
|
-
slot: PluginSlot;
|
|
33
|
-
/** Display name (from package or source) */
|
|
34
|
-
name: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A provider registration captured from pi.registerProvider().
|
|
39
|
-
* The config is opaque here — it's passed directly to ModelRegistry.registerProvider().
|
|
40
|
-
*/
|
|
41
|
-
export interface ProviderRegistration {
|
|
42
|
-
/** Provider name (e.g., "corporate-ai", "my-proxy") */
|
|
43
|
-
name: string;
|
|
44
|
-
/** Provider config (ProviderConfigInput from pi-coding-agent) */
|
|
45
|
-
config: Record<string, unknown>;
|
|
46
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for config-driven LLM providers.
|
|
3
|
-
* Loaded from the `providers` section of system skills config.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface ProviderConfigEntry {
|
|
7
|
-
/** Display name in settings page (e.g. "Groq") */
|
|
8
|
-
displayName: string;
|
|
9
|
-
/** Provider icon URL */
|
|
10
|
-
iconUrl: string;
|
|
11
|
-
/** Env var name for API key (e.g. "GROQ_API_KEY") */
|
|
12
|
-
envVarName: string;
|
|
13
|
-
/** Provider's API base URL (e.g. "https://api.groq.com/openai") */
|
|
14
|
-
upstreamBaseUrl: string;
|
|
15
|
-
/** HTML help text for the settings page API key input */
|
|
16
|
-
apiKeyInstructions: string;
|
|
17
|
-
/** Placeholder text for the API key input */
|
|
18
|
-
apiKeyPlaceholder: string;
|
|
19
|
-
/** SDK compatibility hint — "openai" means OpenAI-compatible API format */
|
|
20
|
-
sdkCompat?: "openai";
|
|
21
|
-
/** Default model ID when none is configured */
|
|
22
|
-
defaultModel?: string;
|
|
23
|
-
/** Relative path to fetch model list (e.g. "/v1/models") */
|
|
24
|
-
modelsEndpoint?: string;
|
|
25
|
-
/** Override provider name for model registry lookup */
|
|
26
|
-
registryAlias?: string;
|
|
27
|
-
/** Whether to show in "Add Provider" catalog (default: true) */
|
|
28
|
-
catalogVisible?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Optional speech-to-text configuration.
|
|
31
|
-
* If omitted and sdkCompat is "openai", STT is enabled with default endpoint/model.
|
|
32
|
-
* Use this block to override endpoint/model or disable STT for a provider.
|
|
33
|
-
*/
|
|
34
|
-
stt?: {
|
|
35
|
-
/** Set false to disable STT even when this block exists. */
|
|
36
|
-
enabled?: boolean;
|
|
37
|
-
/** STT protocol compatibility; currently only OpenAI-compatible is supported here. */
|
|
38
|
-
sdkCompat?: "openai";
|
|
39
|
-
/** Optional upstream base URL override for STT requests. */
|
|
40
|
-
baseUrl?: string;
|
|
41
|
-
/** Relative or absolute transcription endpoint path/URL. */
|
|
42
|
-
transcriptionPath?: string;
|
|
43
|
-
/** STT model ID (for OpenAI-compatible endpoints). */
|
|
44
|
-
model?: string;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Metadata passed from gateway to worker for config-driven providers. */
|
|
49
|
-
export interface ConfigProviderMeta {
|
|
50
|
-
sdkCompat?: "openai";
|
|
51
|
-
defaultModel?: string;
|
|
52
|
-
registryAlias?: string;
|
|
53
|
-
baseUrlEnvVar: string;
|
|
54
|
-
}
|
package/src/redis/base-store.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import type Redis from "ioredis";
|
|
2
|
-
import { createLogger, type Logger } from "../logger";
|
|
3
|
-
import { safeJsonParse, safeJsonStringify } from "../utils/json";
|
|
4
|
-
|
|
5
|
-
function errMsg(error: unknown): string {
|
|
6
|
-
return error instanceof Error ? error.message : String(error);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface RedisStoreConfig {
|
|
10
|
-
redis: Redis;
|
|
11
|
-
keyPrefix: string;
|
|
12
|
-
loggerName: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Base class for all Redis-backed stores
|
|
17
|
-
* Provides common CRUD operations with JSON serialization
|
|
18
|
-
*
|
|
19
|
-
* Consolidates:
|
|
20
|
-
* - packages/gateway/src/auth/credential-store.ts (BaseCredentialStore)
|
|
21
|
-
* - packages/gateway/src/infrastructure/redis/store.ts (BaseRedisStore)
|
|
22
|
-
*/
|
|
23
|
-
export abstract class BaseRedisStore<T> {
|
|
24
|
-
protected logger: Logger;
|
|
25
|
-
protected redis: Redis;
|
|
26
|
-
protected keyPrefix: string;
|
|
27
|
-
|
|
28
|
-
constructor(config: RedisStoreConfig) {
|
|
29
|
-
this.redis = config.redis;
|
|
30
|
-
this.keyPrefix = config.keyPrefix;
|
|
31
|
-
this.logger = createLogger(config.loggerName);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build Redis key from parts
|
|
36
|
-
*/
|
|
37
|
-
protected buildKey(...parts: string[]): string {
|
|
38
|
-
return [this.keyPrefix, ...parts].join(":");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get value from Redis
|
|
43
|
-
* Returns null if not found or validation fails
|
|
44
|
-
*/
|
|
45
|
-
protected async get(key: string): Promise<T | null> {
|
|
46
|
-
try {
|
|
47
|
-
const data = await this.redis.get(key);
|
|
48
|
-
if (!data) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const value = this.deserialize(data);
|
|
53
|
-
|
|
54
|
-
// Validate after deserialization
|
|
55
|
-
if (!this.validate(value)) {
|
|
56
|
-
this.logger.warn("Invalid data after deserialization", { key });
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return value;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
this.logger.error("Failed to get from Redis", {
|
|
63
|
-
error: errMsg(error),
|
|
64
|
-
key,
|
|
65
|
-
});
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Set value in Redis
|
|
72
|
-
*/
|
|
73
|
-
protected async set(
|
|
74
|
-
key: string,
|
|
75
|
-
value: T,
|
|
76
|
-
ttlSeconds?: number
|
|
77
|
-
): Promise<void> {
|
|
78
|
-
try {
|
|
79
|
-
const data = this.serialize(value);
|
|
80
|
-
if (ttlSeconds) {
|
|
81
|
-
await this.redis.setex(key, ttlSeconds, data);
|
|
82
|
-
} else {
|
|
83
|
-
await this.redis.set(key, data);
|
|
84
|
-
}
|
|
85
|
-
} catch (error) {
|
|
86
|
-
this.logger.error("Failed to set in Redis", {
|
|
87
|
-
error: errMsg(error),
|
|
88
|
-
key,
|
|
89
|
-
});
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Delete value from Redis
|
|
96
|
-
*/
|
|
97
|
-
protected async delete(key: string): Promise<void> {
|
|
98
|
-
try {
|
|
99
|
-
await this.redis.del(key);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
this.logger.error("Failed to delete from Redis", {
|
|
102
|
-
error: errMsg(error),
|
|
103
|
-
key,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Scan for all keys matching a prefix (cursor-based, production-safe).
|
|
110
|
-
* Returns full key strings matching `{prefix}*`.
|
|
111
|
-
*/
|
|
112
|
-
protected async scanByPrefix(prefix: string): Promise<string[]> {
|
|
113
|
-
const results: string[] = [];
|
|
114
|
-
let cursor = "0";
|
|
115
|
-
try {
|
|
116
|
-
do {
|
|
117
|
-
const [nextCursor, keys] = await this.redis.scan(
|
|
118
|
-
cursor,
|
|
119
|
-
"MATCH",
|
|
120
|
-
`${prefix}*`,
|
|
121
|
-
"COUNT",
|
|
122
|
-
100
|
|
123
|
-
);
|
|
124
|
-
cursor = nextCursor;
|
|
125
|
-
results.push(...keys);
|
|
126
|
-
} while (cursor !== "0");
|
|
127
|
-
} catch (error) {
|
|
128
|
-
this.logger.error("Failed to scan by prefix", {
|
|
129
|
-
error: errMsg(error),
|
|
130
|
-
prefix,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return results;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check if key exists in Redis
|
|
138
|
-
*/
|
|
139
|
-
protected async exists(key: string): Promise<boolean> {
|
|
140
|
-
try {
|
|
141
|
-
const result = await this.redis.exists(key);
|
|
142
|
-
return result === 1;
|
|
143
|
-
} catch (error) {
|
|
144
|
-
this.logger.error("Failed to check existence in Redis", {
|
|
145
|
-
error: errMsg(error),
|
|
146
|
-
key,
|
|
147
|
-
});
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Serialize value to string
|
|
154
|
-
* Override for custom serialization
|
|
155
|
-
*/
|
|
156
|
-
protected serialize(value: T): string {
|
|
157
|
-
const result = safeJsonStringify(value);
|
|
158
|
-
if (result === null) {
|
|
159
|
-
throw new Error("Failed to serialize value to JSON");
|
|
160
|
-
}
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Deserialize string to value
|
|
166
|
-
* Override for custom deserialization
|
|
167
|
-
*/
|
|
168
|
-
protected deserialize(data: string): T {
|
|
169
|
-
const result = safeJsonParse<T>(data);
|
|
170
|
-
if (result === null) {
|
|
171
|
-
throw new Error("Failed to deserialize JSON data");
|
|
172
|
-
}
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Validate value after deserialization
|
|
178
|
-
* Override to add custom validation logic
|
|
179
|
-
* Return false to reject invalid data
|
|
180
|
-
*/
|
|
181
|
-
protected validate(_value: T): boolean {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Specialized base for credential stores
|
|
188
|
-
* Validates that accessToken field exists
|
|
189
|
-
*/
|
|
190
|
-
export abstract class BaseCredentialStore<
|
|
191
|
-
T extends { accessToken: string },
|
|
192
|
-
> extends BaseRedisStore<T> {
|
|
193
|
-
protected override validate(value: T): boolean {
|
|
194
|
-
if (!value.accessToken) {
|
|
195
|
-
this.logger.warn("Invalid credentials: missing accessToken");
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
}
|
package/src/sentry.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { createLogger, type Logger } from "./logger";
|
|
2
|
-
|
|
3
|
-
// Lazy logger initialization to avoid circular dependency
|
|
4
|
-
let _logger: Logger | null = null;
|
|
5
|
-
function getLogger(): Logger {
|
|
6
|
-
if (!_logger) {
|
|
7
|
-
_logger = createLogger("sentry");
|
|
8
|
-
}
|
|
9
|
-
return _logger;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let sentryInstance: typeof import("@sentry/node") | null = null;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Initialize Sentry with configuration from environment variables.
|
|
16
|
-
* Only initializes if SENTRY_DSN is set — no implicit error reporting.
|
|
17
|
-
* Uses dynamic import to avoid module resolution issues in dev mode.
|
|
18
|
-
*/
|
|
19
|
-
export async function initSentry() {
|
|
20
|
-
const sentryDsn = process.env.SENTRY_DSN;
|
|
21
|
-
if (!sentryDsn) {
|
|
22
|
-
getLogger().debug("Sentry disabled (no SENTRY_DSN configured)");
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const Sentry = await import("@sentry/node");
|
|
28
|
-
sentryInstance = Sentry;
|
|
29
|
-
|
|
30
|
-
Sentry.init({
|
|
31
|
-
dsn: sentryDsn,
|
|
32
|
-
sendDefaultPii: true,
|
|
33
|
-
profileSessionSampleRate: 1.0,
|
|
34
|
-
tracesSampleRate: 1.0, // Capture 100% of traces for better visibility
|
|
35
|
-
integrations: [
|
|
36
|
-
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
|
|
37
|
-
Sentry.redisIntegration(),
|
|
38
|
-
],
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
getLogger().debug("Sentry monitoring initialized");
|
|
42
|
-
} catch (error) {
|
|
43
|
-
getLogger().warn(
|
|
44
|
-
"Sentry initialization failed (continuing without monitoring):",
|
|
45
|
-
error
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get the initialized Sentry instance
|
|
52
|
-
* @returns Sentry instance or null if not initialized
|
|
53
|
-
*/
|
|
54
|
-
export function getSentry(): typeof import("@sentry/node") | null {
|
|
55
|
-
return sentryInstance;
|
|
56
|
-
}
|
package/src/trace.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trace ID utilities for end-to-end message lifecycle observability.
|
|
3
|
-
* Trace IDs propagate through the entire pipeline:
|
|
4
|
-
* [WhatsApp Message] -> [Queue] -> [Worker Creation] -> [PVC Setup] -> [Agent Runtime] -> [Response]
|
|
5
|
-
*
|
|
6
|
-
* When OpenTelemetry is initialized, spans are sent to Tempo for waterfall visualization.
|
|
7
|
-
* Use createSpan/createChildSpan from ./otel.ts for actual span creation.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generate a trace ID from a message ID.
|
|
12
|
-
* Format: tr-{messageId prefix}-{timestamp base36}-{random}
|
|
13
|
-
* Example: tr-abc12345-lx4k-a3b2
|
|
14
|
-
*/
|
|
15
|
-
export function generateTraceId(messageId: string): string {
|
|
16
|
-
const timestamp = Date.now().toString(36);
|
|
17
|
-
const random = Math.random().toString(36).substring(2, 6);
|
|
18
|
-
// Take first 8 chars of messageId, sanitize for safe logging
|
|
19
|
-
const shortMessageId = messageId.replace(/[^a-zA-Z0-9]/g, "").substring(0, 8);
|
|
20
|
-
return `tr-${shortMessageId}-${timestamp}-${random}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Extract trace ID from various payload formats.
|
|
25
|
-
* Checks both top-level and nested platformMetadata.
|
|
26
|
-
*/
|
|
27
|
-
export function extractTraceId(payload: {
|
|
28
|
-
traceId?: string;
|
|
29
|
-
platformMetadata?: { traceId?: string };
|
|
30
|
-
}): string | undefined {
|
|
31
|
-
return payload?.traceId || payload?.platformMetadata?.traceId;
|
|
32
|
-
}
|