@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,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
|
package/src/db/client.ts
ADDED
|
@@ -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
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -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 }
|