codevault 1.8.3 → 1.8.5
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/README.md +33 -5
- package/dist/chunking/file-grouper.d.ts +1 -1
- package/dist/chunking/file-grouper.d.ts.map +1 -1
- package/dist/chunking/file-grouper.js +3 -3
- package/dist/chunking/file-grouper.js.map +1 -1
- package/dist/chunking/semantic-chunker.d.ts +1 -1
- package/dist/chunking/semantic-chunker.d.ts.map +1 -1
- package/dist/chunking/token-counter.d.ts +1 -1
- package/dist/chunking/token-counter.d.ts.map +1 -1
- package/dist/chunking/token-counter.js +16 -10
- package/dist/chunking/token-counter.js.map +1 -1
- package/dist/cli/commands/ask-cmd.js +15 -15
- package/dist/cli/commands/ask-cmd.js.map +1 -1
- package/dist/cli/commands/chat-cmd.d.ts.map +1 -1
- package/dist/cli/commands/chat-cmd.js +40 -40
- package/dist/cli/commands/chat-cmd.js.map +1 -1
- package/dist/cli/commands/config-cmd.d.ts.map +1 -1
- package/dist/cli/commands/config-cmd.js +61 -52
- package/dist/cli/commands/config-cmd.js.map +1 -1
- package/dist/cli/commands/context.d.ts.map +1 -1
- package/dist/cli/commands/context.js +20 -11
- package/dist/cli/commands/context.js.map +1 -1
- package/dist/cli/commands/index-cmd.d.ts.map +1 -1
- package/dist/cli/commands/index-cmd.js +109 -85
- package/dist/cli/commands/index-cmd.js.map +1 -1
- package/dist/cli/commands/info-cmd.d.ts.map +1 -1
- package/dist/cli/commands/info-cmd.js +12 -11
- package/dist/cli/commands/info-cmd.js.map +1 -1
- package/dist/cli/commands/interactive-config.d.ts.map +1 -1
- package/dist/cli/commands/interactive-config.js +60 -20
- package/dist/cli/commands/interactive-config.js.map +1 -1
- package/dist/cli/commands/search-cmd.d.ts.map +1 -1
- package/dist/cli/commands/search-cmd.js +22 -11
- package/dist/cli/commands/search-cmd.js.map +1 -1
- package/dist/cli/commands/search-with-code-cmd.d.ts.map +1 -1
- package/dist/cli/commands/search-with-code-cmd.js +25 -16
- package/dist/cli/commands/search-with-code-cmd.js.map +1 -1
- package/dist/cli/commands/update-cmd.d.ts.map +1 -1
- package/dist/cli/commands/update-cmd.js +16 -7
- package/dist/cli/commands/update-cmd.js.map +1 -1
- package/dist/cli/commands/watch-cmd.d.ts.map +1 -1
- package/dist/cli/commands/watch-cmd.js +21 -11
- package/dist/cli/commands/watch-cmd.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils.d.ts +56 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +98 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/cli.js +0 -0
- package/dist/codemap/io.js.map +1 -1
- package/dist/config/constants.d.ts +4 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +2 -0
- package/dist/config/constants.js.map +1 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/context/packs.d.ts +2 -2
- package/dist/context/packs.d.ts.map +1 -1
- package/dist/context/packs.js +7 -4
- package/dist/context/packs.js.map +1 -1
- package/dist/core/IndexerEngine.d.ts +2 -0
- package/dist/core/IndexerEngine.d.ts.map +1 -1
- package/dist/core/IndexerEngine.js +34 -26
- package/dist/core/IndexerEngine.js.map +1 -1
- package/dist/core/SearchService.d.ts +2 -1
- package/dist/core/SearchService.d.ts.map +1 -1
- package/dist/core/SearchService.js +25 -18
- package/dist/core/SearchService.js.map +1 -1
- package/dist/core/batch-indexer.d.ts +4 -3
- package/dist/core/batch-indexer.d.ts.map +1 -1
- package/dist/core/batch-indexer.js +32 -35
- package/dist/core/batch-indexer.js.map +1 -1
- package/dist/core/indexing/FileProcessor.d.ts +1 -0
- package/dist/core/indexing/FileProcessor.d.ts.map +1 -1
- package/dist/core/indexing/FileProcessor.js +32 -9
- package/dist/core/indexing/FileProcessor.js.map +1 -1
- package/dist/core/indexing/IndexContext.d.ts +6 -4
- package/dist/core/indexing/IndexContext.d.ts.map +1 -1
- package/dist/core/indexing/IndexContext.js +3 -3
- package/dist/core/indexing/IndexContext.js.map +1 -1
- package/dist/core/indexing/IndexFinalizationStage.d.ts +6 -1
- package/dist/core/indexing/IndexFinalizationStage.d.ts.map +1 -1
- package/dist/core/indexing/IndexFinalizationStage.js +22 -3
- package/dist/core/indexing/IndexFinalizationStage.js.map +1 -1
- package/dist/core/indexing/IndexState.d.ts +3 -8
- package/dist/core/indexing/IndexState.d.ts.map +1 -1
- package/dist/core/indexing/IndexState.js.map +1 -1
- package/dist/core/indexing/PersistManager.d.ts +1 -1
- package/dist/core/indexing/PersistManager.d.ts.map +1 -1
- package/dist/core/indexing/PersistManager.js +17 -17
- package/dist/core/indexing/PersistManager.js.map +1 -1
- package/dist/core/indexing/chunk-pipeline.d.ts +33 -7
- package/dist/core/indexing/chunk-pipeline.d.ts.map +1 -1
- package/dist/core/indexing/chunk-pipeline.js +20 -8
- package/dist/core/indexing/chunk-pipeline.js.map +1 -1
- package/dist/core/metadata.d.ts +1 -1
- package/dist/core/metadata.d.ts.map +1 -1
- package/dist/core/metadata.js +1 -1
- package/dist/core/metadata.js.map +1 -1
- package/dist/core/search/CandidateRetriever.d.ts +1 -1
- package/dist/core/search/CandidateRetriever.d.ts.map +1 -1
- package/dist/core/search/CandidateRetriever.js +7 -5
- package/dist/core/search/CandidateRetriever.js.map +1 -1
- package/dist/core/search/HybridFusion.d.ts +1 -2
- package/dist/core/search/HybridFusion.d.ts.map +1 -1
- package/dist/core/search/HybridFusion.js +5 -17
- package/dist/core/search/HybridFusion.js.map +1 -1
- package/dist/core/search/ResultMapper.d.ts +0 -1
- package/dist/core/search/ResultMapper.d.ts.map +1 -1
- package/dist/core/search/ResultMapper.js +3 -14
- package/dist/core/search/ResultMapper.js.map +1 -1
- package/dist/core/search/SearchContextManager.d.ts +4 -1
- package/dist/core/search/SearchContextManager.d.ts.map +1 -1
- package/dist/core/search/SearchContextManager.js +19 -6
- package/dist/core/search/SearchContextManager.js.map +1 -1
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/search.js +9 -4
- package/dist/core/search.js.map +1 -1
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/database/db.d.ts +14 -14
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +14 -14
- package/dist/database/db.js.map +1 -1
- package/dist/indexer/merkle.js +1 -1
- package/dist/indexer/merkle.js.map +1 -1
- package/dist/languages/rules.d.ts +2 -1
- package/dist/languages/rules.d.ts.map +1 -1
- package/dist/languages/rules.js +14 -5
- package/dist/languages/rules.js.map +1 -1
- package/dist/languages/tree-sitter-loader.d.ts +24 -24
- package/dist/languages/tree-sitter-loader.d.ts.map +1 -1
- package/dist/languages/tree-sitter-loader.js +14 -10
- package/dist/languages/tree-sitter-loader.js.map +1 -1
- package/dist/mcp/handlers/context.d.ts +5 -11
- package/dist/mcp/handlers/context.d.ts.map +1 -1
- package/dist/mcp/handlers/context.js.map +1 -1
- package/dist/mcp/handlers/project.d.ts +9 -27
- package/dist/mcp/handlers/project.d.ts.map +1 -1
- package/dist/mcp/handlers/search.d.ts +24 -18
- package/dist/mcp/handlers/search.d.ts.map +1 -1
- package/dist/mcp/handlers/search.js +8 -2
- package/dist/mcp/handlers/search.js.map +1 -1
- package/dist/mcp/handlers/synthesis.d.ts +15 -11
- package/dist/mcp/handlers/synthesis.d.ts.map +1 -1
- package/dist/mcp/handlers/synthesis.js +4 -1
- package/dist/mcp/handlers/synthesis.js.map +1 -1
- package/dist/mcp/schemas.d.ts +2 -2
- package/dist/mcp/tools/ask-codebase.d.ts +10 -28
- package/dist/mcp/tools/ask-codebase.d.ts.map +1 -1
- package/dist/mcp/tools/ask-codebase.js +1 -1
- package/dist/mcp/tools/ask-codebase.js.map +1 -1
- package/dist/mcp/tools/use-context-pack.d.ts +14 -23
- package/dist/mcp/tools/use-context-pack.d.ts.map +1 -1
- package/dist/mcp/tools/use-context-pack.js +4 -3
- package/dist/mcp/tools/use-context-pack.js.map +1 -1
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +39 -24
- package/dist/mcp-server.js.map +1 -1
- package/dist/providers/base.d.ts +3 -2
- package/dist/providers/base.d.ts.map +1 -1
- package/dist/providers/base.js +3 -10
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/chat-llm.d.ts +3 -2
- package/dist/providers/chat-llm.d.ts.map +1 -1
- package/dist/providers/chat-llm.js +13 -9
- package/dist/providers/chat-llm.js.map +1 -1
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +6 -5
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +15 -19
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/token-counter.d.ts.map +1 -1
- package/dist/providers/token-counter.js +11 -3
- package/dist/providers/token-counter.js.map +1 -1
- package/dist/ranking/api-reranker.d.ts +1 -1
- package/dist/ranking/api-reranker.d.ts.map +1 -1
- package/dist/ranking/api-reranker.js +50 -13
- package/dist/ranking/api-reranker.js.map +1 -1
- package/dist/ranking/symbol-boost.d.ts.map +1 -1
- package/dist/ranking/symbol-boost.js +4 -11
- package/dist/ranking/symbol-boost.js.map +1 -1
- package/dist/search/bm25.d.ts +10 -0
- package/dist/search/bm25.d.ts.map +1 -1
- package/dist/search/bm25.js +16 -0
- package/dist/search/bm25.js.map +1 -1
- package/dist/search/hybrid.js.map +1 -1
- package/dist/search/scope.d.ts.map +1 -1
- package/dist/search/scope.js +3 -2
- package/dist/search/scope.js.map +1 -1
- package/dist/storage/encrypted-chunks.d.ts +3 -0
- package/dist/storage/encrypted-chunks.d.ts.map +1 -1
- package/dist/storage/encrypted-chunks.js +126 -47
- package/dist/storage/encrypted-chunks.js.map +1 -1
- package/dist/symbols/extract.d.ts.map +1 -1
- package/dist/symbols/extract.js +3 -2
- package/dist/symbols/extract.js.map +1 -1
- package/dist/symbols/graph.d.ts.map +1 -1
- package/dist/symbols/graph.js +14 -8
- package/dist/symbols/graph.js.map +1 -1
- package/dist/synthesis/conversational-synthesizer.d.ts +2 -1
- package/dist/synthesis/conversational-synthesizer.d.ts.map +1 -1
- package/dist/synthesis/conversational-synthesizer.js +6 -1
- package/dist/synthesis/conversational-synthesizer.js.map +1 -1
- package/dist/synthesis/markdown-formatter.d.ts.map +1 -1
- package/dist/synthesis/markdown-formatter.js +1 -1
- package/dist/synthesis/markdown-formatter.js.map +1 -1
- package/dist/synthesis/prompt-builder.d.ts.map +1 -1
- package/dist/synthesis/prompt-builder.js +42 -15
- package/dist/synthesis/prompt-builder.js.map +1 -1
- package/dist/synthesis/synthesizer.d.ts.map +1 -1
- package/dist/synthesis/synthesizer.js +23 -10
- package/dist/synthesis/synthesizer.js.map +1 -1
- package/dist/tests/api-reranker.test.d.ts +2 -0
- package/dist/tests/api-reranker.test.d.ts.map +1 -0
- package/dist/tests/api-reranker.test.js +575 -0
- package/dist/tests/api-reranker.test.js.map +1 -0
- package/dist/tests/bm25.test.d.ts +2 -0
- package/dist/tests/bm25.test.d.ts.map +1 -0
- package/dist/tests/bm25.test.js +340 -0
- package/dist/tests/bm25.test.js.map +1 -0
- package/dist/tests/chunking/file-grouper.test.d.ts +2 -0
- package/dist/tests/chunking/file-grouper.test.d.ts.map +1 -0
- package/dist/tests/chunking/file-grouper.test.js +495 -0
- package/dist/tests/chunking/file-grouper.test.js.map +1 -0
- package/dist/tests/chunking/semantic-chunker.test.d.ts +2 -0
- package/dist/tests/chunking/semantic-chunker.test.d.ts.map +1 -0
- package/dist/tests/chunking/semantic-chunker.test.js +509 -0
- package/dist/tests/chunking/semantic-chunker.test.js.map +1 -0
- package/dist/tests/chunking/token-counter.test.d.ts +2 -0
- package/dist/tests/chunking/token-counter.test.d.ts.map +1 -0
- package/dist/tests/chunking/token-counter.test.js +441 -0
- package/dist/tests/chunking/token-counter.test.js.map +1 -0
- package/dist/tests/cli/ask-cmd.test.d.ts +2 -0
- package/dist/tests/cli/ask-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/ask-cmd.test.js +152 -0
- package/dist/tests/cli/ask-cmd.test.js.map +1 -0
- package/dist/tests/cli/chat-cmd.test.d.ts +2 -0
- package/dist/tests/cli/chat-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/chat-cmd.test.js +118 -0
- package/dist/tests/cli/chat-cmd.test.js.map +1 -0
- package/dist/tests/cli/config-cmd.test.d.ts +2 -0
- package/dist/tests/cli/config-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/config-cmd.test.js +226 -0
- package/dist/tests/cli/config-cmd.test.js.map +1 -0
- package/dist/tests/cli/context.test.d.ts +2 -0
- package/dist/tests/cli/context.test.d.ts.map +1 -0
- package/dist/tests/cli/context.test.js +158 -0
- package/dist/tests/cli/context.test.js.map +1 -0
- package/dist/tests/cli/index-cmd.test.d.ts +2 -0
- package/dist/tests/cli/index-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/index-cmd.test.js +89 -0
- package/dist/tests/cli/index-cmd.test.js.map +1 -0
- package/dist/tests/cli/index.test.d.ts +2 -0
- package/dist/tests/cli/index.test.d.ts.map +1 -0
- package/dist/tests/cli/index.test.js +167 -0
- package/dist/tests/cli/index.test.js.map +1 -0
- package/dist/tests/cli/info-cmd.test.d.ts +2 -0
- package/dist/tests/cli/info-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/info-cmd.test.js +47 -0
- package/dist/tests/cli/info-cmd.test.js.map +1 -0
- package/dist/tests/cli/interactive-config.test.d.ts +2 -0
- package/dist/tests/cli/interactive-config.test.d.ts.map +1 -0
- package/dist/tests/cli/interactive-config.test.js +30 -0
- package/dist/tests/cli/interactive-config.test.js.map +1 -0
- package/dist/tests/cli/mcp-cmd.test.d.ts +2 -0
- package/dist/tests/cli/mcp-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/mcp-cmd.test.js +47 -0
- package/dist/tests/cli/mcp-cmd.test.js.map +1 -0
- package/dist/tests/cli/search-cmd.test.d.ts +2 -0
- package/dist/tests/cli/search-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/search-cmd.test.js +120 -0
- package/dist/tests/cli/search-cmd.test.js.map +1 -0
- package/dist/tests/cli/search-with-code-cmd.test.d.ts +2 -0
- package/dist/tests/cli/search-with-code-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/search-with-code-cmd.test.js +140 -0
- package/dist/tests/cli/search-with-code-cmd.test.js.map +1 -0
- package/dist/tests/cli/update-cmd.test.d.ts +2 -0
- package/dist/tests/cli/update-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/update-cmd.test.js +75 -0
- package/dist/tests/cli/update-cmd.test.js.map +1 -0
- package/dist/tests/cli/utils.test.d.ts +2 -0
- package/dist/tests/cli/utils.test.d.ts.map +1 -0
- package/dist/tests/cli/utils.test.js +119 -0
- package/dist/tests/cli/utils.test.js.map +1 -0
- package/dist/tests/cli/watch-cmd.test.d.ts +2 -0
- package/dist/tests/cli/watch-cmd.test.d.ts.map +1 -0
- package/dist/tests/cli/watch-cmd.test.js +84 -0
- package/dist/tests/cli/watch-cmd.test.js.map +1 -0
- package/dist/tests/cli-ui.test.d.ts +2 -0
- package/dist/tests/cli-ui.test.d.ts.map +1 -0
- package/dist/tests/cli-ui.test.js +608 -0
- package/dist/tests/cli-ui.test.js.map +1 -0
- package/dist/tests/codemap-io.test.d.ts +2 -0
- package/dist/tests/codemap-io.test.d.ts.map +1 -0
- package/dist/tests/codemap-io.test.js +992 -0
- package/dist/tests/codemap-io.test.js.map +1 -0
- package/dist/tests/config/apply-env.test.d.ts +2 -0
- package/dist/tests/config/apply-env.test.d.ts.map +1 -0
- package/dist/tests/config/apply-env.test.js +717 -0
- package/dist/tests/config/apply-env.test.js.map +1 -0
- package/dist/tests/config/constants.test.d.ts +2 -0
- package/dist/tests/config/constants.test.d.ts.map +1 -0
- package/dist/tests/config/constants.test.js +406 -0
- package/dist/tests/config/constants.test.js.map +1 -0
- package/dist/tests/config/loader.test.d.ts +2 -0
- package/dist/tests/config/loader.test.d.ts.map +1 -0
- package/dist/tests/config/loader.test.js +716 -0
- package/dist/tests/config/loader.test.js.map +1 -0
- package/dist/tests/config/resolver.test.d.ts +2 -0
- package/dist/tests/config/resolver.test.d.ts.map +1 -0
- package/dist/tests/config/resolver.test.js +402 -0
- package/dist/tests/config/resolver.test.js.map +1 -0
- package/dist/tests/config/types.test.d.ts +2 -0
- package/dist/tests/config/types.test.d.ts.map +1 -0
- package/dist/tests/config/types.test.js +460 -0
- package/dist/tests/config/types.test.js.map +1 -0
- package/dist/tests/context-packs.test.d.ts +2 -0
- package/dist/tests/context-packs.test.d.ts.map +1 -0
- package/dist/tests/context-packs.test.js +826 -0
- package/dist/tests/context-packs.test.js.map +1 -0
- package/dist/tests/conversational-synthesizer.test.d.ts +2 -0
- package/dist/tests/conversational-synthesizer.test.d.ts.map +1 -0
- package/dist/tests/conversational-synthesizer.test.js +595 -0
- package/dist/tests/conversational-synthesizer.test.js.map +1 -0
- package/dist/tests/database.test.d.ts +2 -0
- package/dist/tests/database.test.d.ts.map +1 -0
- package/dist/tests/database.test.js +965 -0
- package/dist/tests/database.test.js.map +1 -0
- package/dist/tests/encrypted-chunks.test.d.ts +2 -0
- package/dist/tests/encrypted-chunks.test.d.ts.map +1 -0
- package/dist/tests/encrypted-chunks.test.js +1470 -0
- package/dist/tests/encrypted-chunks.test.js.map +1 -0
- package/dist/tests/hybrid.test.d.ts +2 -0
- package/dist/tests/hybrid.test.d.ts.map +1 -0
- package/dist/tests/hybrid.test.js +456 -0
- package/dist/tests/hybrid.test.js.map +1 -0
- package/dist/tests/indexer/ChangeQueue.test.d.ts +12 -0
- package/dist/tests/indexer/ChangeQueue.test.d.ts.map +1 -0
- package/dist/tests/indexer/ChangeQueue.test.js +441 -0
- package/dist/tests/indexer/ChangeQueue.test.js.map +1 -0
- package/dist/tests/indexer/ProviderManager.test.d.ts +12 -0
- package/dist/tests/indexer/ProviderManager.test.d.ts.map +1 -0
- package/dist/tests/indexer/ProviderManager.test.js +290 -0
- package/dist/tests/indexer/ProviderManager.test.js.map +1 -0
- package/dist/tests/indexer/WatchService.test.d.ts +14 -0
- package/dist/tests/indexer/WatchService.test.d.ts.map +1 -0
- package/dist/tests/indexer/WatchService.test.js +667 -0
- package/dist/tests/indexer/WatchService.test.js.map +1 -0
- package/dist/tests/indexer/merkle.test.d.ts +11 -0
- package/dist/tests/indexer/merkle.test.d.ts.map +1 -0
- package/dist/tests/indexer/merkle.test.js +497 -0
- package/dist/tests/indexer/merkle.test.js.map +1 -0
- package/dist/tests/indexer/update.test.d.ts +10 -0
- package/dist/tests/indexer/update.test.d.ts.map +1 -0
- package/dist/tests/indexer/update.test.js +317 -0
- package/dist/tests/indexer/update.test.js.map +1 -0
- package/dist/tests/indexer/watch.test.d.ts +8 -0
- package/dist/tests/indexer/watch.test.d.ts.map +1 -0
- package/dist/tests/indexer/watch.test.js +95 -0
- package/dist/tests/indexer/watch.test.js.map +1 -0
- package/dist/tests/integration/index-search.integration.test.js +6 -4
- package/dist/tests/integration/index-search.integration.test.js.map +1 -1
- package/dist/tests/languages.test.d.ts +2 -0
- package/dist/tests/languages.test.d.ts.map +1 -0
- package/dist/tests/languages.test.js +575 -0
- package/dist/tests/languages.test.js.map +1 -0
- package/dist/tests/logger-redaction.test.d.ts +2 -0
- package/dist/tests/logger-redaction.test.d.ts.map +1 -0
- package/dist/tests/logger-redaction.test.js +48 -0
- package/dist/tests/logger-redaction.test.js.map +1 -0
- package/dist/tests/logger.test.d.ts +2 -0
- package/dist/tests/logger.test.d.ts.map +1 -0
- package/dist/tests/logger.test.js +468 -0
- package/dist/tests/logger.test.js.map +1 -0
- package/dist/tests/markdown-formatter.test.d.ts +2 -0
- package/dist/tests/markdown-formatter.test.d.ts.map +1 -0
- package/dist/tests/markdown-formatter.test.js +453 -0
- package/dist/tests/markdown-formatter.test.js.map +1 -0
- package/dist/tests/mcp/tools/use-context-pack.test.d.ts +7 -0
- package/dist/tests/mcp/tools/use-context-pack.test.d.ts.map +1 -0
- package/dist/tests/mcp/tools/use-context-pack.test.js +505 -0
- package/dist/tests/mcp/tools/use-context-pack.test.js.map +1 -0
- package/dist/tests/mutex.test.d.ts +2 -0
- package/dist/tests/mutex.test.d.ts.map +1 -0
- package/dist/tests/mutex.test.js +489 -0
- package/dist/tests/mutex.test.js.map +1 -0
- package/dist/tests/path-helpers.test.d.ts +2 -0
- package/dist/tests/path-helpers.test.d.ts.map +1 -0
- package/dist/tests/path-helpers.test.js +332 -0
- package/dist/tests/path-helpers.test.js.map +1 -0
- package/dist/tests/prompt-builder.test.d.ts +2 -0
- package/dist/tests/prompt-builder.test.d.ts.map +1 -0
- package/dist/tests/prompt-builder.test.js +417 -0
- package/dist/tests/prompt-builder.test.js.map +1 -0
- package/dist/tests/providers/base.test.d.ts +2 -0
- package/dist/tests/providers/base.test.d.ts.map +1 -0
- package/dist/tests/providers/base.test.js +299 -0
- package/dist/tests/providers/base.test.js.map +1 -0
- package/dist/tests/providers/chat-llm.test.d.ts +2 -0
- package/dist/tests/providers/chat-llm.test.d.ts.map +1 -0
- package/dist/tests/providers/chat-llm.test.js +435 -0
- package/dist/tests/providers/chat-llm.test.js.map +1 -0
- package/dist/tests/providers/index.test.d.ts +2 -0
- package/dist/tests/providers/index.test.d.ts.map +1 -0
- package/dist/tests/providers/index.test.js +204 -0
- package/dist/tests/providers/index.test.js.map +1 -0
- package/dist/tests/providers/mock.test.d.ts +2 -0
- package/dist/tests/providers/mock.test.d.ts.map +1 -0
- package/dist/tests/providers/mock.test.js +225 -0
- package/dist/tests/providers/mock.test.js.map +1 -0
- package/dist/tests/providers/openai.test.d.ts +2 -0
- package/dist/tests/providers/openai.test.d.ts.map +1 -0
- package/dist/tests/providers/openai.test.js +408 -0
- package/dist/tests/providers/openai.test.js.map +1 -0
- package/dist/tests/providers/token-counter.test.d.ts +2 -0
- package/dist/tests/providers/token-counter.test.d.ts.map +1 -0
- package/dist/tests/providers/token-counter.test.js +247 -0
- package/dist/tests/providers/token-counter.test.js.map +1 -0
- package/dist/tests/rate-limiter.test.js +392 -1
- package/dist/tests/rate-limiter.test.js.map +1 -1
- package/dist/tests/scope.test.d.ts +2 -0
- package/dist/tests/scope.test.d.ts.map +1 -0
- package/dist/tests/scope.test.js +529 -0
- package/dist/tests/scope.test.js.map +1 -0
- package/dist/tests/search-normalization.test.js.map +1 -1
- package/dist/tests/semantic-chunker.test.js.map +1 -1
- package/dist/tests/simple-lru.test.js +377 -0
- package/dist/tests/simple-lru.test.js.map +1 -1
- package/dist/tests/symbol-boost.test.js +730 -10
- package/dist/tests/symbol-boost.test.js.map +1 -1
- package/dist/tests/symbols-extract.test.d.ts +2 -0
- package/dist/tests/symbols-extract.test.d.ts.map +1 -0
- package/dist/tests/symbols-extract.test.js +536 -0
- package/dist/tests/symbols-extract.test.js.map +1 -0
- package/dist/tests/symbols-graph.test.d.ts +2 -0
- package/dist/tests/symbols-graph.test.d.ts.map +1 -0
- package/dist/tests/symbols-graph.test.js +656 -0
- package/dist/tests/symbols-graph.test.js.map +1 -0
- package/dist/tests/synthesizer.test.d.ts +2 -0
- package/dist/tests/synthesizer.test.d.ts.map +1 -0
- package/dist/tests/synthesizer.test.js +381 -0
- package/dist/tests/synthesizer.test.js.map +1 -0
- package/dist/types/codemap.d.ts +2 -2
- package/dist/types/codemap.d.ts.map +1 -1
- package/dist/types/codemap.js +17 -9
- package/dist/types/codemap.js.map +1 -1
- package/dist/types/context-pack.d.ts +5 -5
- package/dist/types/context-pack.d.ts.map +1 -1
- package/dist/types/context-pack.js +6 -3
- package/dist/types/context-pack.js.map +1 -1
- package/dist/utils/cli-ui.d.ts +1 -1
- package/dist/utils/cli-ui.d.ts.map +1 -1
- package/dist/utils/cli-ui.js +26 -26
- package/dist/utils/cli-ui.js.map +1 -1
- package/dist/utils/indexer-with-progress.d.ts.map +1 -1
- package/dist/utils/indexer-with-progress.js +0 -6
- package/dist/utils/indexer-with-progress.js.map +1 -1
- package/dist/utils/logger.d.ts +10 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +158 -6
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/mutex.d.ts +7 -2
- package/dist/utils/mutex.d.ts.map +1 -1
- package/dist/utils/mutex.js +35 -7
- package/dist/utils/mutex.js.map +1 -1
- package/dist/utils/path-helpers.d.ts.map +1 -1
- package/dist/utils/path-helpers.js +5 -2
- package/dist/utils/path-helpers.js.map +1 -1
- package/dist/utils/rate-limiter.d.ts.map +1 -1
- package/dist/utils/rate-limiter.js +23 -4
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/simple-lru.d.ts +6 -0
- package/dist/utils/simple-lru.d.ts.map +1 -1
- package/dist/utils/simple-lru.js +26 -0
- package/dist/utils/simple-lru.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { gzipSync } from 'zlib';
|
|
7
|
+
import { getActiveEncryptionKey, getEncryptionKeyError, getEncryptionKeySet, isChunkEncryptedOnDisk, readChunkFromDisk, removeChunkArtifacts, resetEncryptionCacheForTests, resetEncryptionGuardsForTests, resolveEncryptionPreference, setEncryptionRandomBytes, writeChunkToDisk } from '../storage/encrypted-chunks.js';
|
|
8
|
+
import { ENCRYPTION_CONSTANTS } from '../config/constants.js';
|
|
9
|
+
const { REQUIRED_KEY_LENGTH, SALT_LENGTH, IV_LENGTH, MAGIC_HEADER, HKDF_INFO, KEY_ID_LENGTH, TAG_LENGTH } = ENCRYPTION_CONSTANTS;
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Test Utilities
|
|
12
|
+
// ============================================================================
|
|
13
|
+
function generateValidKey() {
|
|
14
|
+
return crypto.randomBytes(REQUIRED_KEY_LENGTH);
|
|
15
|
+
}
|
|
16
|
+
function generateBase64Key() {
|
|
17
|
+
return crypto.randomBytes(REQUIRED_KEY_LENGTH).toString('base64');
|
|
18
|
+
}
|
|
19
|
+
function generateHexKey() {
|
|
20
|
+
return crypto.randomBytes(REQUIRED_KEY_LENGTH).toString('hex');
|
|
21
|
+
}
|
|
22
|
+
async function createTempDir(prefix) {
|
|
23
|
+
return fs.mkdtemp(path.join(process.cwd(), `tmp-${prefix}-`));
|
|
24
|
+
}
|
|
25
|
+
async function cleanupTempDir(dirPath) {
|
|
26
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Key Management Tests
|
|
30
|
+
// ============================================================================
|
|
31
|
+
test('getActiveEncryptionKey returns null when no env var is set', () => {
|
|
32
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
33
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
34
|
+
resetEncryptionCacheForTests();
|
|
35
|
+
try {
|
|
36
|
+
const key = getActiveEncryptionKey();
|
|
37
|
+
assert.equal(key, null);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (originalKey !== undefined) {
|
|
41
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
42
|
+
}
|
|
43
|
+
resetEncryptionCacheForTests();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
test('getActiveEncryptionKey decodes base64 key correctly', () => {
|
|
47
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
48
|
+
const testKey = generateBase64Key();
|
|
49
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = testKey;
|
|
50
|
+
resetEncryptionCacheForTests();
|
|
51
|
+
try {
|
|
52
|
+
const key = getActiveEncryptionKey();
|
|
53
|
+
assert.ok(key);
|
|
54
|
+
assert.equal(key.length, REQUIRED_KEY_LENGTH);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (originalKey !== undefined) {
|
|
58
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
62
|
+
}
|
|
63
|
+
resetEncryptionCacheForTests();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
test('getActiveEncryptionKey decodes hex key correctly', () => {
|
|
67
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
68
|
+
const testKey = generateHexKey();
|
|
69
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = testKey;
|
|
70
|
+
resetEncryptionCacheForTests();
|
|
71
|
+
try {
|
|
72
|
+
const key = getActiveEncryptionKey();
|
|
73
|
+
assert.ok(key);
|
|
74
|
+
assert.equal(key.length, REQUIRED_KEY_LENGTH);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (originalKey !== undefined) {
|
|
78
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
82
|
+
}
|
|
83
|
+
resetEncryptionCacheForTests();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test('getActiveEncryptionKey returns null for invalid key format', () => {
|
|
87
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
88
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = 'invalid-key-too-short';
|
|
89
|
+
resetEncryptionCacheForTests();
|
|
90
|
+
try {
|
|
91
|
+
const key = getActiveEncryptionKey();
|
|
92
|
+
assert.equal(key, null);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (originalKey !== undefined) {
|
|
96
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
100
|
+
}
|
|
101
|
+
resetEncryptionCacheForTests();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
test('getEncryptionKeyError returns error for invalid key', () => {
|
|
105
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
106
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = 'invalid-key-format';
|
|
107
|
+
resetEncryptionCacheForTests();
|
|
108
|
+
try {
|
|
109
|
+
const error = getEncryptionKeyError();
|
|
110
|
+
assert.ok(error);
|
|
111
|
+
assert.ok(error.message.includes('CODEVAULT_ENCRYPTION_KEY'));
|
|
112
|
+
assert.ok(error.message.includes(`${REQUIRED_KEY_LENGTH}-byte`));
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (originalKey !== undefined) {
|
|
116
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
120
|
+
}
|
|
121
|
+
resetEncryptionCacheForTests();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
test('getEncryptionKeyError returns null for valid key', () => {
|
|
125
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
126
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = generateBase64Key();
|
|
127
|
+
resetEncryptionCacheForTests();
|
|
128
|
+
try {
|
|
129
|
+
const error = getEncryptionKeyError();
|
|
130
|
+
assert.equal(error, null);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (originalKey !== undefined) {
|
|
134
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
138
|
+
}
|
|
139
|
+
resetEncryptionCacheForTests();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
test('getEncryptionKeyError returns null when no key is set', () => {
|
|
143
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
144
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
145
|
+
resetEncryptionCacheForTests();
|
|
146
|
+
try {
|
|
147
|
+
const error = getEncryptionKeyError();
|
|
148
|
+
assert.equal(error, null);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (originalKey !== undefined) {
|
|
152
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
153
|
+
}
|
|
154
|
+
resetEncryptionCacheForTests();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
test('getEncryptionKeySet returns primary and deprecated keys', () => {
|
|
158
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
159
|
+
const originalDeprecated = process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS;
|
|
160
|
+
const primaryKey = generateBase64Key();
|
|
161
|
+
const deprecatedKey1 = generateBase64Key();
|
|
162
|
+
const deprecatedKey2 = generateHexKey();
|
|
163
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = primaryKey;
|
|
164
|
+
process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS = `${deprecatedKey1},${deprecatedKey2}`;
|
|
165
|
+
resetEncryptionCacheForTests();
|
|
166
|
+
try {
|
|
167
|
+
const keySet = getEncryptionKeySet();
|
|
168
|
+
assert.ok(keySet.primary);
|
|
169
|
+
assert.equal(keySet.primary.length, REQUIRED_KEY_LENGTH);
|
|
170
|
+
assert.equal(keySet.deprecated.length, 2);
|
|
171
|
+
assert.equal(keySet.deprecated[0].length, REQUIRED_KEY_LENGTH);
|
|
172
|
+
assert.equal(keySet.deprecated[1].length, REQUIRED_KEY_LENGTH);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (originalKey !== undefined) {
|
|
176
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
180
|
+
}
|
|
181
|
+
if (originalDeprecated !== undefined) {
|
|
182
|
+
process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS = originalDeprecated;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
delete process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS;
|
|
186
|
+
}
|
|
187
|
+
resetEncryptionCacheForTests();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
test('getEncryptionKeySet skips invalid deprecated keys', () => {
|
|
191
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
192
|
+
const originalDeprecated = process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS;
|
|
193
|
+
const primaryKey = generateBase64Key();
|
|
194
|
+
const validDeprecated = generateBase64Key();
|
|
195
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = primaryKey;
|
|
196
|
+
process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS = `${validDeprecated},invalid-key,${generateBase64Key()}`;
|
|
197
|
+
resetEncryptionCacheForTests();
|
|
198
|
+
try {
|
|
199
|
+
const keySet = getEncryptionKeySet();
|
|
200
|
+
assert.ok(keySet.primary);
|
|
201
|
+
assert.equal(keySet.deprecated.length, 2); // Only valid keys
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
if (originalKey !== undefined) {
|
|
205
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
209
|
+
}
|
|
210
|
+
if (originalDeprecated !== undefined) {
|
|
211
|
+
process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS = originalDeprecated;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
delete process.env.CODEVAULT_ENCRYPTION_DEPRECATED_KEYS;
|
|
215
|
+
}
|
|
216
|
+
resetEncryptionCacheForTests();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
test('key decoding handles whitespace in keys', () => {
|
|
220
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
221
|
+
const testKey = ` ${generateBase64Key()} `;
|
|
222
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = testKey;
|
|
223
|
+
resetEncryptionCacheForTests();
|
|
224
|
+
try {
|
|
225
|
+
const key = getActiveEncryptionKey();
|
|
226
|
+
assert.ok(key);
|
|
227
|
+
assert.equal(key.length, REQUIRED_KEY_LENGTH);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (originalKey !== undefined) {
|
|
231
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
235
|
+
}
|
|
236
|
+
resetEncryptionCacheForTests();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
test('key decoding returns null for empty string', () => {
|
|
240
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
241
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = '';
|
|
242
|
+
resetEncryptionCacheForTests();
|
|
243
|
+
try {
|
|
244
|
+
const key = getActiveEncryptionKey();
|
|
245
|
+
assert.equal(key, null);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (originalKey !== undefined) {
|
|
249
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
253
|
+
}
|
|
254
|
+
resetEncryptionCacheForTests();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
test('key decoding returns null for whitespace-only string', () => {
|
|
258
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
259
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = ' ';
|
|
260
|
+
resetEncryptionCacheForTests();
|
|
261
|
+
try {
|
|
262
|
+
const key = getActiveEncryptionKey();
|
|
263
|
+
assert.equal(key, null);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (originalKey !== undefined) {
|
|
267
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
271
|
+
}
|
|
272
|
+
resetEncryptionCacheForTests();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Encryption Preference Resolution Tests
|
|
277
|
+
// ============================================================================
|
|
278
|
+
test('resolveEncryptionPreference returns disabled when mode is off', () => {
|
|
279
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
280
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = generateBase64Key();
|
|
281
|
+
resetEncryptionCacheForTests();
|
|
282
|
+
try {
|
|
283
|
+
const pref = resolveEncryptionPreference({ mode: 'off' });
|
|
284
|
+
assert.equal(pref.enabled, false);
|
|
285
|
+
assert.equal(pref.key, null);
|
|
286
|
+
assert.equal(pref.reason, 'flag_off');
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
if (originalKey !== undefined) {
|
|
290
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
294
|
+
}
|
|
295
|
+
resetEncryptionCacheForTests();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
test('resolveEncryptionPreference returns enabled when mode is on with valid key', () => {
|
|
299
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
300
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = generateBase64Key();
|
|
301
|
+
resetEncryptionCacheForTests();
|
|
302
|
+
try {
|
|
303
|
+
const pref = resolveEncryptionPreference({ mode: 'on' });
|
|
304
|
+
assert.equal(pref.enabled, true);
|
|
305
|
+
assert.ok(pref.key);
|
|
306
|
+
assert.equal(pref.reason, 'enabled');
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
if (originalKey !== undefined) {
|
|
310
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
314
|
+
}
|
|
315
|
+
resetEncryptionCacheForTests();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
test('resolveEncryptionPreference throws when mode is on but no key', () => {
|
|
319
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
320
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
321
|
+
resetEncryptionCacheForTests();
|
|
322
|
+
try {
|
|
323
|
+
assert.throws(() => resolveEncryptionPreference({ mode: 'on' }), (error) => {
|
|
324
|
+
return error.message.includes('CODEVAULT_ENCRYPTION_KEY is not configured');
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
if (originalKey !== undefined) {
|
|
329
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
330
|
+
}
|
|
331
|
+
resetEncryptionCacheForTests();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
test('resolveEncryptionPreference throws when mode is on but key is invalid', () => {
|
|
335
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
336
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = 'invalid-key';
|
|
337
|
+
resetEncryptionCacheForTests();
|
|
338
|
+
try {
|
|
339
|
+
assert.throws(() => resolveEncryptionPreference({ mode: 'on' }), (error) => {
|
|
340
|
+
return error.message.includes('CODEVAULT_ENCRYPTION_KEY');
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
if (originalKey !== undefined) {
|
|
345
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
349
|
+
}
|
|
350
|
+
resetEncryptionCacheForTests();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
test('resolveEncryptionPreference returns disabled with missing_key reason when no key set', () => {
|
|
354
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
355
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
356
|
+
resetEncryptionCacheForTests();
|
|
357
|
+
try {
|
|
358
|
+
const pref = resolveEncryptionPreference({});
|
|
359
|
+
assert.equal(pref.enabled, false);
|
|
360
|
+
assert.equal(pref.key, null);
|
|
361
|
+
assert.equal(pref.reason, 'missing_key');
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (originalKey !== undefined) {
|
|
365
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
366
|
+
}
|
|
367
|
+
resetEncryptionCacheForTests();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
test('resolveEncryptionPreference returns disabled with invalid_key reason for bad key', () => {
|
|
371
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
372
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = 'bad-key-format';
|
|
373
|
+
resetEncryptionCacheForTests();
|
|
374
|
+
const warnings = [];
|
|
375
|
+
const mockLogger = {
|
|
376
|
+
warn: (msg) => warnings.push(msg)
|
|
377
|
+
};
|
|
378
|
+
try {
|
|
379
|
+
const pref = resolveEncryptionPreference({ logger: mockLogger });
|
|
380
|
+
assert.equal(pref.enabled, false);
|
|
381
|
+
assert.equal(pref.key, null);
|
|
382
|
+
assert.equal(pref.reason, 'invalid_key');
|
|
383
|
+
assert.ok(warnings.length > 0);
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
if (originalKey !== undefined) {
|
|
387
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
391
|
+
}
|
|
392
|
+
resetEncryptionCacheForTests();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
test('resolveEncryptionPreference warns for invalid mode values', () => {
|
|
396
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
397
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
398
|
+
resetEncryptionCacheForTests();
|
|
399
|
+
const warnings = [];
|
|
400
|
+
const mockLogger = {
|
|
401
|
+
warn: (msg) => warnings.push(msg)
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
resolveEncryptionPreference({ mode: 'invalid-mode', logger: mockLogger });
|
|
405
|
+
assert.ok(warnings.some(w => w.includes('Unknown --encrypt mode')));
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
if (originalKey !== undefined) {
|
|
409
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
410
|
+
}
|
|
411
|
+
resetEncryptionCacheForTests();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
test('resolveEncryptionPreference handles case-insensitive mode values', () => {
|
|
415
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
416
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = generateBase64Key();
|
|
417
|
+
resetEncryptionCacheForTests();
|
|
418
|
+
try {
|
|
419
|
+
const prefOff = resolveEncryptionPreference({ mode: 'OFF' });
|
|
420
|
+
assert.equal(prefOff.enabled, false);
|
|
421
|
+
assert.equal(prefOff.reason, 'flag_off');
|
|
422
|
+
const prefOn = resolveEncryptionPreference({ mode: 'ON' });
|
|
423
|
+
assert.equal(prefOn.enabled, true);
|
|
424
|
+
assert.equal(prefOn.reason, 'enabled');
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
if (originalKey !== undefined) {
|
|
428
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
432
|
+
}
|
|
433
|
+
resetEncryptionCacheForTests();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
test('resolveEncryptionPreference handles whitespace in mode values', () => {
|
|
437
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
438
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
439
|
+
resetEncryptionCacheForTests();
|
|
440
|
+
try {
|
|
441
|
+
const pref = resolveEncryptionPreference({ mode: ' off ' });
|
|
442
|
+
assert.equal(pref.enabled, false);
|
|
443
|
+
assert.equal(pref.reason, 'flag_off');
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
if (originalKey !== undefined) {
|
|
447
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
448
|
+
}
|
|
449
|
+
resetEncryptionCacheForTests();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
// ============================================================================
|
|
453
|
+
// Encryption/Decryption Roundtrip Tests
|
|
454
|
+
// ============================================================================
|
|
455
|
+
test('writeChunkToDisk and readChunkFromDisk roundtrip with encryption', async () => {
|
|
456
|
+
resetEncryptionCacheForTests();
|
|
457
|
+
resetEncryptionGuardsForTests();
|
|
458
|
+
const chunkDir = await createTempDir('roundtrip-enc');
|
|
459
|
+
const sha = 'roundtrip-sha-1';
|
|
460
|
+
const key = generateValidKey();
|
|
461
|
+
const testCode = 'function hello() { return "world"; }';
|
|
462
|
+
try {
|
|
463
|
+
const writeResult = await writeChunkToDisk({
|
|
464
|
+
chunkDir,
|
|
465
|
+
sha,
|
|
466
|
+
code: testCode,
|
|
467
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
468
|
+
});
|
|
469
|
+
assert.equal(writeResult.encrypted, true);
|
|
470
|
+
assert.ok(writeResult.path.endsWith('.gz.enc'));
|
|
471
|
+
const readResult = await readChunkFromDisk({
|
|
472
|
+
chunkDir,
|
|
473
|
+
sha,
|
|
474
|
+
keySet: { primary: key, deprecated: [] }
|
|
475
|
+
});
|
|
476
|
+
assert.ok(readResult);
|
|
477
|
+
assert.equal(readResult.code, testCode);
|
|
478
|
+
assert.equal(readResult.encrypted, true);
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
await cleanupTempDir(chunkDir);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
test('writeChunkToDisk and readChunkFromDisk roundtrip without encryption', async () => {
|
|
485
|
+
resetEncryptionCacheForTests();
|
|
486
|
+
resetEncryptionGuardsForTests();
|
|
487
|
+
const chunkDir = await createTempDir('roundtrip-plain');
|
|
488
|
+
const sha = 'roundtrip-sha-2';
|
|
489
|
+
const testCode = 'const x = 42;';
|
|
490
|
+
try {
|
|
491
|
+
const writeResult = await writeChunkToDisk({
|
|
492
|
+
chunkDir,
|
|
493
|
+
sha,
|
|
494
|
+
code: testCode,
|
|
495
|
+
encryption: { enabled: false, key: null, reason: 'test' }
|
|
496
|
+
});
|
|
497
|
+
assert.equal(writeResult.encrypted, false);
|
|
498
|
+
assert.ok(writeResult.path.endsWith('.gz'));
|
|
499
|
+
assert.ok(!writeResult.path.endsWith('.gz.enc'));
|
|
500
|
+
const readResult = await readChunkFromDisk({
|
|
501
|
+
chunkDir,
|
|
502
|
+
sha,
|
|
503
|
+
keySet: { primary: null, deprecated: [] }
|
|
504
|
+
});
|
|
505
|
+
assert.ok(readResult);
|
|
506
|
+
assert.equal(readResult.code, testCode);
|
|
507
|
+
assert.equal(readResult.encrypted, false);
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
await cleanupTempDir(chunkDir);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
test('writeChunkToDisk accepts Buffer input', async () => {
|
|
514
|
+
resetEncryptionCacheForTests();
|
|
515
|
+
resetEncryptionGuardsForTests();
|
|
516
|
+
const chunkDir = await createTempDir('buffer-input');
|
|
517
|
+
const sha = 'buffer-sha-1';
|
|
518
|
+
const key = generateValidKey();
|
|
519
|
+
const testCode = Buffer.from('buffer content test');
|
|
520
|
+
try {
|
|
521
|
+
const writeResult = await writeChunkToDisk({
|
|
522
|
+
chunkDir,
|
|
523
|
+
sha,
|
|
524
|
+
code: testCode,
|
|
525
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
526
|
+
});
|
|
527
|
+
assert.equal(writeResult.encrypted, true);
|
|
528
|
+
const readResult = await readChunkFromDisk({
|
|
529
|
+
chunkDir,
|
|
530
|
+
sha,
|
|
531
|
+
keySet: { primary: key, deprecated: [] }
|
|
532
|
+
});
|
|
533
|
+
assert.ok(readResult);
|
|
534
|
+
assert.equal(readResult.code, 'buffer content test');
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
await cleanupTempDir(chunkDir);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
test('writeChunkToDisk handles empty string', async () => {
|
|
541
|
+
resetEncryptionCacheForTests();
|
|
542
|
+
resetEncryptionGuardsForTests();
|
|
543
|
+
const chunkDir = await createTempDir('empty-string');
|
|
544
|
+
const sha = 'empty-sha-1';
|
|
545
|
+
const key = generateValidKey();
|
|
546
|
+
try {
|
|
547
|
+
const writeResult = await writeChunkToDisk({
|
|
548
|
+
chunkDir,
|
|
549
|
+
sha,
|
|
550
|
+
code: '',
|
|
551
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
552
|
+
});
|
|
553
|
+
assert.equal(writeResult.encrypted, true);
|
|
554
|
+
const readResult = await readChunkFromDisk({
|
|
555
|
+
chunkDir,
|
|
556
|
+
sha,
|
|
557
|
+
keySet: { primary: key, deprecated: [] }
|
|
558
|
+
});
|
|
559
|
+
assert.ok(readResult);
|
|
560
|
+
assert.equal(readResult.code, '');
|
|
561
|
+
assert.equal(readResult.encrypted, true);
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
await cleanupTempDir(chunkDir);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
test('writeChunkToDisk handles large content', async () => {
|
|
568
|
+
resetEncryptionCacheForTests();
|
|
569
|
+
resetEncryptionGuardsForTests();
|
|
570
|
+
const chunkDir = await createTempDir('large-content');
|
|
571
|
+
const sha = 'large-sha-1';
|
|
572
|
+
const key = generateValidKey();
|
|
573
|
+
const largeContent = 'x'.repeat(100000); // 100KB of content
|
|
574
|
+
try {
|
|
575
|
+
const writeResult = await writeChunkToDisk({
|
|
576
|
+
chunkDir,
|
|
577
|
+
sha,
|
|
578
|
+
code: largeContent,
|
|
579
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
580
|
+
});
|
|
581
|
+
assert.equal(writeResult.encrypted, true);
|
|
582
|
+
const readResult = await readChunkFromDisk({
|
|
583
|
+
chunkDir,
|
|
584
|
+
sha,
|
|
585
|
+
keySet: { primary: key, deprecated: [] }
|
|
586
|
+
});
|
|
587
|
+
assert.ok(readResult);
|
|
588
|
+
assert.equal(readResult.code, largeContent);
|
|
589
|
+
assert.equal(readResult.encrypted, true);
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
await cleanupTempDir(chunkDir);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
test('writeChunkToDisk handles unicode content', async () => {
|
|
596
|
+
resetEncryptionCacheForTests();
|
|
597
|
+
resetEncryptionGuardsForTests();
|
|
598
|
+
const chunkDir = await createTempDir('unicode');
|
|
599
|
+
const sha = 'unicode-sha-1';
|
|
600
|
+
const key = generateValidKey();
|
|
601
|
+
const unicodeContent = 'Hello World! Bonjour le monde! Hallo Welt!';
|
|
602
|
+
try {
|
|
603
|
+
const writeResult = await writeChunkToDisk({
|
|
604
|
+
chunkDir,
|
|
605
|
+
sha,
|
|
606
|
+
code: unicodeContent,
|
|
607
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
608
|
+
});
|
|
609
|
+
assert.equal(writeResult.encrypted, true);
|
|
610
|
+
const readResult = await readChunkFromDisk({
|
|
611
|
+
chunkDir,
|
|
612
|
+
sha,
|
|
613
|
+
keySet: { primary: key, deprecated: [] }
|
|
614
|
+
});
|
|
615
|
+
assert.ok(readResult);
|
|
616
|
+
assert.equal(readResult.code, unicodeContent);
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
await cleanupTempDir(chunkDir);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
// ============================================================================
|
|
623
|
+
// Error Case Tests
|
|
624
|
+
// ============================================================================
|
|
625
|
+
test('readChunkFromDisk fails with wrong key', async () => {
|
|
626
|
+
resetEncryptionCacheForTests();
|
|
627
|
+
resetEncryptionGuardsForTests();
|
|
628
|
+
const chunkDir = await createTempDir('wrong-key');
|
|
629
|
+
const sha = 'wrong-key-sha';
|
|
630
|
+
const correctKey = generateValidKey();
|
|
631
|
+
const wrongKey = generateValidKey();
|
|
632
|
+
try {
|
|
633
|
+
await writeChunkToDisk({
|
|
634
|
+
chunkDir,
|
|
635
|
+
sha,
|
|
636
|
+
code: 'secret content',
|
|
637
|
+
encryption: { enabled: true, key: correctKey, reason: 'test' }
|
|
638
|
+
});
|
|
639
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
640
|
+
chunkDir,
|
|
641
|
+
sha,
|
|
642
|
+
keySet: { primary: wrongKey, deprecated: [] }
|
|
643
|
+
}), (error) => {
|
|
644
|
+
const typed = error;
|
|
645
|
+
// May return ENCRYPTION_KEY_NOT_FOUND when key ID in payload doesn't match any configured key
|
|
646
|
+
return typed.code === 'ENCRYPTION_AUTH_FAILED' ||
|
|
647
|
+
typed.code === 'ENCRYPTION_KEY_ID_MISMATCH' ||
|
|
648
|
+
typed.code === 'ENCRYPTION_KEY_NOT_FOUND';
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
await cleanupTempDir(chunkDir);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
test('readChunkFromDisk fails when encrypted chunk has no key', async () => {
|
|
656
|
+
resetEncryptionCacheForTests();
|
|
657
|
+
resetEncryptionGuardsForTests();
|
|
658
|
+
const chunkDir = await createTempDir('no-key');
|
|
659
|
+
const sha = 'no-key-sha';
|
|
660
|
+
const key = generateValidKey();
|
|
661
|
+
try {
|
|
662
|
+
await writeChunkToDisk({
|
|
663
|
+
chunkDir,
|
|
664
|
+
sha,
|
|
665
|
+
code: 'encrypted content',
|
|
666
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
667
|
+
});
|
|
668
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
669
|
+
chunkDir,
|
|
670
|
+
sha,
|
|
671
|
+
keySet: { primary: null, deprecated: [] }
|
|
672
|
+
}), (error) => {
|
|
673
|
+
const typed = error;
|
|
674
|
+
return typed.code === 'ENCRYPTION_KEY_REQUIRED';
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
await cleanupTempDir(chunkDir);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
test('readChunkFromDisk fails with truncated payload', async () => {
|
|
682
|
+
resetEncryptionCacheForTests();
|
|
683
|
+
resetEncryptionGuardsForTests();
|
|
684
|
+
const chunkDir = await createTempDir('truncated');
|
|
685
|
+
const sha = 'truncated-sha';
|
|
686
|
+
const key = generateValidKey();
|
|
687
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
688
|
+
const magicHeader = Buffer.from(MAGIC_HEADER, 'utf8');
|
|
689
|
+
try {
|
|
690
|
+
await fs.mkdir(chunkDir, { recursive: true });
|
|
691
|
+
// Write a truncated payload (just header + version + partial key id)
|
|
692
|
+
await fs.writeFile(encryptedPath, Buffer.concat([magicHeader, Buffer.from([2]), Buffer.alloc(4)]));
|
|
693
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
694
|
+
chunkDir,
|
|
695
|
+
sha,
|
|
696
|
+
keySet: { primary: key, deprecated: [] }
|
|
697
|
+
}), (error) => {
|
|
698
|
+
const typed = error;
|
|
699
|
+
return typed.code === 'ENCRYPTION_PAYLOAD_INVALID' || typed.code === 'ENCRYPTION_AUTH_FAILED';
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
await cleanupTempDir(chunkDir);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
test('readChunkFromDisk fails with corrupted ciphertext', async () => {
|
|
707
|
+
resetEncryptionCacheForTests();
|
|
708
|
+
resetEncryptionGuardsForTests();
|
|
709
|
+
const chunkDir = await createTempDir('corrupted');
|
|
710
|
+
const sha = 'corrupted-sha';
|
|
711
|
+
const key = generateValidKey();
|
|
712
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
713
|
+
try {
|
|
714
|
+
await writeChunkToDisk({
|
|
715
|
+
chunkDir,
|
|
716
|
+
sha,
|
|
717
|
+
code: 'original content',
|
|
718
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
719
|
+
});
|
|
720
|
+
// Corrupt the payload by modifying bytes
|
|
721
|
+
const payload = await fs.readFile(encryptedPath);
|
|
722
|
+
const corruptedPayload = Buffer.from(payload);
|
|
723
|
+
// Corrupt ciphertext area (after header + version + keyId + salt + iv)
|
|
724
|
+
const corruptionOffset = MAGIC_HEADER.length + 1 + KEY_ID_LENGTH + SALT_LENGTH + IV_LENGTH + 5;
|
|
725
|
+
if (corruptedPayload.length > corruptionOffset) {
|
|
726
|
+
corruptedPayload[corruptionOffset] ^= 0xFF;
|
|
727
|
+
}
|
|
728
|
+
await fs.writeFile(encryptedPath, corruptedPayload);
|
|
729
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
730
|
+
chunkDir,
|
|
731
|
+
sha,
|
|
732
|
+
keySet: { primary: key, deprecated: [] }
|
|
733
|
+
}), (error) => {
|
|
734
|
+
const typed = error;
|
|
735
|
+
return typed.code === 'ENCRYPTION_AUTH_FAILED';
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
await cleanupTempDir(chunkDir);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
test('readChunkFromDisk fails with unrecognized header', async () => {
|
|
743
|
+
resetEncryptionCacheForTests();
|
|
744
|
+
resetEncryptionGuardsForTests();
|
|
745
|
+
const chunkDir = await createTempDir('bad-header');
|
|
746
|
+
const sha = 'bad-header-sha';
|
|
747
|
+
const key = generateValidKey();
|
|
748
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
749
|
+
try {
|
|
750
|
+
await fs.mkdir(chunkDir, { recursive: true });
|
|
751
|
+
// Write a file with unrecognized header
|
|
752
|
+
await fs.writeFile(encryptedPath, Buffer.from('BADHEADER' + 'x'.repeat(100)));
|
|
753
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
754
|
+
chunkDir,
|
|
755
|
+
sha,
|
|
756
|
+
keySet: { primary: key, deprecated: [] }
|
|
757
|
+
}), (error) => {
|
|
758
|
+
const typed = error;
|
|
759
|
+
return typed.code === 'ENCRYPTION_FORMAT_UNRECOGNIZED';
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
await cleanupTempDir(chunkDir);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
test('readChunkFromDisk fails with unsupported version', async () => {
|
|
767
|
+
resetEncryptionCacheForTests();
|
|
768
|
+
resetEncryptionGuardsForTests();
|
|
769
|
+
const chunkDir = await createTempDir('future-version');
|
|
770
|
+
const sha = 'future-version-sha';
|
|
771
|
+
const key = generateValidKey();
|
|
772
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
773
|
+
const magicHeader = Buffer.from(MAGIC_HEADER, 'utf8');
|
|
774
|
+
try {
|
|
775
|
+
await fs.mkdir(chunkDir, { recursive: true });
|
|
776
|
+
// Write payload with version 99 (unsupported future version)
|
|
777
|
+
const futureVersionPayload = Buffer.concat([
|
|
778
|
+
magicHeader,
|
|
779
|
+
Buffer.from([99]), // Future version
|
|
780
|
+
Buffer.alloc(KEY_ID_LENGTH + SALT_LENGTH + IV_LENGTH + 100 + TAG_LENGTH) // Dummy data
|
|
781
|
+
]);
|
|
782
|
+
await fs.writeFile(encryptedPath, futureVersionPayload);
|
|
783
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
784
|
+
chunkDir,
|
|
785
|
+
sha,
|
|
786
|
+
keySet: { primary: key, deprecated: [] }
|
|
787
|
+
}), (error) => {
|
|
788
|
+
const typed = error;
|
|
789
|
+
// The fallback decryption path may result in auth failure or version unsupported error
|
|
790
|
+
return typed.code === 'ENCRYPTION_VERSION_UNSUPPORTED' ||
|
|
791
|
+
typed.code === 'ENCRYPTION_AUTH_FAILED' ||
|
|
792
|
+
(typed.message && typed.message.includes('Unsupported encryption version'));
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
await cleanupTempDir(chunkDir);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
test('readChunkFromDisk returns null for missing chunk', async () => {
|
|
800
|
+
resetEncryptionCacheForTests();
|
|
801
|
+
resetEncryptionGuardsForTests();
|
|
802
|
+
const chunkDir = await createTempDir('missing');
|
|
803
|
+
const sha = 'nonexistent-sha';
|
|
804
|
+
try {
|
|
805
|
+
const result = await readChunkFromDisk({
|
|
806
|
+
chunkDir,
|
|
807
|
+
sha,
|
|
808
|
+
keySet: { primary: null, deprecated: [] }
|
|
809
|
+
});
|
|
810
|
+
assert.equal(result, null);
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
await cleanupTempDir(chunkDir);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
test('readChunkFromDisk fails with corrupted gzip data in plain file', async () => {
|
|
817
|
+
resetEncryptionCacheForTests();
|
|
818
|
+
resetEncryptionGuardsForTests();
|
|
819
|
+
const chunkDir = await createTempDir('bad-gzip');
|
|
820
|
+
const sha = 'bad-gzip-sha';
|
|
821
|
+
const plainPath = path.join(chunkDir, `${sha}.gz`);
|
|
822
|
+
try {
|
|
823
|
+
await fs.mkdir(chunkDir, { recursive: true });
|
|
824
|
+
// Write invalid gzip data
|
|
825
|
+
await fs.writeFile(plainPath, Buffer.from('not valid gzip data'));
|
|
826
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
827
|
+
chunkDir,
|
|
828
|
+
sha,
|
|
829
|
+
keySet: { primary: null, deprecated: [] }
|
|
830
|
+
}), (error) => {
|
|
831
|
+
const typed = error;
|
|
832
|
+
return typed.code === 'CHUNK_READ_FAILED';
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
finally {
|
|
836
|
+
await cleanupTempDir(chunkDir);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
test('readChunkFromDisk fails with corrupted gzip data in encrypted file', async () => {
|
|
840
|
+
resetEncryptionCacheForTests();
|
|
841
|
+
resetEncryptionGuardsForTests();
|
|
842
|
+
const chunkDir = await createTempDir('bad-gzip-enc');
|
|
843
|
+
const sha = 'bad-gzip-enc-sha';
|
|
844
|
+
const key = generateValidKey();
|
|
845
|
+
try {
|
|
846
|
+
// First write a valid encrypted chunk
|
|
847
|
+
await writeChunkToDisk({
|
|
848
|
+
chunkDir,
|
|
849
|
+
sha,
|
|
850
|
+
code: 'test',
|
|
851
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
852
|
+
});
|
|
853
|
+
// Now manually create an encrypted chunk with invalid compressed data
|
|
854
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
855
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
856
|
+
const hkdfInfo = Buffer.from(HKDF_INFO, 'utf8');
|
|
857
|
+
const derivedKey = Buffer.from(crypto.hkdfSync('sha256', key, salt, hkdfInfo, REQUIRED_KEY_LENGTH));
|
|
858
|
+
const keyId = crypto.createHash('sha256').update(key).digest().subarray(0, KEY_ID_LENGTH);
|
|
859
|
+
// Encrypt invalid gzip data
|
|
860
|
+
const badGzipData = Buffer.from('this is not gzip');
|
|
861
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', derivedKey, iv);
|
|
862
|
+
const ciphertext = Buffer.concat([cipher.update(badGzipData), cipher.final()]);
|
|
863
|
+
const tag = cipher.getAuthTag();
|
|
864
|
+
const magicHeader = Buffer.from(MAGIC_HEADER, 'utf8');
|
|
865
|
+
const payload = Buffer.concat([magicHeader, Buffer.from([2]), keyId, salt, iv, ciphertext, tag]);
|
|
866
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
867
|
+
await fs.writeFile(encryptedPath, payload);
|
|
868
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
869
|
+
chunkDir,
|
|
870
|
+
sha,
|
|
871
|
+
keySet: { primary: key, deprecated: [] }
|
|
872
|
+
}), (error) => {
|
|
873
|
+
const typed = error;
|
|
874
|
+
return typed.code === 'CHUNK_DECOMPRESSION_FAILED';
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
finally {
|
|
878
|
+
await cleanupTempDir(chunkDir);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
// ============================================================================
|
|
882
|
+
// Key ID and Fallback Tests
|
|
883
|
+
// ============================================================================
|
|
884
|
+
test('decrypt prefers matching key id first and still falls back to other keys', async () => {
|
|
885
|
+
resetEncryptionCacheForTests();
|
|
886
|
+
resetEncryptionGuardsForTests();
|
|
887
|
+
const chunkDir = await createTempDir('keyid-fallback');
|
|
888
|
+
const sha = 'enc-sha-1';
|
|
889
|
+
const activeKey = generateValidKey();
|
|
890
|
+
const fallbackKey = generateValidKey();
|
|
891
|
+
try {
|
|
892
|
+
await writeChunkToDisk({
|
|
893
|
+
chunkDir,
|
|
894
|
+
sha,
|
|
895
|
+
code: 'encrypted payload',
|
|
896
|
+
encryption: { enabled: true, key: activeKey, reason: 'test' }
|
|
897
|
+
});
|
|
898
|
+
const payload = await fs.readFile(path.join(chunkDir, `${sha}.gz.enc`));
|
|
899
|
+
const headerLength = Buffer.from(MAGIC_HEADER, 'utf8').length;
|
|
900
|
+
const storedKeyId = payload.subarray(headerLength + 1, headerLength + 1 + KEY_ID_LENGTH);
|
|
901
|
+
const expectedKeyId = crypto.createHash('sha256').update(activeKey).digest().subarray(0, KEY_ID_LENGTH);
|
|
902
|
+
assert.ok(storedKeyId.equals(expectedKeyId));
|
|
903
|
+
const result = await readChunkFromDisk({
|
|
904
|
+
chunkDir,
|
|
905
|
+
sha,
|
|
906
|
+
keySet: { primary: fallbackKey, deprecated: [activeKey] }
|
|
907
|
+
});
|
|
908
|
+
assert.ok(result);
|
|
909
|
+
assert.equal(result?.code, 'encrypted payload');
|
|
910
|
+
assert.equal(result?.encrypted, true);
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
await cleanupTempDir(chunkDir);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
test('decrypt fails when key id does not match any available key', async () => {
|
|
917
|
+
resetEncryptionCacheForTests();
|
|
918
|
+
resetEncryptionGuardsForTests();
|
|
919
|
+
const chunkDir = await createTempDir('no-matching-key');
|
|
920
|
+
const sha = 'no-match-sha';
|
|
921
|
+
const encryptionKey = generateValidKey();
|
|
922
|
+
const wrongKey = generateValidKey();
|
|
923
|
+
try {
|
|
924
|
+
await writeChunkToDisk({
|
|
925
|
+
chunkDir,
|
|
926
|
+
sha,
|
|
927
|
+
code: 'secret data',
|
|
928
|
+
encryption: { enabled: true, key: encryptionKey, reason: 'test' }
|
|
929
|
+
});
|
|
930
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
931
|
+
chunkDir,
|
|
932
|
+
sha,
|
|
933
|
+
keySet: { primary: wrongKey, deprecated: [] }
|
|
934
|
+
}), (error) => {
|
|
935
|
+
const typed = error;
|
|
936
|
+
// When key ID doesn't match, it reports KEY_NOT_FOUND
|
|
937
|
+
return typed.code === 'ENCRYPTION_KEY_ID_MISMATCH' ||
|
|
938
|
+
typed.code === 'ENCRYPTION_AUTH_FAILED' ||
|
|
939
|
+
typed.code === 'ENCRYPTION_KEY_NOT_FOUND';
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
finally {
|
|
943
|
+
await cleanupTempDir(chunkDir);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
// ============================================================================
|
|
947
|
+
// IV Reuse Detection Tests
|
|
948
|
+
// ============================================================================
|
|
949
|
+
test('detects IV reuse attempts for the same key', async () => {
|
|
950
|
+
resetEncryptionCacheForTests();
|
|
951
|
+
resetEncryptionGuardsForTests();
|
|
952
|
+
const chunkDir = await createTempDir('nonce-reuse');
|
|
953
|
+
const shaOne = 'nonce-1';
|
|
954
|
+
const shaTwo = 'nonce-2';
|
|
955
|
+
const key = generateValidKey();
|
|
956
|
+
// Stub randomBytes to return the same salt/iv pairs to force reuse
|
|
957
|
+
const stubRandomBytes = (size, callback) => {
|
|
958
|
+
const buffer = Buffer.alloc(size, 0x11);
|
|
959
|
+
if (callback) {
|
|
960
|
+
callback(null, buffer);
|
|
961
|
+
}
|
|
962
|
+
return buffer;
|
|
963
|
+
};
|
|
964
|
+
setEncryptionRandomBytes(stubRandomBytes);
|
|
965
|
+
try {
|
|
966
|
+
await writeChunkToDisk({
|
|
967
|
+
chunkDir,
|
|
968
|
+
sha: shaOne,
|
|
969
|
+
code: 'first',
|
|
970
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
971
|
+
});
|
|
972
|
+
await assert.rejects(() => writeChunkToDisk({
|
|
973
|
+
chunkDir,
|
|
974
|
+
sha: shaTwo,
|
|
975
|
+
code: 'second',
|
|
976
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
977
|
+
}), (error) => {
|
|
978
|
+
const typed = error;
|
|
979
|
+
return typed.code === 'ENCRYPTION_IV_REUSE';
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
resetEncryptionGuardsForTests();
|
|
984
|
+
await cleanupTempDir(chunkDir);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
test('allows same IV with different keys (different key IDs)', async () => {
|
|
988
|
+
resetEncryptionCacheForTests();
|
|
989
|
+
resetEncryptionGuardsForTests();
|
|
990
|
+
const chunkDir = await createTempDir('different-keys-same-iv');
|
|
991
|
+
const sha1 = 'key1-sha';
|
|
992
|
+
const sha2 = 'key2-sha';
|
|
993
|
+
const key1 = generateValidKey();
|
|
994
|
+
const key2 = generateValidKey();
|
|
995
|
+
// Stub randomBytes to return the same values
|
|
996
|
+
const stubRandomBytes = (size, callback) => {
|
|
997
|
+
const buffer = Buffer.alloc(size, 0x22);
|
|
998
|
+
if (callback) {
|
|
999
|
+
callback(null, buffer);
|
|
1000
|
+
}
|
|
1001
|
+
return buffer;
|
|
1002
|
+
};
|
|
1003
|
+
setEncryptionRandomBytes(stubRandomBytes);
|
|
1004
|
+
try {
|
|
1005
|
+
// Should succeed - different key means different key ID
|
|
1006
|
+
await writeChunkToDisk({
|
|
1007
|
+
chunkDir,
|
|
1008
|
+
sha: sha1,
|
|
1009
|
+
code: 'first with key1',
|
|
1010
|
+
encryption: { enabled: true, key: key1, reason: 'test' }
|
|
1011
|
+
});
|
|
1012
|
+
// Should also succeed - same IV but different key ID
|
|
1013
|
+
await writeChunkToDisk({
|
|
1014
|
+
chunkDir,
|
|
1015
|
+
sha: sha2,
|
|
1016
|
+
code: 'second with key2',
|
|
1017
|
+
encryption: { enabled: true, key: key2, reason: 'test' }
|
|
1018
|
+
});
|
|
1019
|
+
// Verify both can be read
|
|
1020
|
+
const result1 = await readChunkFromDisk({
|
|
1021
|
+
chunkDir,
|
|
1022
|
+
sha: sha1,
|
|
1023
|
+
keySet: { primary: key1, deprecated: [] }
|
|
1024
|
+
});
|
|
1025
|
+
assert.ok(result1);
|
|
1026
|
+
assert.equal(result1.code, 'first with key1');
|
|
1027
|
+
const result2 = await readChunkFromDisk({
|
|
1028
|
+
chunkDir,
|
|
1029
|
+
sha: sha2,
|
|
1030
|
+
keySet: { primary: key2, deprecated: [] }
|
|
1031
|
+
});
|
|
1032
|
+
assert.ok(result2);
|
|
1033
|
+
assert.equal(result2.code, 'second with key2');
|
|
1034
|
+
}
|
|
1035
|
+
finally {
|
|
1036
|
+
resetEncryptionGuardsForTests();
|
|
1037
|
+
await cleanupTempDir(chunkDir);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
// ============================================================================
|
|
1041
|
+
// Backward Compatibility Tests
|
|
1042
|
+
// ============================================================================
|
|
1043
|
+
test('remains backward compatible with version 1 payloads', async () => {
|
|
1044
|
+
resetEncryptionCacheForTests();
|
|
1045
|
+
resetEncryptionGuardsForTests();
|
|
1046
|
+
const chunkDir = await createTempDir('legacy-v1');
|
|
1047
|
+
const sha = 'legacy-sha';
|
|
1048
|
+
const key = generateValidKey();
|
|
1049
|
+
const salt = Buffer.alloc(SALT_LENGTH, 0x01);
|
|
1050
|
+
const iv = Buffer.alloc(IV_LENGTH, 0x02);
|
|
1051
|
+
const compressed = gzipSync(Buffer.from('legacy data', 'utf8'));
|
|
1052
|
+
const hkdfInfo = Buffer.from(HKDF_INFO, 'utf8');
|
|
1053
|
+
const derivedKey = crypto.hkdfSync('sha256', key, salt, hkdfInfo, REQUIRED_KEY_LENGTH);
|
|
1054
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', derivedKey, iv);
|
|
1055
|
+
const ciphertext = Buffer.concat([cipher.update(compressed), cipher.final()]);
|
|
1056
|
+
const tag = cipher.getAuthTag();
|
|
1057
|
+
const header = Buffer.from(MAGIC_HEADER, 'utf8');
|
|
1058
|
+
const payload = Buffer.concat([header, Buffer.from([1]), salt, iv, ciphertext, tag]);
|
|
1059
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1060
|
+
try {
|
|
1061
|
+
await fs.mkdir(chunkDir, { recursive: true });
|
|
1062
|
+
await fs.writeFile(encryptedPath, payload);
|
|
1063
|
+
const result = await readChunkFromDisk({ chunkDir, sha, keySet: { primary: key, deprecated: [] } });
|
|
1064
|
+
assert.ok(result);
|
|
1065
|
+
assert.equal(result?.code, 'legacy data');
|
|
1066
|
+
assert.equal(result?.encrypted, true);
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
await cleanupTempDir(chunkDir);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
test('remains backward compatible with legacy payloads without version byte', async () => {
|
|
1073
|
+
resetEncryptionCacheForTests();
|
|
1074
|
+
resetEncryptionGuardsForTests();
|
|
1075
|
+
const chunkDir = await createTempDir('legacy-no-version');
|
|
1076
|
+
const sha = 'legacy-no-ver-sha';
|
|
1077
|
+
const key = generateValidKey();
|
|
1078
|
+
const salt = Buffer.alloc(SALT_LENGTH, 0x03);
|
|
1079
|
+
const iv = Buffer.alloc(IV_LENGTH, 0x04);
|
|
1080
|
+
const compressed = gzipSync(Buffer.from('very old legacy', 'utf8'));
|
|
1081
|
+
const hkdfInfo = Buffer.from(HKDF_INFO, 'utf8');
|
|
1082
|
+
const derivedKey = crypto.hkdfSync('sha256', key, salt, hkdfInfo, REQUIRED_KEY_LENGTH);
|
|
1083
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', derivedKey, iv);
|
|
1084
|
+
const ciphertext = Buffer.concat([cipher.update(compressed), cipher.final()]);
|
|
1085
|
+
const tag = cipher.getAuthTag();
|
|
1086
|
+
const header = Buffer.from(MAGIC_HEADER, 'utf8');
|
|
1087
|
+
// No version byte - legacy format
|
|
1088
|
+
const payload = Buffer.concat([header, salt, iv, ciphertext, tag]);
|
|
1089
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1090
|
+
try {
|
|
1091
|
+
await fs.mkdir(chunkDir, { recursive: true });
|
|
1092
|
+
await fs.writeFile(encryptedPath, payload);
|
|
1093
|
+
const result = await readChunkFromDisk({ chunkDir, sha, keySet: { primary: key, deprecated: [] } });
|
|
1094
|
+
assert.ok(result);
|
|
1095
|
+
assert.equal(result?.code, 'very old legacy');
|
|
1096
|
+
assert.equal(result?.encrypted, true);
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
await cleanupTempDir(chunkDir);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
// ============================================================================
|
|
1103
|
+
// File Operation Tests
|
|
1104
|
+
// ============================================================================
|
|
1105
|
+
test('writeChunkToDisk removes plain file when writing encrypted', async () => {
|
|
1106
|
+
resetEncryptionCacheForTests();
|
|
1107
|
+
resetEncryptionGuardsForTests();
|
|
1108
|
+
const chunkDir = await createTempDir('remove-plain');
|
|
1109
|
+
const sha = 'remove-plain-sha';
|
|
1110
|
+
const key = generateValidKey();
|
|
1111
|
+
const plainPath = path.join(chunkDir, `${sha}.gz`);
|
|
1112
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1113
|
+
try {
|
|
1114
|
+
// First write as plain
|
|
1115
|
+
await writeChunkToDisk({
|
|
1116
|
+
chunkDir,
|
|
1117
|
+
sha,
|
|
1118
|
+
code: 'plain content',
|
|
1119
|
+
encryption: { enabled: false, key: null, reason: 'test' }
|
|
1120
|
+
});
|
|
1121
|
+
assert.ok((await fs.stat(plainPath).catch(() => null)) !== null);
|
|
1122
|
+
assert.equal((await fs.stat(encryptedPath).catch(() => null)), null);
|
|
1123
|
+
// Then write as encrypted - should remove plain file
|
|
1124
|
+
await writeChunkToDisk({
|
|
1125
|
+
chunkDir,
|
|
1126
|
+
sha,
|
|
1127
|
+
code: 'encrypted content',
|
|
1128
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
1129
|
+
});
|
|
1130
|
+
assert.equal((await fs.stat(plainPath).catch(() => null)), null);
|
|
1131
|
+
assert.ok((await fs.stat(encryptedPath).catch(() => null)) !== null);
|
|
1132
|
+
}
|
|
1133
|
+
finally {
|
|
1134
|
+
await cleanupTempDir(chunkDir);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
test('writeChunkToDisk removes encrypted file when writing plain', async () => {
|
|
1138
|
+
resetEncryptionCacheForTests();
|
|
1139
|
+
resetEncryptionGuardsForTests();
|
|
1140
|
+
const chunkDir = await createTempDir('remove-enc');
|
|
1141
|
+
const sha = 'remove-enc-sha';
|
|
1142
|
+
const key = generateValidKey();
|
|
1143
|
+
const plainPath = path.join(chunkDir, `${sha}.gz`);
|
|
1144
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1145
|
+
try {
|
|
1146
|
+
// First write as encrypted
|
|
1147
|
+
await writeChunkToDisk({
|
|
1148
|
+
chunkDir,
|
|
1149
|
+
sha,
|
|
1150
|
+
code: 'encrypted content',
|
|
1151
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
1152
|
+
});
|
|
1153
|
+
assert.equal((await fs.stat(plainPath).catch(() => null)), null);
|
|
1154
|
+
assert.ok((await fs.stat(encryptedPath).catch(() => null)) !== null);
|
|
1155
|
+
// Then write as plain - should remove encrypted file
|
|
1156
|
+
await writeChunkToDisk({
|
|
1157
|
+
chunkDir,
|
|
1158
|
+
sha,
|
|
1159
|
+
code: 'plain content',
|
|
1160
|
+
encryption: { enabled: false, key: null, reason: 'test' }
|
|
1161
|
+
});
|
|
1162
|
+
assert.ok((await fs.stat(plainPath).catch(() => null)) !== null);
|
|
1163
|
+
assert.equal((await fs.stat(encryptedPath).catch(() => null)), null);
|
|
1164
|
+
}
|
|
1165
|
+
finally {
|
|
1166
|
+
await cleanupTempDir(chunkDir);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
test('removeChunkArtifacts removes both plain and encrypted files', async () => {
|
|
1170
|
+
resetEncryptionCacheForTests();
|
|
1171
|
+
resetEncryptionGuardsForTests();
|
|
1172
|
+
const chunkDir = await createTempDir('remove-both');
|
|
1173
|
+
const sha = 'remove-both-sha';
|
|
1174
|
+
const key = generateValidKey();
|
|
1175
|
+
const plainPath = path.join(chunkDir, `${sha}.gz`);
|
|
1176
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1177
|
+
try {
|
|
1178
|
+
// Create both files
|
|
1179
|
+
await writeChunkToDisk({
|
|
1180
|
+
chunkDir,
|
|
1181
|
+
sha,
|
|
1182
|
+
code: 'plain',
|
|
1183
|
+
encryption: { enabled: false, key: null, reason: 'test' }
|
|
1184
|
+
});
|
|
1185
|
+
// Manually write encrypted file too
|
|
1186
|
+
await fs.writeFile(encryptedPath, Buffer.from('fake encrypted'));
|
|
1187
|
+
assert.ok((await fs.stat(plainPath).catch(() => null)) !== null);
|
|
1188
|
+
assert.ok((await fs.stat(encryptedPath).catch(() => null)) !== null);
|
|
1189
|
+
await removeChunkArtifacts(chunkDir, sha);
|
|
1190
|
+
assert.equal((await fs.stat(plainPath).catch(() => null)), null);
|
|
1191
|
+
assert.equal((await fs.stat(encryptedPath).catch(() => null)), null);
|
|
1192
|
+
}
|
|
1193
|
+
finally {
|
|
1194
|
+
await cleanupTempDir(chunkDir);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
test('removeChunkArtifacts handles missing files gracefully', async () => {
|
|
1198
|
+
resetEncryptionCacheForTests();
|
|
1199
|
+
const chunkDir = await createTempDir('remove-missing');
|
|
1200
|
+
const sha = 'missing-sha';
|
|
1201
|
+
try {
|
|
1202
|
+
// Should not throw even if files don't exist
|
|
1203
|
+
await removeChunkArtifacts(chunkDir, sha);
|
|
1204
|
+
// If we get here without throwing, the test passes
|
|
1205
|
+
assert.ok(true);
|
|
1206
|
+
}
|
|
1207
|
+
finally {
|
|
1208
|
+
await cleanupTempDir(chunkDir);
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
test('isChunkEncryptedOnDisk returns true for encrypted chunks', async () => {
|
|
1212
|
+
resetEncryptionCacheForTests();
|
|
1213
|
+
resetEncryptionGuardsForTests();
|
|
1214
|
+
const chunkDir = await createTempDir('is-encrypted-true');
|
|
1215
|
+
const sha = 'is-enc-sha';
|
|
1216
|
+
const key = generateValidKey();
|
|
1217
|
+
try {
|
|
1218
|
+
await writeChunkToDisk({
|
|
1219
|
+
chunkDir,
|
|
1220
|
+
sha,
|
|
1221
|
+
code: 'encrypted',
|
|
1222
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
1223
|
+
});
|
|
1224
|
+
assert.equal(isChunkEncryptedOnDisk(chunkDir, sha), true);
|
|
1225
|
+
}
|
|
1226
|
+
finally {
|
|
1227
|
+
await cleanupTempDir(chunkDir);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
test('isChunkEncryptedOnDisk returns false for plain chunks', async () => {
|
|
1231
|
+
resetEncryptionCacheForTests();
|
|
1232
|
+
resetEncryptionGuardsForTests();
|
|
1233
|
+
const chunkDir = await createTempDir('is-encrypted-false');
|
|
1234
|
+
const sha = 'is-plain-sha';
|
|
1235
|
+
try {
|
|
1236
|
+
await writeChunkToDisk({
|
|
1237
|
+
chunkDir,
|
|
1238
|
+
sha,
|
|
1239
|
+
code: 'plain',
|
|
1240
|
+
encryption: { enabled: false, key: null, reason: 'test' }
|
|
1241
|
+
});
|
|
1242
|
+
assert.equal(isChunkEncryptedOnDisk(chunkDir, sha), false);
|
|
1243
|
+
}
|
|
1244
|
+
finally {
|
|
1245
|
+
await cleanupTempDir(chunkDir);
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
test('isChunkEncryptedOnDisk returns false for missing chunks', async () => {
|
|
1249
|
+
resetEncryptionCacheForTests();
|
|
1250
|
+
const chunkDir = await createTempDir('is-encrypted-missing');
|
|
1251
|
+
const sha = 'missing-sha';
|
|
1252
|
+
try {
|
|
1253
|
+
assert.equal(isChunkEncryptedOnDisk(chunkDir, sha), false);
|
|
1254
|
+
}
|
|
1255
|
+
finally {
|
|
1256
|
+
await cleanupTempDir(chunkDir);
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
// ============================================================================
|
|
1260
|
+
// Auth Tag Verification Tests
|
|
1261
|
+
// ============================================================================
|
|
1262
|
+
test('decryption fails when auth tag is modified', async () => {
|
|
1263
|
+
resetEncryptionCacheForTests();
|
|
1264
|
+
resetEncryptionGuardsForTests();
|
|
1265
|
+
const chunkDir = await createTempDir('bad-tag');
|
|
1266
|
+
const sha = 'bad-tag-sha';
|
|
1267
|
+
const key = generateValidKey();
|
|
1268
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1269
|
+
try {
|
|
1270
|
+
await writeChunkToDisk({
|
|
1271
|
+
chunkDir,
|
|
1272
|
+
sha,
|
|
1273
|
+
code: 'test content',
|
|
1274
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
1275
|
+
});
|
|
1276
|
+
// Modify the auth tag (last 16 bytes)
|
|
1277
|
+
const payload = await fs.readFile(encryptedPath);
|
|
1278
|
+
const modifiedPayload = Buffer.from(payload);
|
|
1279
|
+
modifiedPayload[modifiedPayload.length - 1] ^= 0xFF;
|
|
1280
|
+
await fs.writeFile(encryptedPath, modifiedPayload);
|
|
1281
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
1282
|
+
chunkDir,
|
|
1283
|
+
sha,
|
|
1284
|
+
keySet: { primary: key, deprecated: [] }
|
|
1285
|
+
}), (error) => {
|
|
1286
|
+
const typed = error;
|
|
1287
|
+
return typed.code === 'ENCRYPTION_AUTH_FAILED';
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
finally {
|
|
1291
|
+
await cleanupTempDir(chunkDir);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
test('decryption fails when IV is modified', async () => {
|
|
1295
|
+
resetEncryptionCacheForTests();
|
|
1296
|
+
resetEncryptionGuardsForTests();
|
|
1297
|
+
const chunkDir = await createTempDir('bad-iv');
|
|
1298
|
+
const sha = 'bad-iv-sha';
|
|
1299
|
+
const key = generateValidKey();
|
|
1300
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1301
|
+
try {
|
|
1302
|
+
await writeChunkToDisk({
|
|
1303
|
+
chunkDir,
|
|
1304
|
+
sha,
|
|
1305
|
+
code: 'test content',
|
|
1306
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
1307
|
+
});
|
|
1308
|
+
// Modify the IV (after header + version + keyId + salt)
|
|
1309
|
+
const payload = await fs.readFile(encryptedPath);
|
|
1310
|
+
const modifiedPayload = Buffer.from(payload);
|
|
1311
|
+
const ivOffset = MAGIC_HEADER.length + 1 + KEY_ID_LENGTH + SALT_LENGTH;
|
|
1312
|
+
modifiedPayload[ivOffset] ^= 0xFF;
|
|
1313
|
+
await fs.writeFile(encryptedPath, modifiedPayload);
|
|
1314
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
1315
|
+
chunkDir,
|
|
1316
|
+
sha,
|
|
1317
|
+
keySet: { primary: key, deprecated: [] }
|
|
1318
|
+
}), (error) => {
|
|
1319
|
+
const typed = error;
|
|
1320
|
+
return typed.code === 'ENCRYPTION_AUTH_FAILED';
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
finally {
|
|
1324
|
+
await cleanupTempDir(chunkDir);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
test('decryption fails when salt is modified', async () => {
|
|
1328
|
+
resetEncryptionCacheForTests();
|
|
1329
|
+
resetEncryptionGuardsForTests();
|
|
1330
|
+
const chunkDir = await createTempDir('bad-salt');
|
|
1331
|
+
const sha = 'bad-salt-sha';
|
|
1332
|
+
const key = generateValidKey();
|
|
1333
|
+
const encryptedPath = path.join(chunkDir, `${sha}.gz.enc`);
|
|
1334
|
+
try {
|
|
1335
|
+
await writeChunkToDisk({
|
|
1336
|
+
chunkDir,
|
|
1337
|
+
sha,
|
|
1338
|
+
code: 'test content',
|
|
1339
|
+
encryption: { enabled: true, key, reason: 'test' }
|
|
1340
|
+
});
|
|
1341
|
+
// Modify the salt (after header + version + keyId)
|
|
1342
|
+
const payload = await fs.readFile(encryptedPath);
|
|
1343
|
+
const modifiedPayload = Buffer.from(payload);
|
|
1344
|
+
const saltOffset = MAGIC_HEADER.length + 1 + KEY_ID_LENGTH;
|
|
1345
|
+
modifiedPayload[saltOffset] ^= 0xFF;
|
|
1346
|
+
await fs.writeFile(encryptedPath, modifiedPayload);
|
|
1347
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
1348
|
+
chunkDir,
|
|
1349
|
+
sha,
|
|
1350
|
+
keySet: { primary: key, deprecated: [] }
|
|
1351
|
+
}), (error) => {
|
|
1352
|
+
const typed = error;
|
|
1353
|
+
return typed.code === 'ENCRYPTION_AUTH_FAILED';
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
finally {
|
|
1357
|
+
await cleanupTempDir(chunkDir);
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
// ============================================================================
|
|
1361
|
+
// Encryption Preference with No Logger Tests
|
|
1362
|
+
// ============================================================================
|
|
1363
|
+
test('resolveEncryptionPreference works without logger', () => {
|
|
1364
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
1365
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = 'invalid';
|
|
1366
|
+
resetEncryptionCacheForTests();
|
|
1367
|
+
try {
|
|
1368
|
+
// Should not throw even without logger
|
|
1369
|
+
const pref = resolveEncryptionPreference({});
|
|
1370
|
+
assert.equal(pref.enabled, false);
|
|
1371
|
+
assert.equal(pref.reason, 'invalid_key');
|
|
1372
|
+
}
|
|
1373
|
+
finally {
|
|
1374
|
+
if (originalKey !== undefined) {
|
|
1375
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
1379
|
+
}
|
|
1380
|
+
resetEncryptionCacheForTests();
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
test('resolveEncryptionPreference works with logger missing warn method', () => {
|
|
1384
|
+
const originalKey = process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
1385
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = 'invalid';
|
|
1386
|
+
resetEncryptionCacheForTests();
|
|
1387
|
+
try {
|
|
1388
|
+
// Logger without warn method
|
|
1389
|
+
const pref = resolveEncryptionPreference({ logger: {} });
|
|
1390
|
+
assert.equal(pref.enabled, false);
|
|
1391
|
+
assert.equal(pref.reason, 'invalid_key');
|
|
1392
|
+
}
|
|
1393
|
+
finally {
|
|
1394
|
+
if (originalKey !== undefined) {
|
|
1395
|
+
process.env.CODEVAULT_ENCRYPTION_KEY = originalKey;
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
delete process.env.CODEVAULT_ENCRYPTION_KEY;
|
|
1399
|
+
}
|
|
1400
|
+
resetEncryptionCacheForTests();
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
// ============================================================================
|
|
1404
|
+
// Multiple Deprecated Keys Tests
|
|
1405
|
+
// ============================================================================
|
|
1406
|
+
test('reads chunk encrypted with second deprecated key', async () => {
|
|
1407
|
+
resetEncryptionCacheForTests();
|
|
1408
|
+
resetEncryptionGuardsForTests();
|
|
1409
|
+
const chunkDir = await createTempDir('multi-deprecated');
|
|
1410
|
+
const sha = 'multi-dep-sha';
|
|
1411
|
+
const primaryKey = generateValidKey();
|
|
1412
|
+
const deprecatedKey1 = generateValidKey();
|
|
1413
|
+
const deprecatedKey2 = generateValidKey();
|
|
1414
|
+
try {
|
|
1415
|
+
// Encrypt with deprecatedKey2
|
|
1416
|
+
await writeChunkToDisk({
|
|
1417
|
+
chunkDir,
|
|
1418
|
+
sha,
|
|
1419
|
+
code: 'encrypted with deprecated key 2',
|
|
1420
|
+
encryption: { enabled: true, key: deprecatedKey2, reason: 'test' }
|
|
1421
|
+
});
|
|
1422
|
+
// Read with primary + both deprecated keys
|
|
1423
|
+
const result = await readChunkFromDisk({
|
|
1424
|
+
chunkDir,
|
|
1425
|
+
sha,
|
|
1426
|
+
keySet: {
|
|
1427
|
+
primary: primaryKey,
|
|
1428
|
+
deprecated: [deprecatedKey1, deprecatedKey2]
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
assert.ok(result);
|
|
1432
|
+
assert.equal(result.code, 'encrypted with deprecated key 2');
|
|
1433
|
+
assert.equal(result.encrypted, true);
|
|
1434
|
+
}
|
|
1435
|
+
finally {
|
|
1436
|
+
await cleanupTempDir(chunkDir);
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
// ============================================================================
|
|
1440
|
+
// Edge Case: Payload Key ID Extraction Tests
|
|
1441
|
+
// ============================================================================
|
|
1442
|
+
test('readChunkFromDisk provides key id context in error message', async () => {
|
|
1443
|
+
resetEncryptionCacheForTests();
|
|
1444
|
+
resetEncryptionGuardsForTests();
|
|
1445
|
+
const chunkDir = await createTempDir('keyid-error');
|
|
1446
|
+
const sha = 'keyid-error-sha';
|
|
1447
|
+
const encryptionKey = generateValidKey();
|
|
1448
|
+
const wrongKey = generateValidKey();
|
|
1449
|
+
try {
|
|
1450
|
+
await writeChunkToDisk({
|
|
1451
|
+
chunkDir,
|
|
1452
|
+
sha,
|
|
1453
|
+
code: 'secret',
|
|
1454
|
+
encryption: { enabled: true, key: encryptionKey, reason: 'test' }
|
|
1455
|
+
});
|
|
1456
|
+
await assert.rejects(() => readChunkFromDisk({
|
|
1457
|
+
chunkDir,
|
|
1458
|
+
sha,
|
|
1459
|
+
keySet: { primary: wrongKey, deprecated: [] }
|
|
1460
|
+
}), (error) => {
|
|
1461
|
+
const typed = error;
|
|
1462
|
+
// Error should contain key id or authentication context
|
|
1463
|
+
return typed.message !== undefined && typed.message.length > 0;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
finally {
|
|
1467
|
+
await cleanupTempDir(chunkDir);
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
//# sourceMappingURL=encrypted-chunks.test.js.map
|