@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,71 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { isAbsolute, resolve } from 'node:path'
3
+ import { config as dotenvConfig } from 'dotenv'
4
+
5
+ export type EnvFileSource = 'cli' | 'SUPERMEMORY_ENV_FILE' | '.env.local' | '.env'
6
+
7
+ export interface EnvFileResolution {
8
+ path: string
9
+ exists: boolean
10
+ explicit: boolean
11
+ source: EnvFileSource
12
+ }
13
+
14
+ function toAbsolutePath(candidate: string, cwd: string): string {
15
+ return isAbsolute(candidate) ? candidate : resolve(cwd, candidate)
16
+ }
17
+
18
+ export function resolveEnvFile(options?: { cliEnvFile?: string; cwd?: string }): EnvFileResolution {
19
+ const cwd = options?.cwd ?? process.cwd()
20
+
21
+ if (options?.cliEnvFile) {
22
+ const path = toAbsolutePath(options.cliEnvFile, cwd)
23
+ return {
24
+ path,
25
+ exists: existsSync(path),
26
+ explicit: true,
27
+ source: 'cli',
28
+ }
29
+ }
30
+
31
+ if (process.env.SUPERMEMORY_ENV_FILE) {
32
+ const path = toAbsolutePath(process.env.SUPERMEMORY_ENV_FILE, cwd)
33
+ return {
34
+ path,
35
+ exists: existsSync(path),
36
+ explicit: true,
37
+ source: 'SUPERMEMORY_ENV_FILE',
38
+ }
39
+ }
40
+
41
+ const envLocalPath = resolve(cwd, '.env.local')
42
+ if (existsSync(envLocalPath)) {
43
+ return {
44
+ path: envLocalPath,
45
+ exists: true,
46
+ explicit: false,
47
+ source: '.env.local',
48
+ }
49
+ }
50
+
51
+ const envPath = resolve(cwd, '.env')
52
+ return {
53
+ path: envPath,
54
+ exists: existsSync(envPath),
55
+ explicit: false,
56
+ source: '.env',
57
+ }
58
+ }
59
+
60
+ export function loadEnvFile(options?: { cliEnvFile?: string; cwd?: string; override?: boolean }): EnvFileResolution {
61
+ const resolution = resolveEnvFile(options)
62
+
63
+ if (resolution.exists) {
64
+ dotenvConfig({
65
+ path: resolution.path,
66
+ override: options?.override ?? false,
67
+ })
68
+ }
69
+
70
+ return resolution
71
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Feature Flags for Memory Service Behavior
3
+ *
4
+ * Defaults are local/offline-friendly: LLM and embedding paths are disabled
5
+ * unless explicitly enabled via environment variables.
6
+ */
7
+
8
+ const ENV_FLAGS = {
9
+ MEMORY_ENABLE_LLM: 'MEMORY_ENABLE_LLM',
10
+ MEMORY_ENABLE_EMBEDDINGS: 'MEMORY_ENABLE_EMBEDDINGS',
11
+ } as const
12
+
13
+ function isFlagEnabled(name: string): boolean {
14
+ const raw = process.env[name]
15
+ if (!raw) return false
16
+ return raw.toLowerCase() === 'true' || raw === '1'
17
+ }
18
+
19
+ export function isLLMFeatureEnabled(): boolean {
20
+ return isFlagEnabled(ENV_FLAGS.MEMORY_ENABLE_LLM)
21
+ }
22
+
23
+ export function isEmbeddingRelationshipsEnabled(): boolean {
24
+ return isFlagEnabled(ENV_FLAGS.MEMORY_ENABLE_EMBEDDINGS)
25
+ }
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod'
2
+ import './bootstrap-env.js'
3
+ import { ConfigurationError } from '../utils/errors.js'
4
+
5
+ const configSchema = z.object({
6
+ // OpenAI (optional - local fallback available)
7
+ openaiApiKey: z.string().optional(),
8
+ embeddingModel: z.string().default('text-embedding-3-small'),
9
+ embeddingDimensions: z.coerce.number().default(1536),
10
+
11
+ // LLM Provider Configuration
12
+ llmProvider: z.enum(['openai', 'anthropic', 'mock']).optional().describe('LLM provider for memory extraction'),
13
+ anthropicApiKey: z.string().optional(),
14
+ llmModel: z.string().optional().describe('Override default model for LLM extraction'),
15
+ llmMaxTokens: z.coerce.number().default(2000),
16
+ llmTemperature: z.coerce.number().default(0.1),
17
+ llmTimeoutMs: z.coerce.number().default(30000),
18
+ llmMaxRetries: z.coerce.number().default(3),
19
+
20
+ // LLM Caching
21
+ llmCacheEnabled: z
22
+ .string()
23
+ .optional()
24
+ .transform((val) => val !== 'false')
25
+ .default('true'),
26
+ llmCacheTtlMs: z.coerce.number().default(900000), // 15 minutes
27
+
28
+ // Database
29
+ databaseUrl: z.string().default('postgresql://supermemory:supermemory_secret@localhost:5432/supermemory'),
30
+
31
+ // Vector Store
32
+ vectorStoreProvider: z.enum(['memory', 'sqlite-vss', 'chroma']).default('memory'),
33
+ vectorDimensions: z.coerce.number().default(1536),
34
+ vectorSqlitePath: z.string().default('./data/vectors.db'),
35
+ chromaUrl: z.string().default('http://localhost:8000'),
36
+ chromaCollection: z.string().default('supermemory_vectors'),
37
+
38
+ // Server
39
+ apiPort: z.coerce.number().default(3000),
40
+ apiHost: z.string().default('localhost'),
41
+
42
+ // Minimal API authentication (optional)
43
+ authEnabled: z
44
+ .string()
45
+ .optional()
46
+ .transform((val) => val === 'true' || val === '1')
47
+ .default('false'),
48
+ authToken: z.string().optional(),
49
+
50
+ // Rate Limiting
51
+ rateLimitRequests: z.coerce.number().default(100),
52
+ rateLimitWindowMs: z.coerce.number().default(60000),
53
+
54
+ // Logging
55
+ logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
56
+ })
57
+
58
+ export type Config = z.infer<typeof configSchema>
59
+
60
+ function isPostgresDatabaseUrl(url: string): boolean {
61
+ return url.startsWith('postgresql://') || url.startsWith('postgres://')
62
+ }
63
+
64
+ function normalizeEnvValue(value: string | undefined): string | undefined {
65
+ if (value === undefined) return undefined
66
+ const trimmed = value.trim()
67
+ return trimmed.length > 0 ? trimmed : undefined
68
+ }
69
+
70
+ function loadConfig(): Config {
71
+ const result = configSchema.safeParse({
72
+ openaiApiKey: normalizeEnvValue(process.env.OPENAI_API_KEY),
73
+ embeddingModel: normalizeEnvValue(process.env.EMBEDDING_MODEL),
74
+ embeddingDimensions: normalizeEnvValue(process.env.EMBEDDING_DIMENSIONS),
75
+
76
+ // LLM Provider
77
+ llmProvider: normalizeEnvValue(process.env.LLM_PROVIDER),
78
+ anthropicApiKey: normalizeEnvValue(process.env.ANTHROPIC_API_KEY),
79
+ llmModel: normalizeEnvValue(process.env.LLM_MODEL),
80
+ llmMaxTokens: normalizeEnvValue(process.env.LLM_MAX_TOKENS),
81
+ llmTemperature: normalizeEnvValue(process.env.LLM_TEMPERATURE),
82
+ llmTimeoutMs: normalizeEnvValue(process.env.LLM_TIMEOUT_MS),
83
+ llmMaxRetries: normalizeEnvValue(process.env.LLM_MAX_RETRIES),
84
+ llmCacheEnabled: normalizeEnvValue(process.env.LLM_CACHE_ENABLED),
85
+ llmCacheTtlMs: normalizeEnvValue(process.env.LLM_CACHE_TTL_MS),
86
+
87
+ databaseUrl: normalizeEnvValue(process.env.DATABASE_URL),
88
+
89
+ // Vector Store
90
+ vectorStoreProvider: normalizeEnvValue(process.env.VECTOR_STORE_PROVIDER),
91
+ vectorDimensions: normalizeEnvValue(process.env.VECTOR_DIMENSIONS),
92
+ vectorSqlitePath: normalizeEnvValue(process.env.VECTOR_SQLITE_PATH),
93
+ chromaUrl: normalizeEnvValue(process.env.CHROMA_URL),
94
+ chromaCollection: normalizeEnvValue(process.env.CHROMA_COLLECTION),
95
+
96
+ apiPort: normalizeEnvValue(process.env.API_PORT),
97
+ apiHost: normalizeEnvValue(process.env.API_HOST),
98
+ authEnabled: normalizeEnvValue(process.env.AUTH_ENABLED),
99
+ authToken: normalizeEnvValue(process.env.AUTH_TOKEN),
100
+ rateLimitRequests: normalizeEnvValue(process.env.RATE_LIMIT_REQUESTS),
101
+ rateLimitWindowMs: normalizeEnvValue(process.env.RATE_LIMIT_WINDOW_MS),
102
+ logLevel: normalizeEnvValue(process.env.LOG_LEVEL),
103
+ })
104
+
105
+ if (!result.success) {
106
+ console.error('Configuration validation failed:')
107
+ result.error.issues.forEach((issue) => {
108
+ console.error(` - ${issue.path.join('.')}: ${issue.message}`)
109
+ })
110
+ const fieldErrors: Record<string, string[]> = {}
111
+ result.error.issues.forEach((issue) => {
112
+ const path = issue.path.join('.') || '_root'
113
+ if (!fieldErrors[path]) {
114
+ fieldErrors[path] = []
115
+ }
116
+ fieldErrors[path].push(issue.message)
117
+ })
118
+ throw new ConfigurationError('Invalid configuration', undefined, { fieldErrors })
119
+ }
120
+
121
+ const config = result.data
122
+
123
+ if (process.env.NODE_ENV !== 'test' && !isPostgresDatabaseUrl(config.databaseUrl)) {
124
+ throw new ConfigurationError(
125
+ 'DATABASE_URL must use postgres:// or postgresql:// outside tests. SQLite is only allowed when NODE_ENV=test.',
126
+ undefined,
127
+ {
128
+ fieldErrors: {
129
+ databaseUrl: [
130
+ 'DATABASE_URL must use postgres:// or postgresql:// outside tests. SQLite is only allowed when NODE_ENV=test.',
131
+ ],
132
+ },
133
+ }
134
+ )
135
+ }
136
+
137
+ return config
138
+ }
139
+
140
+ export const config = loadConfig()
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Secrets Configuration
3
+ *
4
+ * Defines required secrets, optional secrets, validation rules, and rotation policies.
5
+ * Used during application startup to validate environment configuration.
6
+ */
7
+
8
+ /** Secret definition with validation rules */
9
+ export interface SecretDefinition {
10
+ envVar: string
11
+ description: string
12
+ required: boolean
13
+ format?: 'api_key' | 'database_url' | 'jwt' | 'password' | 'generic'
14
+ minLength?: number
15
+ rotationDays?: number
16
+ defaultValue?: string
17
+ validate?: (value: string) => { valid: boolean; error?: string }
18
+ }
19
+
20
+ /** Secret category for organization */
21
+ export interface SecretCategory {
22
+ name: string
23
+ description: string
24
+ secrets: SecretDefinition[]
25
+ }
26
+
27
+ export const DATABASE_SECRETS: SecretCategory = {
28
+ name: 'Database',
29
+ description: 'Database connection credentials',
30
+ secrets: [
31
+ {
32
+ envVar: 'DATABASE_URL',
33
+ description: 'PostgreSQL connection URL',
34
+ required: true,
35
+ format: 'database_url',
36
+ rotationDays: 90,
37
+ },
38
+ {
39
+ envVar: 'REDIS_URL',
40
+ description: 'Redis connection URL (for caching and queues)',
41
+ required: false,
42
+ format: 'database_url',
43
+ defaultValue: 'redis://localhost:6379',
44
+ },
45
+ ],
46
+ }
47
+
48
+ export const ENCRYPTION_SECRETS: SecretCategory = {
49
+ name: 'Encryption',
50
+ description: 'Master encryption keys',
51
+ secrets: [
52
+ {
53
+ envVar: 'SECRETS_MASTER_PASSWORD',
54
+ description: 'Optional master password for secrets encryption features',
55
+ required: false,
56
+ format: 'password',
57
+ minLength: 32,
58
+ rotationDays: 180,
59
+ },
60
+ {
61
+ envVar: 'SECRETS_SALT',
62
+ description: 'Salt for key derivation (base64)',
63
+ required: false,
64
+ format: 'generic',
65
+ minLength: 24,
66
+ },
67
+ ],
68
+ }
69
+
70
+ export const API_SECRETS: SecretCategory = {
71
+ name: 'API',
72
+ description: 'External API keys and tokens',
73
+ secrets: [
74
+ {
75
+ envVar: 'ANTHROPIC_API_KEY',
76
+ description: 'Anthropic API key for Claude',
77
+ required: false,
78
+ format: 'api_key',
79
+ rotationDays: 365,
80
+ validate: (value: string) => {
81
+ if (value.startsWith('sk-ant-')) {
82
+ return { valid: true }
83
+ }
84
+ return { valid: false, error: 'Must start with sk-ant-' }
85
+ },
86
+ },
87
+ {
88
+ envVar: 'OPENAI_API_KEY',
89
+ description: 'OpenAI API key',
90
+ required: false,
91
+ format: 'api_key',
92
+ rotationDays: 365,
93
+ validate: (value: string) => {
94
+ if (value.startsWith('sk-')) {
95
+ return { valid: true }
96
+ }
97
+ return { valid: false, error: 'Must start with sk-' }
98
+ },
99
+ },
100
+ ],
101
+ }
102
+
103
+ export const AUTH_SECRETS: SecretCategory = {
104
+ name: 'Authentication',
105
+ description: 'Authentication and authorization secrets',
106
+ secrets: [
107
+ {
108
+ envVar: 'JWT_SECRET',
109
+ description: 'Secret for JWT token signing',
110
+ required: false,
111
+ format: 'password',
112
+ minLength: 32,
113
+ rotationDays: 90,
114
+ },
115
+ {
116
+ envVar: 'AUTH_TOKEN',
117
+ description: 'Bearer token for optional REST API auth',
118
+ required: false,
119
+ format: 'password',
120
+ minLength: 16,
121
+ rotationDays: 90,
122
+ },
123
+ {
124
+ envVar: 'CSRF_SECRET',
125
+ description: 'Secret for CSRF token generation',
126
+ required: false,
127
+ format: 'password',
128
+ minLength: 32,
129
+ rotationDays: 90,
130
+ },
131
+ ],
132
+ }
133
+
134
+ export const SESSION_SECRETS: SecretCategory = {
135
+ name: 'Session',
136
+ description: 'Session management secrets',
137
+ secrets: [
138
+ {
139
+ envVar: 'SESSION_SECRET',
140
+ description: 'Secret for session cookie signing',
141
+ required: false,
142
+ format: 'password',
143
+ minLength: 32,
144
+ rotationDays: 90,
145
+ },
146
+ ],
147
+ }
148
+
149
+ export const ALL_SECRET_CATEGORIES: SecretCategory[] = [
150
+ DATABASE_SECRETS,
151
+ ENCRYPTION_SECRETS,
152
+ API_SECRETS,
153
+ AUTH_SECRETS,
154
+ SESSION_SECRETS,
155
+ ]
156
+
157
+ /** Rotation policy definition */
158
+ export interface RotationPolicy {
159
+ secretName: string
160
+ intervalDays: number
161
+ autoRotate: boolean
162
+ gracePeriodDays: number
163
+ notifyBeforeDays: number
164
+ }
165
+
166
+ export const ROTATION_POLICIES: RotationPolicy[] = [
167
+ {
168
+ secretName: 'SECRETS_MASTER_PASSWORD',
169
+ intervalDays: 180,
170
+ autoRotate: false, // Manual rotation required for master password
171
+ gracePeriodDays: 0,
172
+ notifyBeforeDays: 30,
173
+ },
174
+ {
175
+ secretName: 'DATABASE_URL',
176
+ intervalDays: 90,
177
+ autoRotate: false, // Manual rotation for database credentials
178
+ gracePeriodDays: 7,
179
+ notifyBeforeDays: 14,
180
+ },
181
+ {
182
+ secretName: 'JWT_SECRET',
183
+ intervalDays: 90,
184
+ autoRotate: true, // Can auto-rotate JWT secret with grace period
185
+ gracePeriodDays: 14,
186
+ notifyBeforeDays: 7,
187
+ },
188
+ {
189
+ secretName: 'CSRF_SECRET',
190
+ intervalDays: 90,
191
+ autoRotate: true,
192
+ gracePeriodDays: 7,
193
+ notifyBeforeDays: 7,
194
+ },
195
+ {
196
+ secretName: 'SESSION_SECRET',
197
+ intervalDays: 90,
198
+ autoRotate: true,
199
+ gracePeriodDays: 14,
200
+ notifyBeforeDays: 7,
201
+ },
202
+ ]
203
+
204
+ /** Get all required secrets */
205
+ export function getRequiredSecrets(): SecretDefinition[] {
206
+ return ALL_SECRET_CATEGORIES.flatMap((cat) => cat.secrets.filter((s) => s.required))
207
+ }
208
+
209
+ /** Get all optional secrets */
210
+ export function getOptionalSecrets(): SecretDefinition[] {
211
+ return ALL_SECRET_CATEGORIES.flatMap((cat) => cat.secrets.filter((s) => !s.required))
212
+ }
213
+
214
+ /** Get all secrets (required + optional) */
215
+ export function getAllSecrets(): SecretDefinition[] {
216
+ return ALL_SECRET_CATEGORIES.flatMap((cat) => cat.secrets)
217
+ }
218
+
219
+ /** Get secret definition by environment variable name */
220
+ export function getSecretDefinition(envVar: string): SecretDefinition | undefined {
221
+ for (const category of ALL_SECRET_CATEGORIES) {
222
+ const secret = category.secrets.find((s) => s.envVar === envVar)
223
+ if (secret) return secret
224
+ }
225
+ return undefined
226
+ }
227
+
228
+ /** Get rotation policy for a secret */
229
+ export function getRotationPolicy(secretName: string): RotationPolicy | undefined {
230
+ return ROTATION_POLICIES.find((p) => p.secretName === secretName)
231
+ }
232
+
233
+ /** Check if a secret is due for rotation */
234
+ export function isRotationDue(secretName: string, lastRotated: Date): boolean {
235
+ const policy = getRotationPolicy(secretName)
236
+ if (!policy) {
237
+ return false
238
+ }
239
+
240
+ const now = new Date()
241
+ const daysSinceRotation = (now.getTime() - lastRotated.getTime()) / (1000 * 60 * 60 * 24)
242
+
243
+ return daysSinceRotation >= policy.intervalDays
244
+ }
245
+
246
+ /** Check if rotation warning should be shown */
247
+ export function shouldWarnRotation(secretName: string, lastRotated: Date): boolean {
248
+ const policy = getRotationPolicy(secretName)
249
+ if (!policy) {
250
+ return false
251
+ }
252
+
253
+ const now = new Date()
254
+ const daysSinceRotation = (now.getTime() - lastRotated.getTime()) / (1000 * 60 * 60 * 24)
255
+ const daysUntilRotation = policy.intervalDays - daysSinceRotation
256
+
257
+ return daysUntilRotation <= policy.notifyBeforeDays && daysUntilRotation > 0
258
+ }
259
+
260
+ export interface EncryptionKeyConfig {
261
+ kdf: 'pbkdf2' | 'scrypt' | 'argon2'
262
+ iterations: number
263
+ keyLength: number
264
+ digest: 'sha256' | 'sha512'
265
+ }
266
+
267
+ /** Default encryption key configuration (OWASP 2023 recommendations) */
268
+ export const DEFAULT_ENCRYPTION_CONFIG: EncryptionKeyConfig = {
269
+ kdf: 'pbkdf2',
270
+ iterations: 600000, // OWASP 2023 recommendation for PBKDF2-SHA512
271
+ keyLength: 32, // 256 bits
272
+ digest: 'sha512',
273
+ }
274
+
275
+ /** Alternative encryption configs for different security levels */
276
+ export const ENCRYPTION_CONFIGS = {
277
+ standard: DEFAULT_ENCRYPTION_CONFIG,
278
+ high: {
279
+ kdf: 'pbkdf2' as const,
280
+ iterations: 1200000,
281
+ keyLength: 32,
282
+ digest: 'sha512' as const,
283
+ },
284
+ /** Performance-optimized (minimum secure iterations) */
285
+ performance: {
286
+ kdf: 'pbkdf2' as const,
287
+ iterations: 310000, // OWASP minimum for PBKDF2-SHA256
288
+ keyLength: 32,
289
+ digest: 'sha256' as const,
290
+ },
291
+ } as const
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Unified Database Client
3
+ * Automatically selects between SQLite and PostgreSQL based on DATABASE_URL
4
+ */
5
+
6
+ import {
7
+ getDatabase as getSqliteDatabase,
8
+ runMigrations as runSqliteMigrations,
9
+ closeDatabase as closeSqliteDatabase,
10
+ type DatabaseInstance as SqliteDatabaseInstance,
11
+ } from './index.js'
12
+
13
+ import {
14
+ getPostgresDatabase,
15
+ runPostgresMigrations,
16
+ closePostgresDatabase,
17
+ type PostgresDatabaseInstance,
18
+ } from './postgres.js'
19
+
20
+ export type DatabaseInstance = SqliteDatabaseInstance | PostgresDatabaseInstance
21
+
22
+ export function getDatabaseUrl(): string {
23
+ return process.env.DATABASE_URL ?? './data/supermemory.db'
24
+ }
25
+
26
+ export function isPostgresUrl(url: string): boolean {
27
+ return url.startsWith('postgresql://') || url.startsWith('postgres://')
28
+ }
29
+
30
+ function assertPostgresUrlAllowed(url: string): void {
31
+ if (process.env.NODE_ENV === 'test') {
32
+ return
33
+ }
34
+
35
+ if (!isPostgresUrl(url)) {
36
+ throw new Error(
37
+ 'DATABASE_URL must use postgres:// or postgresql:// outside tests. SQLite is only allowed when NODE_ENV=test.'
38
+ )
39
+ }
40
+ }
41
+
42
+ export function getDatabase(): DatabaseInstance {
43
+ const url = getDatabaseUrl()
44
+ const isPostgres = isPostgresUrl(url)
45
+
46
+ assertPostgresUrlAllowed(url)
47
+
48
+ if (isPostgres) {
49
+ return getPostgresDatabase(url) as DatabaseInstance
50
+ } else {
51
+ return getSqliteDatabase(url) as DatabaseInstance
52
+ }
53
+ }
54
+
55
+ export async function runMigrations(): Promise<void> {
56
+ const url = getDatabaseUrl()
57
+ const isPostgres = isPostgresUrl(url)
58
+
59
+ assertPostgresUrlAllowed(url)
60
+
61
+ if (isPostgres) {
62
+ await runPostgresMigrations(url)
63
+ } else {
64
+ runSqliteMigrations(url)
65
+ }
66
+ }
67
+
68
+ export async function closeDatabase(): Promise<void> {
69
+ const url = getDatabaseUrl()
70
+ const isPostgres = isPostgresUrl(url)
71
+
72
+ assertPostgresUrlAllowed(url)
73
+
74
+ if (isPostgres) {
75
+ await closePostgresDatabase()
76
+ } else {
77
+ closeSqliteDatabase()
78
+ }
79
+ }
80
+
81
+ export function getDatabaseInfo() {
82
+ const url = getDatabaseUrl()
83
+ const isPostgres = isPostgresUrl(url)
84
+
85
+ assertPostgresUrlAllowed(url)
86
+
87
+ return {
88
+ type: isPostgres ? 'postgresql' : 'sqlite',
89
+ url: isPostgres ? url.replace(/:[^:@]+@/, ':****@') : url, // Mask password
90
+ isProduction: isPostgres,
91
+ }
92
+ }
@@ -0,0 +1,73 @@
1
+ import Database from 'better-sqlite3'
2
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
3
+ import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
4
+ import * as schema from './schema.js'
5
+ import { existsSync, mkdirSync } from 'node:fs'
6
+ import { dirname } from 'node:path'
7
+
8
+ let db: ReturnType<typeof drizzle<typeof schema>> | null = null
9
+ let sqliteInstance: Database.Database | null = null
10
+
11
+ function isPostgresConnectionString(url: string): boolean {
12
+ const normalized = url.trim().toLowerCase()
13
+ return normalized.startsWith('postgresql://') || normalized.startsWith('postgres://')
14
+ }
15
+
16
+ function createDatabase(databaseUrl: string) {
17
+ if (isPostgresConnectionString(databaseUrl)) {
18
+ throw new Error(
19
+ 'SQLite database module received a PostgreSQL URL. Use src/db/client.ts or src/db/postgres.ts for PostgreSQL connections.'
20
+ )
21
+ }
22
+
23
+ // Ensure the directory exists
24
+ const dir = dirname(databaseUrl)
25
+ if (dir !== '.' && !existsSync(dir)) {
26
+ mkdirSync(dir, { recursive: true })
27
+ }
28
+
29
+ const sqlite = new Database(databaseUrl)
30
+ sqliteInstance = sqlite
31
+
32
+ // Enable WAL mode for better concurrent access
33
+ sqlite.pragma('journal_mode = WAL')
34
+
35
+ // Enable foreign keys
36
+ sqlite.pragma('foreign_keys = ON')
37
+
38
+ // Optimize for performance
39
+ sqlite.pragma('synchronous = NORMAL')
40
+ sqlite.pragma('cache_size = -64000') // 64MB cache
41
+ sqlite.pragma('temp_store = MEMORY')
42
+
43
+ return drizzle(sqlite, { schema })
44
+ }
45
+
46
+ export function getDatabase(databaseUrl: string) {
47
+ if (!db) {
48
+ db = createDatabase(databaseUrl)
49
+ }
50
+ return db
51
+ }
52
+
53
+ export function runMigrations(databaseUrl: string) {
54
+ const database = getDatabase(databaseUrl)
55
+ migrate(database, { migrationsFolder: './drizzle' })
56
+ console.log('Migrations completed successfully')
57
+ }
58
+
59
+ export function closeDatabase() {
60
+ if (sqliteInstance) {
61
+ sqliteInstance.close()
62
+ sqliteInstance = null
63
+ db = null
64
+ }
65
+ }
66
+
67
+ export function getSqliteInstance(): Database.Database | null {
68
+ return sqliteInstance
69
+ }
70
+
71
+ export type DatabaseInstance = ReturnType<typeof createDatabase>
72
+
73
+ export { schema }