@lobu/core 3.0.10 → 3.0.12
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/otel.d.ts +2 -2
- package/dist/otel.js +4 -4
- package/package.json +11 -11
- 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/tsconfig.tsbuildinfo +0 -1
package/src/types.ts
DELETED
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Provider Catalog Types
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Represents a provider installed for a specific agent.
|
|
7
|
-
* Stored in AgentSettings.installedProviders as an ordered array (index 0 = primary).
|
|
8
|
-
*/
|
|
9
|
-
export interface InstalledProvider {
|
|
10
|
-
providerId: string; // "claude", "chatgpt", "gemini", "z-ai"
|
|
11
|
-
installedAt: number;
|
|
12
|
-
config?: {
|
|
13
|
-
baseUrl?: string; // override upstream (e.g. z.ai proxy)
|
|
14
|
-
[key: string]: unknown;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* CLI backend configuration for pi-agent integration.
|
|
20
|
-
* Providers can ship CLI tools that pi-agent invokes as backends.
|
|
21
|
-
*/
|
|
22
|
-
export interface CliBackendConfig {
|
|
23
|
-
name: string; // "claude-code", "codex"
|
|
24
|
-
command: string; // "/usr/local/bin/claude"
|
|
25
|
-
args?: string[];
|
|
26
|
-
env?: Record<string, string>;
|
|
27
|
-
modelArg?: string; // "--model"
|
|
28
|
-
sessionArg?: string; // "--session"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// Auth Profile Types
|
|
33
|
-
// ============================================================================
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Unified authentication profile for any model provider.
|
|
37
|
-
* Stored in AgentSettings.authProfiles as an ordered array (index 0 = primary).
|
|
38
|
-
*/
|
|
39
|
-
export interface AuthProfile {
|
|
40
|
-
id: string; // UUID
|
|
41
|
-
provider: string; // "anthropic", "openai-codex", "gemini", "nvidia"
|
|
42
|
-
model: string; // Full model ref: "openai-codex/gpt-5.2-codex"
|
|
43
|
-
credential: string; // API key or OAuth access token
|
|
44
|
-
label: string; // "user@gmail.com", "sk-ant-...1234"
|
|
45
|
-
authType: "oauth" | "device-code" | "api-key";
|
|
46
|
-
metadata?: {
|
|
47
|
-
email?: string;
|
|
48
|
-
expiresAt?: number;
|
|
49
|
-
refreshToken?: string;
|
|
50
|
-
accountId?: string;
|
|
51
|
-
};
|
|
52
|
-
createdAt: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface SessionContext {
|
|
56
|
-
// Core identifiers
|
|
57
|
-
platform: string; // Platform identifier (e.g., "slack", "discord", "teams")
|
|
58
|
-
channelId: string;
|
|
59
|
-
userId: string;
|
|
60
|
-
messageId: string; // Required - always needed for tracking
|
|
61
|
-
|
|
62
|
-
// Optional context
|
|
63
|
-
conversationId?: string;
|
|
64
|
-
teamId?: string; // Platform workspace/team identifier
|
|
65
|
-
userDisplayName?: string; // For logging/display purposes
|
|
66
|
-
workingDirectory?: string;
|
|
67
|
-
customInstructions?: string;
|
|
68
|
-
conversationHistory?: ConversationMessage[];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ConversationMessage {
|
|
72
|
-
role: "system" | "user" | "assistant";
|
|
73
|
-
content: string;
|
|
74
|
-
timestamp: number;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ============================================================================
|
|
78
|
-
// Conversation History Types
|
|
79
|
-
// ============================================================================
|
|
80
|
-
|
|
81
|
-
// ============================================================================
|
|
82
|
-
// Skills Configuration Types
|
|
83
|
-
// ============================================================================
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Per-skill thinking budget level.
|
|
87
|
-
* Controls how much reasoning the model applies when executing a skill.
|
|
88
|
-
*/
|
|
89
|
-
export type ThinkingLevel = "off" | "low" | "medium" | "high";
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* MCP server declared by a skill manifest.
|
|
93
|
-
*/
|
|
94
|
-
export interface McpOAuthConfig {
|
|
95
|
-
/** Authorization endpoint (user verification page for device-code flow). */
|
|
96
|
-
authUrl?: string;
|
|
97
|
-
/** Token endpoint. Falls back to `{origin}/oauth/token`. */
|
|
98
|
-
tokenUrl?: string;
|
|
99
|
-
/** Pre-registered client ID. When provided, dynamic client registration is skipped. */
|
|
100
|
-
clientId?: string;
|
|
101
|
-
/** Client secret for confidential clients. */
|
|
102
|
-
clientSecret?: string;
|
|
103
|
-
/** OAuth scopes. Falls back to `mcp:read mcp:write profile:read`. */
|
|
104
|
-
scopes?: string[];
|
|
105
|
-
/** Device authorization endpoint. Falls back to `{origin}/oauth/device_authorization`. */
|
|
106
|
-
deviceAuthorizationUrl?: string;
|
|
107
|
-
/** Dynamic client registration endpoint. Falls back to `{origin}/oauth/register`. */
|
|
108
|
-
registrationUrl?: string;
|
|
109
|
-
/** RFC 8707 resource indicator included in token requests. */
|
|
110
|
-
resource?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface SkillMcpServer {
|
|
114
|
-
id: string;
|
|
115
|
-
name?: string;
|
|
116
|
-
url?: string;
|
|
117
|
-
type?: "sse" | "stdio";
|
|
118
|
-
command?: string;
|
|
119
|
-
args?: string[];
|
|
120
|
-
oauth?: McpOAuthConfig;
|
|
121
|
-
inputs?: Array<{ id: string; label?: string; type?: string }>;
|
|
122
|
-
headers?: Record<string, string>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Individual skill configuration.
|
|
127
|
-
* Skills are SKILL.md files from GitHub repos that provide instructions to Claude.
|
|
128
|
-
*/
|
|
129
|
-
export interface SkillConfig {
|
|
130
|
-
/** Skill repository in owner/repo format (e.g., "anthropics/skills/pdf") */
|
|
131
|
-
repo: string;
|
|
132
|
-
/** Skill name derived from SKILL.md frontmatter or folder name */
|
|
133
|
-
name: string;
|
|
134
|
-
/** Optional description from SKILL.md frontmatter */
|
|
135
|
-
description?: string;
|
|
136
|
-
/** Short always-inlined instruction block for critical rules */
|
|
137
|
-
instructions?: string;
|
|
138
|
-
/** Whether this skill is currently enabled */
|
|
139
|
-
enabled: boolean;
|
|
140
|
-
/** True for system-defined skills (from system-skills.json). Cannot be removed by users. */
|
|
141
|
-
system?: boolean;
|
|
142
|
-
/** Cached SKILL.md content (fetched from GitHub) */
|
|
143
|
-
content?: string;
|
|
144
|
-
/** When the content was last fetched (timestamp ms) */
|
|
145
|
-
contentFetchedAt?: number;
|
|
146
|
-
/** MCP servers declared by the skill */
|
|
147
|
-
mcpServers?: SkillMcpServer[];
|
|
148
|
-
/** System packages declared by the skill (nix) */
|
|
149
|
-
nixPackages?: string[];
|
|
150
|
-
/** Network domains the skill needs access to (legacy flat list) */
|
|
151
|
-
permissions?: string[];
|
|
152
|
-
/** Network access policy declared by the skill */
|
|
153
|
-
networkConfig?: { allowedDomains?: string[]; deniedDomains?: string[] };
|
|
154
|
-
/** Tool permission policy declared by the skill */
|
|
155
|
-
toolPermissions?: { allow?: string[]; deny?: string[] };
|
|
156
|
-
/** AI providers the skill requires */
|
|
157
|
-
providers?: string[];
|
|
158
|
-
/** Preferred model for this skill (e.g., "anthropic/claude-opus-4") */
|
|
159
|
-
modelPreference?: string;
|
|
160
|
-
/** Thinking level budget for this skill */
|
|
161
|
-
thinkingLevel?: ThinkingLevel;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Skills configuration for agent settings.
|
|
166
|
-
* Contains list of configured skills that can be enabled/disabled.
|
|
167
|
-
*/
|
|
168
|
-
export interface SkillsConfig {
|
|
169
|
-
/** List of configured skills */
|
|
170
|
-
skills: SkillConfig[];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Platform-agnostic history message format.
|
|
175
|
-
* Used to pass conversation history to workers.
|
|
176
|
-
*/
|
|
177
|
-
export interface HistoryMessage {
|
|
178
|
-
role: "user" | "assistant";
|
|
179
|
-
content: string;
|
|
180
|
-
timestamp: number;
|
|
181
|
-
/** Display name of the message sender */
|
|
182
|
-
userName?: string;
|
|
183
|
-
/** Platform-specific message ID for deduplication */
|
|
184
|
-
messageId?: string;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Network configuration for worker sandbox isolation.
|
|
189
|
-
* Controls which domains the worker can access via HTTP proxy.
|
|
190
|
-
*
|
|
191
|
-
* Filtering rules:
|
|
192
|
-
* - deniedDomains are checked first (take precedence)
|
|
193
|
-
* - allowedDomains are checked second
|
|
194
|
-
* - If neither matches, request is denied
|
|
195
|
-
*
|
|
196
|
-
* Domain pattern format:
|
|
197
|
-
* - "example.com" - exact match
|
|
198
|
-
* - ".example.com" or "*.example.com" - matches subdomains
|
|
199
|
-
*/
|
|
200
|
-
export interface NetworkConfig {
|
|
201
|
-
/** Domains the worker is allowed to access. Empty array = no network access. */
|
|
202
|
-
allowedDomains?: string[];
|
|
203
|
-
/** Domains explicitly blocked (takes precedence over allowedDomains). */
|
|
204
|
-
deniedDomains?: string[];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Nix environment configuration for agent workspace.
|
|
209
|
-
* Allows agents to run with specific Nix packages or flakes.
|
|
210
|
-
*
|
|
211
|
-
* Resolution priority:
|
|
212
|
-
* 1. API-provided flakeUrl (highest)
|
|
213
|
-
* 2. API-provided packages
|
|
214
|
-
* 3. flake.nix in git repo
|
|
215
|
-
* 4. shell.nix in git repo
|
|
216
|
-
* 5. .nix-packages file in git repo
|
|
217
|
-
*/
|
|
218
|
-
export interface NixConfig {
|
|
219
|
-
/** Nix flake URL (e.g., "github:user/repo#devShell") */
|
|
220
|
-
flakeUrl?: string;
|
|
221
|
-
/** Nixpkgs packages to install (e.g., ["python311", "ffmpeg"]) */
|
|
222
|
-
packages?: string[];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ============================================================================
|
|
226
|
-
// Tools Configuration Types
|
|
227
|
-
// ============================================================================
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Tool permission configuration for agent settings.
|
|
231
|
-
* Follows Claude Code's permission patterns for consistency.
|
|
232
|
-
*
|
|
233
|
-
* Pattern formats (Claude Code compatible):
|
|
234
|
-
* - "Read" - exact tool match
|
|
235
|
-
* - "Bash(git:*)" - Bash with command filter (only git commands)
|
|
236
|
-
* - "Bash(npm:*)" - Bash with npm commands only
|
|
237
|
-
* - "mcp__servername__*" - all tools from an MCP server
|
|
238
|
-
* - "*" - wildcard (all tools)
|
|
239
|
-
*
|
|
240
|
-
* Filtering rules:
|
|
241
|
-
* - deniedTools are checked first (take precedence)
|
|
242
|
-
* - allowedTools are checked second
|
|
243
|
-
* - If strictMode=true, only allowedTools are permitted
|
|
244
|
-
* - If strictMode=false, defaults + allowedTools are permitted
|
|
245
|
-
*/
|
|
246
|
-
export interface ToolsConfig {
|
|
247
|
-
/**
|
|
248
|
-
* Tools to auto-allow (in addition to defaults unless strictMode=true).
|
|
249
|
-
* Supports patterns like "Bash(git:*)" or "mcp__github__*".
|
|
250
|
-
*/
|
|
251
|
-
allowedTools?: string[];
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Tools to always deny (takes precedence over allowedTools).
|
|
255
|
-
* Use to block specific tools even if they're in defaults.
|
|
256
|
-
*/
|
|
257
|
-
deniedTools?: string[];
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* If true, ONLY allowedTools are permitted (ignores defaults).
|
|
261
|
-
* If false (default), allowedTools are ADDED to default permissions.
|
|
262
|
-
*/
|
|
263
|
-
strictMode?: boolean;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* MCP server configuration for per-agent MCP servers.
|
|
268
|
-
* Supports both HTTP/SSE and stdio MCP servers.
|
|
269
|
-
*/
|
|
270
|
-
export interface McpServerConfig {
|
|
271
|
-
/** For HTTP/SSE MCPs: upstream URL */
|
|
272
|
-
url?: string;
|
|
273
|
-
/** Server type: "sse" for HTTP MCPs, "stdio" for command-based */
|
|
274
|
-
type?: "sse" | "stdio";
|
|
275
|
-
/** For stdio MCPs: command to execute */
|
|
276
|
-
command?: string;
|
|
277
|
-
/** For stdio MCPs: command arguments */
|
|
278
|
-
args?: string[];
|
|
279
|
-
/** For stdio MCPs: environment variables */
|
|
280
|
-
env?: Record<string, string>;
|
|
281
|
-
/** Additional headers for HTTP MCPs */
|
|
282
|
-
headers?: Record<string, string>;
|
|
283
|
-
/** Optional description for the MCP */
|
|
284
|
-
description?: string;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Per-agent MCP configuration.
|
|
289
|
-
* These MCPs are ADDED to global MCPs (not replacing).
|
|
290
|
-
*/
|
|
291
|
-
export interface AgentMcpConfig {
|
|
292
|
-
/** Additional MCP servers for this agent */
|
|
293
|
-
mcpServers: Record<string, McpServerConfig>;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export interface MemoryFlushOptions {
|
|
297
|
-
enabled?: boolean;
|
|
298
|
-
softThresholdTokens?: number;
|
|
299
|
-
systemPrompt?: string;
|
|
300
|
-
prompt?: string;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export interface AgentCompactionOptions {
|
|
304
|
-
memoryFlush?: MemoryFlushOptions;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Platform-agnostic execution hints passed through gateway → worker.
|
|
309
|
-
* Flexible types (string | string[]) and index signature allow forward
|
|
310
|
-
* compatibility for different agent implementations.
|
|
311
|
-
*/
|
|
312
|
-
export interface AgentOptions {
|
|
313
|
-
runtime?: string;
|
|
314
|
-
model?: string;
|
|
315
|
-
maxTokens?: number;
|
|
316
|
-
temperature?: number;
|
|
317
|
-
allowedTools?: string | string[];
|
|
318
|
-
disallowedTools?: string | string[];
|
|
319
|
-
timeoutMinutes?: number | string;
|
|
320
|
-
compaction?: AgentCompactionOptions;
|
|
321
|
-
// Additional settings passed through from gateway (can be nested objects)
|
|
322
|
-
networkConfig?: Record<string, unknown>;
|
|
323
|
-
envVars?: Record<string, string>;
|
|
324
|
-
[key: string]: unknown;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Platform-agnostic log level type
|
|
329
|
-
* Maps to common logging levels used across different platforms
|
|
330
|
-
*/
|
|
331
|
-
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
332
|
-
|
|
333
|
-
// ============================================================================
|
|
334
|
-
// Instruction Provider Types
|
|
335
|
-
// ============================================================================
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Context information passed to instruction providers
|
|
339
|
-
*/
|
|
340
|
-
export interface InstructionContext {
|
|
341
|
-
userId: string;
|
|
342
|
-
agentId: string;
|
|
343
|
-
sessionKey: string;
|
|
344
|
-
workingDirectory: string;
|
|
345
|
-
availableProjects?: string[];
|
|
346
|
-
userPrompt?: string;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Interface for components that contribute custom instructions
|
|
351
|
-
*/
|
|
352
|
-
export interface InstructionProvider {
|
|
353
|
-
/** Unique identifier for this provider */
|
|
354
|
-
name: string;
|
|
355
|
-
|
|
356
|
-
/** Priority for ordering (lower = earlier in output) */
|
|
357
|
-
priority: number;
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Generate instruction text for this provider
|
|
361
|
-
* @param context - Context information for instruction generation
|
|
362
|
-
* @returns Instruction text or empty string if none
|
|
363
|
-
*/
|
|
364
|
-
getInstructions(context: InstructionContext): Promise<string> | string;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ============================================================================
|
|
368
|
-
// Thread Response Types
|
|
369
|
-
// ============================================================================
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Shared payload contract for worker → platform thread responses.
|
|
373
|
-
* Ensures gateway consumers and workers stay type-aligned.
|
|
374
|
-
*/
|
|
375
|
-
export interface ThreadResponsePayload {
|
|
376
|
-
messageId: string;
|
|
377
|
-
channelId: string;
|
|
378
|
-
conversationId: string;
|
|
379
|
-
userId: string;
|
|
380
|
-
teamId: string;
|
|
381
|
-
platform?: string; // Platform identifier (slack, whatsapp, api, etc.) for routing
|
|
382
|
-
content?: string; // Used only for ephemeral messages (OAuth/auth flows)
|
|
383
|
-
delta?: string;
|
|
384
|
-
isFullReplacement?: boolean;
|
|
385
|
-
processedMessageIds?: string[];
|
|
386
|
-
error?: string;
|
|
387
|
-
errorCode?: string;
|
|
388
|
-
timestamp: number;
|
|
389
|
-
originalMessageId?: string;
|
|
390
|
-
moduleData?: Record<string, unknown>;
|
|
391
|
-
botResponseId?: string;
|
|
392
|
-
ephemeral?: boolean; // If true, message should be sent as ephemeral (only visible to user)
|
|
393
|
-
platformMetadata?: Record<string, unknown>;
|
|
394
|
-
statusUpdate?: {
|
|
395
|
-
elapsedSeconds: number;
|
|
396
|
-
state: string; // e.g., "is running" or "is scheduling"
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
// Exec-specific response fields (for jobType === "exec")
|
|
400
|
-
execId?: string; // Exec job ID for response routing
|
|
401
|
-
execStream?: "stdout" | "stderr"; // Which stream this delta is from
|
|
402
|
-
execExitCode?: number; // Process exit code (sent on completion)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ============================================================================
|
|
406
|
-
// User Interaction Types
|
|
407
|
-
// ============================================================================
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Suggested prompt for user
|
|
411
|
-
*/
|
|
412
|
-
export interface SuggestedPrompt {
|
|
413
|
-
title: string; // Short label shown as chip
|
|
414
|
-
message: string; // Full message sent when clicked
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Skill registry entry (global or per-agent).
|
|
419
|
-
*/
|
|
420
|
-
export interface RegistryEntry {
|
|
421
|
-
id: string;
|
|
422
|
-
type: string;
|
|
423
|
-
apiUrl: string;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Non-blocking suggestions - agent continues immediately
|
|
428
|
-
* Used for optional next steps
|
|
429
|
-
*/
|
|
430
|
-
export interface UserSuggestion {
|
|
431
|
-
id: string;
|
|
432
|
-
userId: string;
|
|
433
|
-
conversationId: string;
|
|
434
|
-
channelId: string;
|
|
435
|
-
teamId?: string;
|
|
436
|
-
|
|
437
|
-
blocking: false; // Always false - distinguishes from interactions
|
|
438
|
-
|
|
439
|
-
prompts: SuggestedPrompt[];
|
|
440
|
-
}
|
package/src/utils/encryption.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import * as crypto from "node:crypto";
|
|
2
|
-
import { createLogger } from "../logger";
|
|
3
|
-
|
|
4
|
-
const IV_LENGTH = 12; // 96-bit nonce for AES-GCM
|
|
5
|
-
const logger = createLogger("encryption");
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Get encryption key from environment with validation
|
|
9
|
-
*
|
|
10
|
-
* IMPORTANT: The ENCRYPTION_KEY must be exactly 32 bytes (256 bits) for AES-256.
|
|
11
|
-
* Generate a secure key using: `openssl rand -base64 32` or `openssl rand -hex 32`
|
|
12
|
-
*/
|
|
13
|
-
function getEncryptionKey(): Buffer {
|
|
14
|
-
const key = process.env.ENCRYPTION_KEY || "";
|
|
15
|
-
if (!key) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
"ENCRYPTION_KEY environment variable is required for secure operation"
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Try to decode as base64 first (most common format)
|
|
22
|
-
let keyBuffer: Buffer;
|
|
23
|
-
try {
|
|
24
|
-
keyBuffer = Buffer.from(key, "base64");
|
|
25
|
-
if (keyBuffer.length === 32) {
|
|
26
|
-
return keyBuffer;
|
|
27
|
-
}
|
|
28
|
-
} catch (err) {
|
|
29
|
-
logger.debug("ENCRYPTION_KEY is not valid base64, trying hex format", err);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Try as hex (must be exactly 64 hex characters for 32 bytes)
|
|
33
|
-
if (/^[0-9a-fA-F]{64}$/.test(key)) {
|
|
34
|
-
keyBuffer = Buffer.from(key, "hex");
|
|
35
|
-
if (keyBuffer.length === 32) {
|
|
36
|
-
return keyBuffer;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
throw new Error(
|
|
41
|
-
"ENCRYPTION_KEY must be a base64 or hex encoded 32-byte key. " +
|
|
42
|
-
"Generate a valid key with: openssl rand -base64 32"
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Encrypt a string using AES-256-GCM
|
|
48
|
-
*/
|
|
49
|
-
export function encrypt(text: string): string {
|
|
50
|
-
const encryptionKey = getEncryptionKey();
|
|
51
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
52
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey, iv);
|
|
53
|
-
const encrypted = Buffer.concat([
|
|
54
|
-
cipher.update(text, "utf8"),
|
|
55
|
-
cipher.final(),
|
|
56
|
-
]);
|
|
57
|
-
const tag = cipher.getAuthTag();
|
|
58
|
-
return `${iv.toString("hex")}:${tag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Decrypt a string encrypted with AES-256-GCM
|
|
63
|
-
*/
|
|
64
|
-
export function decrypt(text: string): string {
|
|
65
|
-
const encryptionKey = getEncryptionKey();
|
|
66
|
-
const parts = text.split(":");
|
|
67
|
-
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
68
|
-
const iv = Buffer.from(parts[0]!, "hex");
|
|
69
|
-
const tag = Buffer.from(parts[1]!, "hex");
|
|
70
|
-
const encryptedText = Buffer.from(parts[2]!, "hex");
|
|
71
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", encryptionKey, iv);
|
|
72
|
-
decipher.setAuthTag(tag);
|
|
73
|
-
const decrypted = Buffer.concat([
|
|
74
|
-
decipher.update(encryptedText),
|
|
75
|
-
decipher.final(),
|
|
76
|
-
]);
|
|
77
|
-
return decrypted.toString("utf8");
|
|
78
|
-
}
|
package/src/utils/env.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { ConfigError } from "../errors";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Get required environment variable
|
|
5
|
-
* Throws ConfigError if not set
|
|
6
|
-
*/
|
|
7
|
-
export function getRequiredEnv(name: string): string {
|
|
8
|
-
const value = process.env[name];
|
|
9
|
-
if (!value) {
|
|
10
|
-
throw new ConfigError(`Missing required environment variable: ${name}`);
|
|
11
|
-
}
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get optional environment variable with default
|
|
17
|
-
*/
|
|
18
|
-
export function getOptionalEnv(name: string, defaultValue: string): string {
|
|
19
|
-
return process.env[name] || defaultValue;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get optional number environment variable with default
|
|
24
|
-
* Throws ConfigError if value is not a valid number
|
|
25
|
-
*/
|
|
26
|
-
export function getOptionalNumber(name: string, defaultValue: number): number {
|
|
27
|
-
const value = process.env[name];
|
|
28
|
-
if (!value) return defaultValue;
|
|
29
|
-
const parsed = parseInt(value, 10);
|
|
30
|
-
if (Number.isNaN(parsed)) {
|
|
31
|
-
throw new ConfigError(
|
|
32
|
-
`Invalid number for ${name}: ${value} (expected integer)`
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
return parsed;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get optional boolean environment variable with default
|
|
40
|
-
* Accepts "true", "1", "yes" as truthy values
|
|
41
|
-
*/
|
|
42
|
-
export function getOptionalBoolean(
|
|
43
|
-
name: string,
|
|
44
|
-
defaultValue: boolean
|
|
45
|
-
): boolean {
|
|
46
|
-
const value = process.env[name];
|
|
47
|
-
if (!value) return defaultValue;
|
|
48
|
-
const lower = value.toLowerCase();
|
|
49
|
-
return lower === "true" || lower === "1" || lower === "yes";
|
|
50
|
-
}
|
package/src/utils/json.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { createLogger } from "../logger";
|
|
2
|
-
|
|
3
|
-
const logger = createLogger("json-utils");
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Safely parse JSON string
|
|
7
|
-
* Returns null on parse failure instead of throwing
|
|
8
|
-
*/
|
|
9
|
-
export function safeJsonParse<T = unknown>(
|
|
10
|
-
data: string,
|
|
11
|
-
fallback: T | null = null
|
|
12
|
-
): T | null {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(data) as T;
|
|
15
|
-
} catch (error) {
|
|
16
|
-
logger.debug("JSON parse failed", {
|
|
17
|
-
error: error instanceof Error ? error.message : String(error),
|
|
18
|
-
dataPreview: data.substring(0, 100),
|
|
19
|
-
});
|
|
20
|
-
return fallback;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Safely stringify value to JSON
|
|
26
|
-
* Returns null on stringify failure instead of throwing
|
|
27
|
-
*/
|
|
28
|
-
export function safeJsonStringify(value: unknown): string | null {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.stringify(value);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
logger.error("JSON stringify failed", {
|
|
33
|
-
error: error instanceof Error ? error.message : String(error),
|
|
34
|
-
});
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/utils/lock.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Async lock for serializing concurrent operations
|
|
3
|
-
* Prevents race conditions in async code by ensuring only one operation runs at a time
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* class StreamSession {
|
|
8
|
-
* private streamLock = new AsyncLock();
|
|
9
|
-
*
|
|
10
|
-
* async appendDelta(delta: string) {
|
|
11
|
-
* return this.streamLock.acquire(() => this.appendDeltaUnsafe(delta));
|
|
12
|
-
* }
|
|
13
|
-
*
|
|
14
|
-
* private async appendDeltaUnsafe(delta: string) {
|
|
15
|
-
* // Critical section - only one execution at a time
|
|
16
|
-
* }
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export class AsyncLock {
|
|
21
|
-
private lock: Promise<void> = Promise.resolve();
|
|
22
|
-
private lockContext: string;
|
|
23
|
-
|
|
24
|
-
constructor(context: string = "unknown") {
|
|
25
|
-
this.lockContext = context;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Acquire lock and execute function exclusively
|
|
30
|
-
*
|
|
31
|
-
* @param fn - The async function to execute with exclusive access
|
|
32
|
-
* @param timeoutMs - Maximum time to wait for lock acquisition (default: 30s)
|
|
33
|
-
* @returns The result of the function
|
|
34
|
-
* @throws Error if lock acquisition times out
|
|
35
|
-
*/
|
|
36
|
-
async acquire<T>(
|
|
37
|
-
fn: () => Promise<T>,
|
|
38
|
-
timeoutMs: number = 30000
|
|
39
|
-
): Promise<T> {
|
|
40
|
-
const currentLock = this.lock;
|
|
41
|
-
let releaseLock: (() => void) | undefined;
|
|
42
|
-
|
|
43
|
-
// Create new lock that will be released when fn completes
|
|
44
|
-
this.lock = new Promise<void>((resolve) => {
|
|
45
|
-
releaseLock = resolve;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
// Wait for previous operation with timeout to prevent deadlock
|
|
52
|
-
const lockTimeout = new Promise<never>((_, reject) => {
|
|
53
|
-
timeoutId = setTimeout(() => {
|
|
54
|
-
reject(
|
|
55
|
-
new Error(
|
|
56
|
-
`Lock acquisition timeout after ${timeoutMs}ms - possible deadlock in ${this.lockContext}`
|
|
57
|
-
)
|
|
58
|
-
);
|
|
59
|
-
}, timeoutMs);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await Promise.race([currentLock, lockTimeout]);
|
|
63
|
-
|
|
64
|
-
// Execute function with exclusive access
|
|
65
|
-
return await fn();
|
|
66
|
-
} finally {
|
|
67
|
-
// Clear the timeout to prevent leak
|
|
68
|
-
if (timeoutId !== undefined) {
|
|
69
|
-
clearTimeout(timeoutId);
|
|
70
|
-
}
|
|
71
|
-
// Always release lock, even on error
|
|
72
|
-
releaseLock?.();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|