@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.
Files changed (156) hide show
  1. package/.env.example +57 -0
  2. package/README.md +374 -0
  3. package/dist/index.js +189 -0
  4. package/dist/mcp/index.js +1132 -0
  5. package/docker-compose.prod.yml +91 -0
  6. package/docker-compose.yml +358 -0
  7. package/drizzle/0000_dapper_the_professor.sql +159 -0
  8. package/drizzle/0001_api_keys.sql +51 -0
  9. package/drizzle/meta/0000_snapshot.json +1532 -0
  10. package/drizzle/meta/_journal.json +13 -0
  11. package/drizzle.config.ts +20 -0
  12. package/package.json +114 -0
  13. package/scripts/add-extraction-job.ts +122 -0
  14. package/scripts/benchmark-pgvector.ts +122 -0
  15. package/scripts/bootstrap.sh +209 -0
  16. package/scripts/check-runtime-pack.ts +111 -0
  17. package/scripts/claude-mcp-config.ts +336 -0
  18. package/scripts/docker-entrypoint.sh +183 -0
  19. package/scripts/doctor.ts +377 -0
  20. package/scripts/init-db.sql +33 -0
  21. package/scripts/install.sh +1110 -0
  22. package/scripts/mcp-setup.ts +271 -0
  23. package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
  24. package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
  25. package/scripts/migrations/003_create_hnsw_index.sql +94 -0
  26. package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
  27. package/scripts/migrations/005_create_chunks_table.sql +95 -0
  28. package/scripts/migrations/006_create_processing_queue.sql +45 -0
  29. package/scripts/migrations/generate_test_data.sql +42 -0
  30. package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
  31. package/scripts/migrations/run_migrations.sh +286 -0
  32. package/scripts/migrations/test_hnsw_index.sql +255 -0
  33. package/scripts/pre-commit-secrets +282 -0
  34. package/scripts/run-extraction-worker.ts +46 -0
  35. package/scripts/run-phase1-tests.sh +291 -0
  36. package/scripts/setup.ts +222 -0
  37. package/scripts/smoke-install.sh +12 -0
  38. package/scripts/test-health-endpoint.sh +328 -0
  39. package/src/api/index.ts +2 -0
  40. package/src/api/middleware/auth.ts +80 -0
  41. package/src/api/middleware/csrf.ts +308 -0
  42. package/src/api/middleware/errorHandler.ts +166 -0
  43. package/src/api/middleware/rateLimit.ts +360 -0
  44. package/src/api/middleware/validation.ts +514 -0
  45. package/src/api/routes/documents.ts +286 -0
  46. package/src/api/routes/profiles.ts +237 -0
  47. package/src/api/routes/search.ts +71 -0
  48. package/src/api/stores/index.ts +58 -0
  49. package/src/config/bootstrap-env.ts +3 -0
  50. package/src/config/env.ts +71 -0
  51. package/src/config/feature-flags.ts +25 -0
  52. package/src/config/index.ts +140 -0
  53. package/src/config/secrets.config.ts +291 -0
  54. package/src/db/client.ts +92 -0
  55. package/src/db/index.ts +73 -0
  56. package/src/db/postgres.ts +72 -0
  57. package/src/db/schema/chunks.schema.ts +31 -0
  58. package/src/db/schema/containers.schema.ts +46 -0
  59. package/src/db/schema/documents.schema.ts +49 -0
  60. package/src/db/schema/embeddings.schema.ts +32 -0
  61. package/src/db/schema/index.ts +11 -0
  62. package/src/db/schema/memories.schema.ts +72 -0
  63. package/src/db/schema/profiles.schema.ts +34 -0
  64. package/src/db/schema/queue.schema.ts +59 -0
  65. package/src/db/schema/relationships.schema.ts +42 -0
  66. package/src/db/schema.ts +223 -0
  67. package/src/db/worker-connection.ts +47 -0
  68. package/src/index.ts +235 -0
  69. package/src/mcp/CLAUDE.md +1 -0
  70. package/src/mcp/index.ts +1380 -0
  71. package/src/mcp/legacyState.ts +22 -0
  72. package/src/mcp/rateLimit.ts +358 -0
  73. package/src/mcp/resources.ts +309 -0
  74. package/src/mcp/results.ts +104 -0
  75. package/src/mcp/tools.ts +401 -0
  76. package/src/queues/config.ts +119 -0
  77. package/src/queues/index.ts +289 -0
  78. package/src/sdk/client.ts +225 -0
  79. package/src/sdk/errors.ts +266 -0
  80. package/src/sdk/http.ts +560 -0
  81. package/src/sdk/index.ts +244 -0
  82. package/src/sdk/resources/base.ts +65 -0
  83. package/src/sdk/resources/connections.ts +204 -0
  84. package/src/sdk/resources/documents.ts +163 -0
  85. package/src/sdk/resources/index.ts +10 -0
  86. package/src/sdk/resources/memories.ts +150 -0
  87. package/src/sdk/resources/search.ts +60 -0
  88. package/src/sdk/resources/settings.ts +36 -0
  89. package/src/sdk/types.ts +674 -0
  90. package/src/services/chunking/index.ts +451 -0
  91. package/src/services/chunking.service.ts +650 -0
  92. package/src/services/csrf.service.ts +252 -0
  93. package/src/services/documents.repository.ts +219 -0
  94. package/src/services/documents.service.ts +191 -0
  95. package/src/services/embedding.service.ts +404 -0
  96. package/src/services/extraction.service.ts +300 -0
  97. package/src/services/extractors/code.extractor.ts +451 -0
  98. package/src/services/extractors/index.ts +9 -0
  99. package/src/services/extractors/markdown.extractor.ts +461 -0
  100. package/src/services/extractors/pdf.extractor.ts +315 -0
  101. package/src/services/extractors/text.extractor.ts +118 -0
  102. package/src/services/extractors/url.extractor.ts +243 -0
  103. package/src/services/index.ts +235 -0
  104. package/src/services/ingestion.service.ts +177 -0
  105. package/src/services/llm/anthropic.ts +400 -0
  106. package/src/services/llm/base.ts +460 -0
  107. package/src/services/llm/contradiction-detector.service.ts +526 -0
  108. package/src/services/llm/heuristics.ts +148 -0
  109. package/src/services/llm/index.ts +309 -0
  110. package/src/services/llm/memory-classifier.service.ts +383 -0
  111. package/src/services/llm/memory-extension-detector.service.ts +523 -0
  112. package/src/services/llm/mock.ts +470 -0
  113. package/src/services/llm/openai.ts +398 -0
  114. package/src/services/llm/prompts.ts +438 -0
  115. package/src/services/llm/types.ts +373 -0
  116. package/src/services/memory.repository.ts +1769 -0
  117. package/src/services/memory.service.ts +1338 -0
  118. package/src/services/memory.types.ts +234 -0
  119. package/src/services/persistence/index.ts +295 -0
  120. package/src/services/pipeline.service.ts +509 -0
  121. package/src/services/profile.repository.ts +436 -0
  122. package/src/services/profile.service.ts +560 -0
  123. package/src/services/profile.types.ts +270 -0
  124. package/src/services/relationships/detector.ts +1128 -0
  125. package/src/services/relationships/index.ts +268 -0
  126. package/src/services/relationships/memory-integration.ts +459 -0
  127. package/src/services/relationships/strategies.ts +132 -0
  128. package/src/services/relationships/types.ts +370 -0
  129. package/src/services/search.service.ts +761 -0
  130. package/src/services/search.types.ts +220 -0
  131. package/src/services/secrets.service.ts +384 -0
  132. package/src/services/vectorstore/base.ts +327 -0
  133. package/src/services/vectorstore/index.ts +444 -0
  134. package/src/services/vectorstore/memory.ts +286 -0
  135. package/src/services/vectorstore/migration.ts +295 -0
  136. package/src/services/vectorstore/mock.ts +403 -0
  137. package/src/services/vectorstore/pgvector.ts +695 -0
  138. package/src/services/vectorstore/types.ts +247 -0
  139. package/src/startup.ts +389 -0
  140. package/src/types/api.types.ts +193 -0
  141. package/src/types/document.types.ts +103 -0
  142. package/src/types/index.ts +241 -0
  143. package/src/types/profile.base.ts +133 -0
  144. package/src/utils/errors.ts +447 -0
  145. package/src/utils/id.ts +15 -0
  146. package/src/utils/index.ts +101 -0
  147. package/src/utils/logger.ts +313 -0
  148. package/src/utils/sanitization.ts +501 -0
  149. package/src/utils/secret-validation.ts +273 -0
  150. package/src/utils/synonyms.ts +188 -0
  151. package/src/utils/validation.ts +581 -0
  152. package/src/workers/chunking.worker.ts +242 -0
  153. package/src/workers/embedding.worker.ts +358 -0
  154. package/src/workers/extraction.worker.ts +346 -0
  155. package/src/workers/indexing.worker.ts +505 -0
  156. 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
+ }