@lobu/core 3.0.12 → 3.0.13

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.
@@ -0,0 +1,91 @@
1
+ import { createLogger } from "../logger";
2
+
3
+ const logger = createLogger("retry");
4
+
5
+ export interface RetryOptions {
6
+ maxRetries?: number;
7
+ baseDelay?: number;
8
+ strategy?: "exponential" | "linear";
9
+ jitter?: boolean;
10
+ onRetry?: (attempt: number, error: Error) => void;
11
+ }
12
+
13
+ /**
14
+ * Retry a function with configurable backoff strategy
15
+ *
16
+ * @param fn - The async function to retry
17
+ * @param options - Retry configuration
18
+ * @returns The result of the function
19
+ * @throws The last error if all retries fail
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Exponential backoff (default)
24
+ * const result = await retryWithBackoff(
25
+ * () => fetch('https://api.example.com'),
26
+ * { maxRetries: 3, baseDelay: 1000 }
27
+ * );
28
+ *
29
+ * // Linear backoff with jitter
30
+ * const result = await retryWithBackoff(
31
+ * () => createDeployment(),
32
+ * {
33
+ * maxRetries: 3,
34
+ * strategy: 'linear',
35
+ * jitter: true,
36
+ * baseDelay: 2000,
37
+ * onRetry: (attempt, error) => {
38
+ * logger.warn(`Attempt ${attempt} failed: ${error.message}`);
39
+ * }
40
+ * }
41
+ * );
42
+ * ```
43
+ */
44
+ export async function retryWithBackoff<T>(
45
+ fn: () => Promise<T>,
46
+ options: RetryOptions = {}
47
+ ): Promise<T> {
48
+ const {
49
+ maxRetries = 3,
50
+ baseDelay = 1000,
51
+ strategy = "exponential",
52
+ jitter = false,
53
+ onRetry,
54
+ } = options;
55
+
56
+ let lastError: Error | undefined;
57
+
58
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
59
+ try {
60
+ return await fn();
61
+ } catch (error) {
62
+ lastError = error as Error;
63
+
64
+ if (attempt < maxRetries) {
65
+ // Calculate delay based on strategy
66
+ const delay =
67
+ strategy === "exponential"
68
+ ? baseDelay * 2 ** attempt
69
+ : baseDelay * (attempt + 1);
70
+
71
+ // Add jitter if requested (0-1000ms random)
72
+ const jitterMs = jitter ? Math.random() * 1000 : 0;
73
+ const finalDelay = delay + jitterMs;
74
+
75
+ // Notify caller of retry
76
+ if (onRetry) {
77
+ onRetry(attempt + 1, lastError);
78
+ } else {
79
+ logger.warn(
80
+ `Retry attempt ${attempt + 1}/${maxRetries} after ${Math.round(finalDelay)}ms`,
81
+ { error: lastError.message }
82
+ );
83
+ }
84
+
85
+ await new Promise((resolve) => setTimeout(resolve, finalDelay));
86
+ }
87
+ }
88
+ }
89
+
90
+ throw lastError;
91
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sanitize filename to prevent path traversal attacks
3
+ * Removes directory separators and dangerous characters
4
+ *
5
+ * @param filename - The filename to sanitize
6
+ * @param maxLength - Maximum filename length (default: 255)
7
+ * @returns Safe filename
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * sanitizeFilename("../../etc/passwd") // "etc_passwd"
12
+ * sanitizeFilename("file<>|name.txt") // "file___name.txt"
13
+ * ```
14
+ */
15
+ export function sanitizeFilename(
16
+ filename: string,
17
+ maxLength: number = 255
18
+ ): string {
19
+ // Remove any directory path components
20
+ const basename = filename.replace(/^.*[\\/]/, "");
21
+
22
+ // Remove null bytes and other dangerous characters
23
+ const sanitized = basename.replace(/[^\w\s.-]/g, "_");
24
+
25
+ // Prevent hidden files and parent directory references
26
+ const safe = sanitized.replace(/^\.+/, "").replace(/\.{2,}/g, ".");
27
+
28
+ // Ensure filename is not empty after sanitization
29
+ if (!safe || safe.length === 0) {
30
+ return "unnamed_file";
31
+ }
32
+
33
+ // Limit filename length
34
+ return safe.length > maxLength ? safe.substring(0, maxLength) : safe;
35
+ }
36
+
37
+ /**
38
+ * Sanitize conversation ID for filesystem usage
39
+ * Removes any characters that aren't safe for directory names
40
+ *
41
+ * @param conversationId - The conversation ID to sanitize
42
+ * @returns Safe conversation ID
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * sanitizeConversationId("1756766056.836119") // "1756766056.836119"
47
+ * sanitizeConversationId("thread/123/../456") // "thread_123___456"
48
+ * ```
49
+ */
50
+ export function sanitizeConversationId(conversationId: string): string {
51
+ return conversationId.replace(/[^a-zA-Z0-9.-]/g, "_");
52
+ }
53
+
54
+ /**
55
+ * Sanitize sensitive data from objects before logging
56
+ * Redacts API keys, tokens, and other credentials
57
+ *
58
+ * @param obj - Object to sanitize
59
+ * @param sensitiveKeys - Additional sensitive key names to redact
60
+ * @returns Sanitized object safe for logging
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const config = {
65
+ * apiKey: "secret-key-123",
66
+ * timeout: 5000,
67
+ * env: { TOKEN: "bearer-xyz" }
68
+ * };
69
+ *
70
+ * sanitizeForLogging(config)
71
+ * // {
72
+ * // apiKey: "[REDACTED:14]",
73
+ * // timeout: 5000,
74
+ * // env: { TOKEN: "[REDACTED:10]" }
75
+ * // }
76
+ * ```
77
+ */
78
+ export function sanitizeForLogging(
79
+ obj: any,
80
+ additionalSensitiveKeys: string[] = []
81
+ ): any {
82
+ if (!obj || typeof obj !== "object") {
83
+ return obj;
84
+ }
85
+
86
+ const defaultSensitiveKeys = [
87
+ "anthropic_api_key",
88
+ "api_key",
89
+ "apiKey",
90
+ "token",
91
+ "password",
92
+ "secret",
93
+ "authorization",
94
+ "bearer",
95
+ "credentials",
96
+ "privateKey",
97
+ "private_key",
98
+ ];
99
+
100
+ const sensitiveKeys = [...defaultSensitiveKeys, ...additionalSensitiveKeys];
101
+
102
+ const sanitized = Array.isArray(obj) ? [...obj] : { ...obj };
103
+
104
+ for (const key in sanitized) {
105
+ const lowerKey = key.toLowerCase();
106
+ const isSensitive = sensitiveKeys.some((k) => lowerKey.includes(k));
107
+
108
+ if (isSensitive && typeof sanitized[key] === "string") {
109
+ // Redact but show length for debugging
110
+ sanitized[key] = `[REDACTED:${sanitized[key].length}]`;
111
+ } else if (key === "env" && typeof sanitized[key] === "object") {
112
+ // Recursively sanitize env object
113
+ sanitized[key] = sanitizeForLogging(
114
+ sanitized[key],
115
+ additionalSensitiveKeys
116
+ );
117
+ } else if (typeof sanitized[key] === "object") {
118
+ // Recursively sanitize nested objects
119
+ sanitized[key] = sanitizeForLogging(
120
+ sanitized[key],
121
+ additionalSensitiveKeys
122
+ );
123
+ }
124
+ }
125
+
126
+ return sanitized;
127
+ }
@@ -0,0 +1,100 @@
1
+ import { createLogger } from "../logger";
2
+ import { decrypt, encrypt } from "../utils/encryption";
3
+
4
+ const logger = createLogger("worker-auth");
5
+
6
+ /**
7
+ * Worker authentication using encrypted conversation ID
8
+ * Token format: encrypted(userId:conversationId:deploymentName:timestamp)
9
+ */
10
+
11
+ export interface WorkerTokenData {
12
+ userId: string;
13
+ conversationId: string;
14
+ channelId: string;
15
+ teamId?: string; // Optional - not all platforms have teams
16
+ agentId?: string; // Space ID for multi-tenant isolation
17
+ connectionId?: string;
18
+ deploymentName: string;
19
+ timestamp: number;
20
+ platform?: string;
21
+ sessionKey?: string;
22
+ traceId?: string; // Trace ID for end-to-end observability
23
+ }
24
+
25
+ /**
26
+ * Generate a worker authentication token by encrypting thread metadata
27
+ */
28
+ export function generateWorkerToken(
29
+ userId: string,
30
+ conversationId: string,
31
+ deploymentName: string,
32
+ options: {
33
+ channelId: string;
34
+ teamId?: string;
35
+ agentId?: string;
36
+ connectionId?: string;
37
+ platform?: string;
38
+ sessionKey?: string;
39
+ traceId?: string; // Trace ID for end-to-end observability
40
+ }
41
+ ): string {
42
+ // Validate required fields
43
+ if (!options.channelId) {
44
+ throw new Error("channelId is required for worker token generation");
45
+ }
46
+
47
+ const timestamp = Date.now();
48
+ const payload: WorkerTokenData = {
49
+ userId,
50
+ conversationId,
51
+ channelId: options.channelId,
52
+ teamId: options.teamId, // Can be undefined - that's ok
53
+ agentId: options.agentId, // Space ID for multi-tenant credential lookup
54
+ connectionId: options.connectionId,
55
+ deploymentName,
56
+ timestamp,
57
+ platform: options.platform,
58
+ sessionKey: options.sessionKey,
59
+ traceId: options.traceId, // Trace ID for observability
60
+ };
61
+
62
+ // Encrypt the payload
63
+ const encrypted = encrypt(JSON.stringify(payload));
64
+ return encrypted;
65
+ }
66
+
67
+ /**
68
+ * Verify and decrypt a worker authentication token
69
+ */
70
+ export function verifyWorkerToken(token: string): WorkerTokenData | null {
71
+ try {
72
+ // Decrypt the token
73
+ const decrypted = decrypt(token);
74
+ const data = JSON.parse(decrypted) as WorkerTokenData;
75
+
76
+ if (
77
+ !data.conversationId ||
78
+ !data.userId ||
79
+ !data.deploymentName ||
80
+ !data.timestamp
81
+ ) {
82
+ logger.error("Worker token rejected: missing required fields");
83
+ return null;
84
+ }
85
+
86
+ // Check token expiration (default 24h)
87
+ const parsedTtl = parseInt(process.env.WORKER_TOKEN_TTL_MS ?? "", 10);
88
+ const ttl =
89
+ !Number.isNaN(parsedTtl) && parsedTtl > 0 ? parsedTtl : 86400000;
90
+ if (Date.now() - data.timestamp > ttl) {
91
+ logger.error("Worker token rejected: expired");
92
+ return null;
93
+ }
94
+
95
+ return data;
96
+ } catch (error) {
97
+ logger.error("Error verifying token:", error);
98
+ return null;
99
+ }
100
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Worker Transport Interface
3
+ * Defines how workers communicate with the gateway (platform-agnostic)
4
+ *
5
+ * This abstraction allows different transport implementations:
6
+ * - HTTP (current implementation)
7
+ * - WebSocket (for real-time bidirectional communication)
8
+ * - gRPC (for high-performance scenarios)
9
+ * - Message Queue (for asynchronous processing)
10
+ */
11
+
12
+ /**
13
+ * Transport interface for worker-to-gateway communication
14
+ */
15
+ export interface WorkerTransport {
16
+ /**
17
+ * Set the job ID for this worker session
18
+ * Used to correlate responses with the originating request
19
+ */
20
+ setJobId(jobId: string): void;
21
+
22
+ /**
23
+ * Set module-specific data to be included in responses
24
+ * Allows modules to attach metadata to worker responses
25
+ */
26
+ setModuleData(moduleData: Record<string, unknown>): void;
27
+
28
+ /**
29
+ * Send a streaming delta to the gateway
30
+ *
31
+ * @param delta - The content delta to send
32
+ * @param isFullReplacement - If true, replaces entire content; if false, appends
33
+ * @param isFinal - If true, indicates this is the final delta
34
+ */
35
+ sendStreamDelta(
36
+ delta: string,
37
+ isFullReplacement?: boolean,
38
+ isFinal?: boolean
39
+ ): Promise<void>;
40
+
41
+ /**
42
+ * Signal that the worker has completed processing
43
+ * Optionally includes a final delta
44
+ *
45
+ * @param finalDelta - Optional final content delta
46
+ */
47
+ signalDone(finalDelta?: string): Promise<void>;
48
+
49
+ /**
50
+ * Signal successful completion without additional content
51
+ */
52
+ signalCompletion(): Promise<void>;
53
+
54
+ /**
55
+ * Signal that an error occurred during processing
56
+ *
57
+ * @param error - The error that occurred
58
+ */
59
+ signalError(error: Error, errorCode?: string): Promise<void>;
60
+
61
+ /**
62
+ * Send a status update to the gateway
63
+ * Used for long-running operations to show progress
64
+ *
65
+ * @param elapsedSeconds - Time elapsed since operation started
66
+ * @param state - Current state description (e.g., "processing", "waiting for API")
67
+ */
68
+ sendStatusUpdate(elapsedSeconds: number, state: string): Promise<void>;
69
+ }
70
+
71
+ /**
72
+ * Configuration for creating a worker transport
73
+ */
74
+ export interface WorkerTransportConfig {
75
+ /** Gateway URL for sending responses */
76
+ gatewayUrl: string;
77
+
78
+ /** Authentication token for worker */
79
+ workerToken: string;
80
+
81
+ /** User ID who initiated the request */
82
+ userId: string;
83
+
84
+ /** Channel/conversation ID */
85
+ channelId: string;
86
+
87
+ /** Conversation ID for organizing messages */
88
+ conversationId: string;
89
+
90
+ /** Original message timestamp/ID */
91
+ originalMessageTs: string;
92
+
93
+ /** Bot's response message timestamp/ID (if exists) */
94
+ botResponseTs?: string;
95
+
96
+ /** Team/workspace ID (required for all platforms) */
97
+ teamId: string;
98
+
99
+ /** Platform identifier (slack, whatsapp, api, etc.) */
100
+ platform?: string;
101
+
102
+ /** Platform-specific metadata needed for response routing */
103
+ platformMetadata?: Record<string, unknown>;
104
+
105
+ /** IDs of messages already processed (for deduplication) */
106
+ processedMessageIds?: string[];
107
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "dist",
4
+ "rootDir": "src",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "sourceMap": true,
8
+ "noEmit": false,
9
+ "moduleResolution": "node",
10
+ "module": "CommonJS",
11
+ "target": "ES2020",
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "esModuleInterop": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "composite": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }