@twelvehart/supermemory-runtime 1.0.0-next.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/.env.example +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Store Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for vector similarity search functionality.
|
|
5
|
+
* These types are designed to be provider-agnostic, supporting
|
|
6
|
+
* in-memory, SQLite-VSS, Chroma, and other vector stores.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Supported vector store providers
|
|
11
|
+
*/
|
|
12
|
+
export type VectorStoreProvider = 'memory' | 'pgvector'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Similarity metrics for vector comparison
|
|
16
|
+
*/
|
|
17
|
+
export type SimilarityMetric = 'cosine' | 'euclidean' | 'dot_product'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Index types for vector search optimization
|
|
21
|
+
*/
|
|
22
|
+
export type IndexType = 'flat' | 'hnsw' | 'ivf'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Metadata filter operators
|
|
26
|
+
*/
|
|
27
|
+
export type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Metadata filter for search queries
|
|
31
|
+
*/
|
|
32
|
+
export interface MetadataFilter {
|
|
33
|
+
/** Field name to filter on */
|
|
34
|
+
key: string
|
|
35
|
+
/** Filter operator */
|
|
36
|
+
operator: FilterOperator
|
|
37
|
+
/** Value to compare against */
|
|
38
|
+
value: string | number | boolean | Array<string | number>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Vector entry for storage and retrieval
|
|
43
|
+
*/
|
|
44
|
+
export interface VectorEntry {
|
|
45
|
+
/** Unique identifier */
|
|
46
|
+
id: string
|
|
47
|
+
/** Vector embedding */
|
|
48
|
+
embedding: number[]
|
|
49
|
+
/** Associated metadata */
|
|
50
|
+
metadata: Record<string, unknown>
|
|
51
|
+
/** Timestamp of creation */
|
|
52
|
+
createdAt?: Date
|
|
53
|
+
/** Timestamp of last update */
|
|
54
|
+
updatedAt?: Date
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for vector search
|
|
59
|
+
*/
|
|
60
|
+
export interface SearchOptions {
|
|
61
|
+
/** Maximum number of results to return */
|
|
62
|
+
limit?: number
|
|
63
|
+
/** Minimum similarity threshold (0-1 for cosine, varies for others) */
|
|
64
|
+
threshold?: number
|
|
65
|
+
/** Metadata filters to apply */
|
|
66
|
+
filters?: MetadataFilter[]
|
|
67
|
+
/** Whether to include vectors in results */
|
|
68
|
+
includeVectors?: boolean
|
|
69
|
+
/** Whether to include metadata in results */
|
|
70
|
+
includeMetadata?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Default search options
|
|
75
|
+
*/
|
|
76
|
+
export const DEFAULT_SEARCH_OPTIONS: Required<Omit<SearchOptions, 'filters'>> & {
|
|
77
|
+
filters?: MetadataFilter[]
|
|
78
|
+
} = {
|
|
79
|
+
limit: 10,
|
|
80
|
+
threshold: 0.7,
|
|
81
|
+
includeVectors: false,
|
|
82
|
+
includeMetadata: true,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Result from a vector similarity search
|
|
87
|
+
*/
|
|
88
|
+
export interface VectorSearchResult {
|
|
89
|
+
/** Unique identifier */
|
|
90
|
+
id: string
|
|
91
|
+
/** Similarity score */
|
|
92
|
+
score: number
|
|
93
|
+
/** Vector embedding (if requested) */
|
|
94
|
+
embedding?: number[]
|
|
95
|
+
/** Associated metadata */
|
|
96
|
+
metadata: Record<string, unknown>
|
|
97
|
+
/** Distance (if using distance metric) */
|
|
98
|
+
distance?: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Options for adding vectors
|
|
103
|
+
*/
|
|
104
|
+
export interface AddOptions {
|
|
105
|
+
/** Whether to overwrite existing entries with same ID */
|
|
106
|
+
overwrite?: boolean
|
|
107
|
+
/** Namespace/collection for the vector */
|
|
108
|
+
namespace?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Options for deleting vectors
|
|
113
|
+
*/
|
|
114
|
+
export interface DeleteOptions {
|
|
115
|
+
/** Delete by IDs */
|
|
116
|
+
ids?: string[]
|
|
117
|
+
/** Delete by metadata filter */
|
|
118
|
+
filter?: MetadataFilter
|
|
119
|
+
/** Delete all vectors in namespace */
|
|
120
|
+
deleteAll?: boolean
|
|
121
|
+
/** Namespace/collection to delete from */
|
|
122
|
+
namespace?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Vector store configuration
|
|
127
|
+
*/
|
|
128
|
+
export interface VectorStoreConfig {
|
|
129
|
+
/** Store provider type */
|
|
130
|
+
provider: VectorStoreProvider
|
|
131
|
+
/** Vector dimensions */
|
|
132
|
+
dimensions: number
|
|
133
|
+
/** Similarity metric to use */
|
|
134
|
+
metric?: SimilarityMetric
|
|
135
|
+
/** Index type for optimization */
|
|
136
|
+
indexType?: IndexType
|
|
137
|
+
/** Default namespace */
|
|
138
|
+
defaultNamespace?: string
|
|
139
|
+
|
|
140
|
+
// Provider-specific options
|
|
141
|
+
/** SQLite database path (for sqlite-vss) */
|
|
142
|
+
sqlitePath?: string
|
|
143
|
+
/** Chroma server URL (for chroma) */
|
|
144
|
+
chromaUrl?: string
|
|
145
|
+
/** Chroma collection name (for chroma) */
|
|
146
|
+
chromaCollection?: string
|
|
147
|
+
/** HNSW parameters */
|
|
148
|
+
hnswConfig?: HNSWConfig
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* HNSW index configuration
|
|
153
|
+
*/
|
|
154
|
+
export interface HNSWConfig {
|
|
155
|
+
/** Maximum number of connections per node */
|
|
156
|
+
M?: number
|
|
157
|
+
/** Size of dynamic candidate list during construction */
|
|
158
|
+
efConstruction?: number
|
|
159
|
+
/** Size of dynamic candidate list during search */
|
|
160
|
+
efSearch?: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Default HNSW configuration
|
|
165
|
+
*/
|
|
166
|
+
export const DEFAULT_HNSW_CONFIG: Required<HNSWConfig> = {
|
|
167
|
+
M: 16,
|
|
168
|
+
efConstruction: 200,
|
|
169
|
+
efSearch: 50,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Vector store statistics
|
|
174
|
+
*/
|
|
175
|
+
export interface VectorStoreStats {
|
|
176
|
+
/** Total number of vectors stored */
|
|
177
|
+
totalVectors: number
|
|
178
|
+
/** Vector dimensions */
|
|
179
|
+
dimensions: number
|
|
180
|
+
/** Index type being used */
|
|
181
|
+
indexType: IndexType
|
|
182
|
+
/** Similarity metric being used */
|
|
183
|
+
metric: SimilarityMetric
|
|
184
|
+
/** Memory usage in bytes (if available) */
|
|
185
|
+
memoryUsageBytes?: number
|
|
186
|
+
/** Index build status */
|
|
187
|
+
indexBuilt: boolean
|
|
188
|
+
/** Namespaces/collections available */
|
|
189
|
+
namespaces?: string[]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Batch operation result
|
|
194
|
+
*/
|
|
195
|
+
export interface BatchResult {
|
|
196
|
+
/** Number of successful operations */
|
|
197
|
+
successful: number
|
|
198
|
+
/** Number of failed operations */
|
|
199
|
+
failed: number
|
|
200
|
+
/** Error messages for failed operations */
|
|
201
|
+
errors?: Array<{ id: string; error: string }>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Migration options for moving between vector stores
|
|
206
|
+
*/
|
|
207
|
+
export interface MigrationOptions {
|
|
208
|
+
/** Source vector store */
|
|
209
|
+
source: VectorStoreProvider
|
|
210
|
+
/** Target vector store */
|
|
211
|
+
target: VectorStoreProvider
|
|
212
|
+
/** Batch size for migration */
|
|
213
|
+
batchSize?: number
|
|
214
|
+
/** Progress callback */
|
|
215
|
+
onProgress?: (progress: MigrationProgress) => void
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Migration progress information
|
|
220
|
+
*/
|
|
221
|
+
export interface MigrationProgress {
|
|
222
|
+
/** Total vectors to migrate */
|
|
223
|
+
total: number
|
|
224
|
+
/** Vectors migrated so far */
|
|
225
|
+
migrated: number
|
|
226
|
+
/** Percentage complete */
|
|
227
|
+
percentage: number
|
|
228
|
+
/** Current batch number */
|
|
229
|
+
currentBatch: number
|
|
230
|
+
/** Total batches */
|
|
231
|
+
totalBatches: number
|
|
232
|
+
/** Estimated time remaining in seconds */
|
|
233
|
+
estimatedTimeRemaining?: number
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Vector store event types
|
|
238
|
+
*/
|
|
239
|
+
export type VectorStoreEvent = 'add' | 'update' | 'delete' | 'search' | 'index_built' | 'index_rebuilt' | 'error'
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Vector store event listener
|
|
243
|
+
*/
|
|
244
|
+
export interface VectorStoreEventListener {
|
|
245
|
+
event: VectorStoreEvent
|
|
246
|
+
callback: (data: unknown) => void
|
|
247
|
+
}
|
package/src/startup.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Startup Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates all required secrets and configuration on startup.
|
|
5
|
+
* Implements fail-fast pattern to prevent running with invalid configuration.
|
|
6
|
+
*
|
|
7
|
+
* This module should be imported and called FIRST in the application entry point.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getSecretsService } from './services/secrets.service.js'
|
|
11
|
+
import { validateApiKey, validateDatabaseUrl, checkSecretStrength } from './utils/secret-validation.js'
|
|
12
|
+
import { getRequiredSecrets, getOptionalSecrets, type SecretDefinition } from './config/secrets.config.js'
|
|
13
|
+
import { logger } from './utils/logger.js'
|
|
14
|
+
import { AppError, ErrorCode } from './utils/errors.js'
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Startup validation result
|
|
22
|
+
*/
|
|
23
|
+
export interface StartupValidationResult {
|
|
24
|
+
/** All validations passed */
|
|
25
|
+
success: boolean
|
|
26
|
+
/** Fatal errors (prevent startup) */
|
|
27
|
+
errors: string[]
|
|
28
|
+
/** Non-fatal warnings */
|
|
29
|
+
warnings: string[]
|
|
30
|
+
/** Loaded secrets count */
|
|
31
|
+
secretsLoaded: number
|
|
32
|
+
/** Weak secrets detected */
|
|
33
|
+
weakSecrets: string[]
|
|
34
|
+
/** Missing optional secrets */
|
|
35
|
+
missingOptional: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration summary (sanitized for logging)
|
|
40
|
+
*/
|
|
41
|
+
export interface ConfigurationSummary {
|
|
42
|
+
/** Environment */
|
|
43
|
+
environment: string
|
|
44
|
+
/** Node version */
|
|
45
|
+
nodeVersion: string
|
|
46
|
+
/** Required secrets status */
|
|
47
|
+
requiredSecrets: {
|
|
48
|
+
present: string[]
|
|
49
|
+
missing: string[]
|
|
50
|
+
}
|
|
51
|
+
/** Optional secrets status */
|
|
52
|
+
optionalSecrets: {
|
|
53
|
+
present: string[]
|
|
54
|
+
missing: string[]
|
|
55
|
+
}
|
|
56
|
+
/** Database configuration (sanitized) */
|
|
57
|
+
database?: {
|
|
58
|
+
type: string
|
|
59
|
+
host: string
|
|
60
|
+
port: number
|
|
61
|
+
database: string
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Validation Functions
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate all secrets on application startup
|
|
71
|
+
* @returns Validation result
|
|
72
|
+
* @throws AppError if critical validation fails
|
|
73
|
+
*/
|
|
74
|
+
export function validateSecretsOnStartup(): StartupValidationResult {
|
|
75
|
+
const result: StartupValidationResult = {
|
|
76
|
+
success: true,
|
|
77
|
+
errors: [],
|
|
78
|
+
warnings: [],
|
|
79
|
+
secretsLoaded: 0,
|
|
80
|
+
weakSecrets: [],
|
|
81
|
+
missingOptional: [],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logger.info('[Startup] Validating secrets configuration...')
|
|
85
|
+
|
|
86
|
+
// Check required secrets
|
|
87
|
+
const requiredSecrets = getRequiredSecrets()
|
|
88
|
+
const missingRequired: string[] = []
|
|
89
|
+
|
|
90
|
+
for (const secret of requiredSecrets) {
|
|
91
|
+
const value = process.env[secret.envVar]
|
|
92
|
+
|
|
93
|
+
if (!value) {
|
|
94
|
+
missingRequired.push(secret.envVar)
|
|
95
|
+
result.errors.push(`Required secret missing: ${secret.envVar} - ${secret.description}`)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Validate the secret
|
|
100
|
+
const validation = validateSecret(secret, value)
|
|
101
|
+
if (!validation.valid) {
|
|
102
|
+
result.errors.push(`Invalid secret ${secret.envVar}: ${validation.errors.join(', ')}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (validation.warnings.length > 0) {
|
|
106
|
+
result.warnings.push(`${secret.envVar}: ${validation.warnings.join(', ')}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (validation.weak) {
|
|
110
|
+
result.weakSecrets.push(secret.envVar)
|
|
111
|
+
result.warnings.push(`Weak secret detected: ${secret.envVar}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
result.secretsLoaded++
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check optional secrets
|
|
118
|
+
const optionalSecrets = getOptionalSecrets()
|
|
119
|
+
|
|
120
|
+
for (const secret of optionalSecrets) {
|
|
121
|
+
const value = process.env[secret.envVar]
|
|
122
|
+
|
|
123
|
+
if (!value) {
|
|
124
|
+
result.missingOptional.push(secret.envVar)
|
|
125
|
+
logger.debug(`[Startup] Optional secret not set: ${secret.envVar}`, {
|
|
126
|
+
description: secret.description,
|
|
127
|
+
default: secret.defaultValue ? '[has default]' : '[no default]',
|
|
128
|
+
})
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate the secret
|
|
133
|
+
const validation = validateSecret(secret, value)
|
|
134
|
+
if (!validation.valid) {
|
|
135
|
+
result.warnings.push(`Optional secret ${secret.envVar} is invalid: ${validation.errors.join(', ')}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (validation.warnings.length > 0) {
|
|
139
|
+
result.warnings.push(`${secret.envVar}: ${validation.warnings.join(', ')}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (validation.weak) {
|
|
143
|
+
result.weakSecrets.push(secret.envVar)
|
|
144
|
+
result.warnings.push(`Weak optional secret: ${secret.envVar}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result.secretsLoaded++
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Determine overall success
|
|
151
|
+
result.success = result.errors.length === 0
|
|
152
|
+
|
|
153
|
+
return result
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validate a single secret according to its definition
|
|
158
|
+
*/
|
|
159
|
+
function validateSecret(
|
|
160
|
+
definition: SecretDefinition,
|
|
161
|
+
value: string
|
|
162
|
+
): {
|
|
163
|
+
valid: boolean
|
|
164
|
+
errors: string[]
|
|
165
|
+
warnings: string[]
|
|
166
|
+
weak: boolean
|
|
167
|
+
} {
|
|
168
|
+
const errors: string[] = []
|
|
169
|
+
const warnings: string[] = []
|
|
170
|
+
let weak = false
|
|
171
|
+
|
|
172
|
+
// Check minimum length
|
|
173
|
+
if (definition.minLength && value.length < definition.minLength) {
|
|
174
|
+
errors.push(`Must be at least ${definition.minLength} characters`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Format-specific validation
|
|
178
|
+
switch (definition.format) {
|
|
179
|
+
case 'api_key': {
|
|
180
|
+
const validation = validateApiKey(value)
|
|
181
|
+
if (!validation.valid && validation.error) {
|
|
182
|
+
warnings.push(validation.error)
|
|
183
|
+
}
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'database_url': {
|
|
188
|
+
try {
|
|
189
|
+
validateDatabaseUrl(value)
|
|
190
|
+
} catch {
|
|
191
|
+
errors.push('Invalid database URL format')
|
|
192
|
+
}
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case 'password':
|
|
197
|
+
case 'generic': {
|
|
198
|
+
const strength = checkSecretStrength(value)
|
|
199
|
+
if (strength.strength === 'weak' || strength.strength === 'fair') {
|
|
200
|
+
weak = true
|
|
201
|
+
warnings.push(...strength.recommendations)
|
|
202
|
+
}
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Custom validation function
|
|
208
|
+
if (definition.validate) {
|
|
209
|
+
const customValidation = definition.validate(value)
|
|
210
|
+
if (!customValidation.valid && customValidation.error) {
|
|
211
|
+
errors.push(customValidation.error)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
valid: errors.length === 0,
|
|
217
|
+
errors,
|
|
218
|
+
warnings,
|
|
219
|
+
weak,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get sanitized configuration summary for logging
|
|
225
|
+
*/
|
|
226
|
+
export function getConfigurationSummary(): ConfigurationSummary {
|
|
227
|
+
const requiredSecrets = getRequiredSecrets()
|
|
228
|
+
const optionalSecrets = getOptionalSecrets()
|
|
229
|
+
|
|
230
|
+
const summary: ConfigurationSummary = {
|
|
231
|
+
environment: process.env.NODE_ENV || 'development',
|
|
232
|
+
nodeVersion: process.version,
|
|
233
|
+
requiredSecrets: {
|
|
234
|
+
present: [],
|
|
235
|
+
missing: [],
|
|
236
|
+
},
|
|
237
|
+
optionalSecrets: {
|
|
238
|
+
present: [],
|
|
239
|
+
missing: [],
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check required secrets
|
|
244
|
+
for (const secret of requiredSecrets) {
|
|
245
|
+
if (process.env[secret.envVar]) {
|
|
246
|
+
summary.requiredSecrets.present.push(secret.envVar)
|
|
247
|
+
} else {
|
|
248
|
+
summary.requiredSecrets.missing.push(secret.envVar)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check optional secrets
|
|
253
|
+
for (const secret of optionalSecrets) {
|
|
254
|
+
if (process.env[secret.envVar]) {
|
|
255
|
+
summary.optionalSecrets.present.push(secret.envVar)
|
|
256
|
+
} else {
|
|
257
|
+
summary.optionalSecrets.missing.push(secret.envVar)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add sanitized database config if present
|
|
262
|
+
const databaseUrl = process.env.DATABASE_URL
|
|
263
|
+
if (databaseUrl) {
|
|
264
|
+
try {
|
|
265
|
+
const parsed = validateDatabaseUrl(databaseUrl)
|
|
266
|
+
summary.database = {
|
|
267
|
+
type: parsed.type,
|
|
268
|
+
host: parsed.host,
|
|
269
|
+
port: parsed.port,
|
|
270
|
+
database: parsed.database,
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Invalid database URL, skip
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return summary
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Initialize secrets service and validate configuration
|
|
282
|
+
* This should be called FIRST in the application startup sequence.
|
|
283
|
+
*
|
|
284
|
+
* @throws AppError if critical validation fails
|
|
285
|
+
*/
|
|
286
|
+
export async function initializeAndValidate(): Promise<void> {
|
|
287
|
+
logger.info('[Startup] Initializing application...')
|
|
288
|
+
|
|
289
|
+
// Validate secrets
|
|
290
|
+
const validationResult = validateSecretsOnStartup()
|
|
291
|
+
|
|
292
|
+
// Log configuration summary (sanitized)
|
|
293
|
+
const summary = getConfigurationSummary()
|
|
294
|
+
logger.info('[Startup] Configuration summary', {
|
|
295
|
+
environment: summary.environment,
|
|
296
|
+
nodeVersion: summary.nodeVersion,
|
|
297
|
+
requiredSecretsPresent: summary.requiredSecrets.present.length,
|
|
298
|
+
requiredSecretsMissing: summary.requiredSecrets.missing.length,
|
|
299
|
+
optionalSecretsPresent: summary.optionalSecrets.present.length,
|
|
300
|
+
database: summary.database
|
|
301
|
+
? `${summary.database.type}://${summary.database.host}:${summary.database.port}/${summary.database.database}`
|
|
302
|
+
: 'not configured',
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// Log warnings
|
|
306
|
+
if (validationResult.warnings.length > 0) {
|
|
307
|
+
logger.warn('[Startup] Configuration warnings', {
|
|
308
|
+
count: validationResult.warnings.length,
|
|
309
|
+
warnings: validationResult.warnings,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Log weak secrets warning
|
|
314
|
+
if (validationResult.weakSecrets.length > 0) {
|
|
315
|
+
logger.warn('[Startup] ⚠️ Weak secrets detected', {
|
|
316
|
+
secrets: validationResult.weakSecrets,
|
|
317
|
+
recommendation: 'Consider rotating these secrets with stronger values',
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Log missing optional secrets
|
|
322
|
+
if (validationResult.missingOptional.length > 0) {
|
|
323
|
+
logger.info('[Startup] Optional secrets not configured', {
|
|
324
|
+
secrets: validationResult.missingOptional,
|
|
325
|
+
note: 'These features may be disabled or using defaults',
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Fail fast on errors
|
|
330
|
+
if (!validationResult.success) {
|
|
331
|
+
logger.error('[Startup] ❌ Critical validation errors', {
|
|
332
|
+
errorCount: validationResult.errors.length,
|
|
333
|
+
errors: validationResult.errors,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
throw new AppError(`Startup validation failed: ${validationResult.errors.join('; ')}`, ErrorCode.VALIDATION_ERROR, {
|
|
337
|
+
errors: validationResult.errors,
|
|
338
|
+
missingRequired: summary.requiredSecrets.missing,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Initialize secrets service if master password is provided
|
|
343
|
+
const masterPassword = process.env.SECRETS_MASTER_PASSWORD
|
|
344
|
+
if (masterPassword) {
|
|
345
|
+
try {
|
|
346
|
+
const secretsService = getSecretsService()
|
|
347
|
+
secretsService.initialize(masterPassword)
|
|
348
|
+
logger.info('[Startup] Secrets service initialized')
|
|
349
|
+
} catch (error) {
|
|
350
|
+
logger.error('[Startup] Failed to initialize secrets service', {
|
|
351
|
+
error: error instanceof Error ? error.message : String(error),
|
|
352
|
+
})
|
|
353
|
+
throw error
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
logger.warn('[Startup] Secrets service not initialized (SECRETS_MASTER_PASSWORD not set)')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
logger.info('[Startup] ✅ Validation complete', {
|
|
360
|
+
secretsLoaded: validationResult.secretsLoaded,
|
|
361
|
+
warnings: validationResult.warnings.length,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Validate environment before starting the application
|
|
367
|
+
* Call this in your main entry point (src/api/index.ts or src/index.ts)
|
|
368
|
+
*/
|
|
369
|
+
export async function validateEnvironment(): Promise<void> {
|
|
370
|
+
try {
|
|
371
|
+
await initializeAndValidate()
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (error instanceof AppError) {
|
|
374
|
+
// Log structured error
|
|
375
|
+
logger.error('[Startup] Environment validation failed', {
|
|
376
|
+
code: error.code,
|
|
377
|
+
message: error.message,
|
|
378
|
+
details: error.details,
|
|
379
|
+
})
|
|
380
|
+
} else {
|
|
381
|
+
logger.error('[Startup] Unexpected error during validation', {
|
|
382
|
+
error: error instanceof Error ? error.message : String(error),
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Exit with error code
|
|
387
|
+
process.exit(1)
|
|
388
|
+
}
|
|
389
|
+
}
|