@imayuur/contexthub-core 1.0.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/dist/config.d.ts +21 -0
- package/dist/config.js +75 -0
- package/dist/contexthub-ignore.d.ts +7 -0
- package/dist/contexthub-ignore.js +96 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +78 -0
- package/dist/limits.d.ts +25 -0
- package/dist/limits.js +34 -0
- package/dist/memory-storage.d.ts +53 -0
- package/dist/memory-storage.js +419 -0
- package/dist/query-pipeline.d.ts +20 -0
- package/dist/query-pipeline.js +182 -0
- package/dist/security.d.ts +118 -0
- package/dist/security.js +408 -0
- package/package.json +54 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecurityManager — Centralized security module for ContextHub
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - AES-256-GCM encryption/decryption for data at rest
|
|
6
|
+
* - Key management with auto-generation
|
|
7
|
+
* - Input sanitization and validation
|
|
8
|
+
* - Sensitive pattern detection (API keys, passwords, tokens)
|
|
9
|
+
* - Path traversal prevention
|
|
10
|
+
* - Auth token generation/verification for MCP
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* File patterns to exclude from repo parsing (contain secrets or certs).
|
|
14
|
+
*/
|
|
15
|
+
export declare const SENSITIVE_FILE_PATTERNS: string[];
|
|
16
|
+
export declare class SecurityManager {
|
|
17
|
+
private repoPath;
|
|
18
|
+
private keyPath;
|
|
19
|
+
private encryptionKey;
|
|
20
|
+
constructor(repoPath: string);
|
|
21
|
+
/**
|
|
22
|
+
* Get or create the encryption key.
|
|
23
|
+
* Priority: CONTEXTHUB_KEY env var > .keyfile on disk > auto-generate
|
|
24
|
+
*/
|
|
25
|
+
private getKey;
|
|
26
|
+
/**
|
|
27
|
+
* Save encryption key to disk with restrictive permissions (owner-only).
|
|
28
|
+
*/
|
|
29
|
+
private saveKeyFile;
|
|
30
|
+
/**
|
|
31
|
+
* Encrypt plaintext string using AES-256-GCM.
|
|
32
|
+
* Returns base64-encoded string: IV (12B) + AuthTag (16B) + Ciphertext
|
|
33
|
+
*/
|
|
34
|
+
encrypt(plaintext: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Decrypt a base64-encoded encrypted string.
|
|
37
|
+
* Verifies auth tag to detect tampering.
|
|
38
|
+
*/
|
|
39
|
+
decrypt(encryptedBase64: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Check if data looks like it's already encrypted (base64 with correct prefix length).
|
|
42
|
+
*/
|
|
43
|
+
isEncrypted(data: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Sanitize user input: trim, enforce max length, strip control characters.
|
|
46
|
+
*/
|
|
47
|
+
sanitizeInput(input: string | undefined, maxLength?: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Validate a query string (shorter max length).
|
|
50
|
+
*/
|
|
51
|
+
sanitizeQuery(query: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Validate a numeric limit parameter.
|
|
54
|
+
*/
|
|
55
|
+
validateLimit(limit: number, min?: number, max?: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Validate a port number.
|
|
58
|
+
*/
|
|
59
|
+
validatePort(port: number): number;
|
|
60
|
+
/**
|
|
61
|
+
* Validate an array of related paths.
|
|
62
|
+
*/
|
|
63
|
+
validateRelatedPaths(paths: unknown): string[];
|
|
64
|
+
/**
|
|
65
|
+
* Validate an array of related symbols.
|
|
66
|
+
*/
|
|
67
|
+
validateRelatedSymbols(symbols: unknown): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Validate a git commit hash.
|
|
70
|
+
*/
|
|
71
|
+
validateCommitHash(hash: unknown): string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Validate memory type against allowed values.
|
|
74
|
+
*/
|
|
75
|
+
validateMemoryType(type: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Check if text contains sensitive patterns (API keys, passwords, etc.).
|
|
78
|
+
* Returns true if the text should NOT be stored.
|
|
79
|
+
*/
|
|
80
|
+
isSensitive(text: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Redact sensitive parts of text, replacing matched patterns with [REDACTED].
|
|
83
|
+
*/
|
|
84
|
+
redactSensitive(text: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Validate that a path resolves within the repo boundary.
|
|
87
|
+
* Prevents directory traversal attacks.
|
|
88
|
+
*/
|
|
89
|
+
validatePath(inputPath: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Check if a filename matches sensitive file patterns.
|
|
92
|
+
*/
|
|
93
|
+
isSensitiveFile(filePath: string): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Generate an HMAC-based auth token for MCP server authentication.
|
|
96
|
+
*/
|
|
97
|
+
generateAuthToken(): string;
|
|
98
|
+
/**
|
|
99
|
+
* Verify an auth token. Returns true if valid.
|
|
100
|
+
*/
|
|
101
|
+
verifyAuthToken(token: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Check if auth is required (CONTEXTHUB_TOKEN env var is set).
|
|
104
|
+
*/
|
|
105
|
+
isAuthRequired(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Check file size before reading. Throws if too large.
|
|
108
|
+
*/
|
|
109
|
+
checkFileSize(filePath: string, maxBytes?: number): void;
|
|
110
|
+
/**
|
|
111
|
+
* Set secure permissions on a file or directory.
|
|
112
|
+
*/
|
|
113
|
+
setSecurePermissions(targetPath: string, isDirectory?: boolean): void;
|
|
114
|
+
get maxMemoryEntries(): number;
|
|
115
|
+
get maxInputLength(): number;
|
|
116
|
+
get maxQueryLength(): number;
|
|
117
|
+
get maxFileSizeBytes(): number;
|
|
118
|
+
}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SecurityManager — Centralized security module for ContextHub
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - AES-256-GCM encryption/decryption for data at rest
|
|
7
|
+
* - Key management with auto-generation
|
|
8
|
+
* - Input sanitization and validation
|
|
9
|
+
* - Sensitive pattern detection (API keys, passwords, tokens)
|
|
10
|
+
* - Path traversal prevention
|
|
11
|
+
* - Auth token generation/verification for MCP
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.SecurityManager = exports.SENSITIVE_FILE_PATTERNS = void 0;
|
|
48
|
+
const crypto = __importStar(require("crypto"));
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const limits_1 = require("./limits");
|
|
52
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
53
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
54
|
+
const IV_LENGTH = 12; // NIST-recommended for GCM
|
|
55
|
+
const AUTH_TAG_LENGTH = 16;
|
|
56
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
57
|
+
const SALT_PREFIX = 'contexthub-v1-';
|
|
58
|
+
/**
|
|
59
|
+
* Patterns that indicate sensitive data that should NOT be stored in memory.
|
|
60
|
+
* Each pattern is tested case-insensitively against command/content text.
|
|
61
|
+
*/
|
|
62
|
+
const SENSITIVE_PATTERNS = [
|
|
63
|
+
// API keys and tokens
|
|
64
|
+
/(?:api[_-]?key|apikey)\s*[=:]\s*\S+/i,
|
|
65
|
+
/(?:secret[_-]?key|secretkey)\s*[=:]\s*\S+/i,
|
|
66
|
+
/(?:access[_-]?token|accesstoken)\s*[=:]\s*\S+/i,
|
|
67
|
+
/(?:auth[_-]?token|authtoken)\s*[=:]\s*\S+/i,
|
|
68
|
+
/(?:bearer)\s+\S+/i,
|
|
69
|
+
/(?:token)\s*[=:]\s*\S{20,}/i,
|
|
70
|
+
// Common key prefixes
|
|
71
|
+
/sk-[a-zA-Z0-9_-]{10,}/, // OpenAI / Stripe style (sk-proj-xxx, sk-xxx)
|
|
72
|
+
/ghp_[a-zA-Z0-9]{36,}/, // GitHub PAT
|
|
73
|
+
/glpat-[a-zA-Z0-9]{20,}/, // GitLab PAT
|
|
74
|
+
/xox[bpras]-[a-zA-Z0-9-]{10,}/, // Slack tokens
|
|
75
|
+
/AKIA[0-9A-Z]{16}/, // AWS Access Key
|
|
76
|
+
// Passwords and credentials
|
|
77
|
+
/(?:password|passwd|pwd)\s*[=:]\s*\S+/i,
|
|
78
|
+
/(?:mysql|postgres|redis|mongo).*-p\s*\S+/i,
|
|
79
|
+
/-p\s*['"]?[^\s'"]+['"]?\s/, // CLI password flags
|
|
80
|
+
// SSH and certificates
|
|
81
|
+
/-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH)?\s*PRIVATE\s+KEY-----/,
|
|
82
|
+
/-----BEGIN\s+CERTIFICATE-----/,
|
|
83
|
+
// Environment variable exports with sensitive names
|
|
84
|
+
/export\s+(?:\w*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)\w*)\s*=/i,
|
|
85
|
+
// Connection strings
|
|
86
|
+
/(?:mongodb|postgres|mysql|redis|amqp):\/\/[^@\s]+:[^@\s]+@/i,
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* File patterns to exclude from repo parsing (contain secrets or certs).
|
|
90
|
+
*/
|
|
91
|
+
exports.SENSITIVE_FILE_PATTERNS = [
|
|
92
|
+
'.env', '.env.*', '*.pem', '*.key', '*.p12', '*.pfx', '*.jks',
|
|
93
|
+
'*.keystore', 'credentials*', 'secrets*', '.npmrc', '.pypirc',
|
|
94
|
+
'id_rsa*', 'id_ed25519*', '*.crt', '*.cert',
|
|
95
|
+
];
|
|
96
|
+
// ─── SecurityManager ─────────────────────────────────────────────────────────
|
|
97
|
+
class SecurityManager {
|
|
98
|
+
constructor(repoPath) {
|
|
99
|
+
this.encryptionKey = null;
|
|
100
|
+
this.repoPath = path.resolve(repoPath);
|
|
101
|
+
this.keyPath = path.join(this.repoPath, '.contexthub', '.keyfile');
|
|
102
|
+
}
|
|
103
|
+
// ── Key Management ──────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Get or create the encryption key.
|
|
106
|
+
* Priority: CONTEXTHUB_KEY env var > .keyfile on disk > auto-generate
|
|
107
|
+
*/
|
|
108
|
+
getKey() {
|
|
109
|
+
if (this.encryptionKey)
|
|
110
|
+
return this.encryptionKey;
|
|
111
|
+
// 1. Check environment variable
|
|
112
|
+
const envKey = process.env.CONTEXTHUB_KEY;
|
|
113
|
+
if (envKey && envKey.length > 0) {
|
|
114
|
+
// Use per-repo unique salt derived from repo path
|
|
115
|
+
const repoSalt = SALT_PREFIX + crypto.createHash('sha256').update(this.repoPath).digest('hex').slice(0, 16);
|
|
116
|
+
this.encryptionKey = crypto.scryptSync(envKey, repoSalt, KEY_LENGTH);
|
|
117
|
+
return this.encryptionKey;
|
|
118
|
+
}
|
|
119
|
+
// 2. Check .keyfile on disk
|
|
120
|
+
if (fs.existsSync(this.keyPath)) {
|
|
121
|
+
try {
|
|
122
|
+
const keyHex = fs.readFileSync(this.keyPath, 'utf-8').trim();
|
|
123
|
+
this.encryptionKey = Buffer.from(keyHex, 'hex');
|
|
124
|
+
if (this.encryptionKey.length !== KEY_LENGTH) {
|
|
125
|
+
throw new Error('Invalid key length in .keyfile');
|
|
126
|
+
}
|
|
127
|
+
return this.encryptionKey;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Key file corrupted — regenerate
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 3. Auto-generate new key
|
|
134
|
+
this.encryptionKey = crypto.randomBytes(KEY_LENGTH);
|
|
135
|
+
this.saveKeyFile(this.encryptionKey);
|
|
136
|
+
return this.encryptionKey;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Save encryption key to disk with restrictive permissions (owner-only).
|
|
140
|
+
*/
|
|
141
|
+
saveKeyFile(key) {
|
|
142
|
+
const dir = path.dirname(this.keyPath);
|
|
143
|
+
if (!fs.existsSync(dir)) {
|
|
144
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
145
|
+
}
|
|
146
|
+
fs.writeFileSync(this.keyPath, key.toString('hex'), { mode: 0o600 });
|
|
147
|
+
}
|
|
148
|
+
// ── Encryption ──────────────────────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Encrypt plaintext string using AES-256-GCM.
|
|
151
|
+
* Returns base64-encoded string: IV (12B) + AuthTag (16B) + Ciphertext
|
|
152
|
+
*/
|
|
153
|
+
encrypt(plaintext) {
|
|
154
|
+
const key = this.getKey();
|
|
155
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
156
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
157
|
+
const encrypted = Buffer.concat([
|
|
158
|
+
cipher.update(plaintext, 'utf-8'),
|
|
159
|
+
cipher.final(),
|
|
160
|
+
]);
|
|
161
|
+
const authTag = cipher.getAuthTag();
|
|
162
|
+
// Combine: IV + AuthTag + Ciphertext
|
|
163
|
+
const combined = Buffer.concat([iv, authTag, encrypted]);
|
|
164
|
+
return combined.toString('base64');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Decrypt a base64-encoded encrypted string.
|
|
168
|
+
* Verifies auth tag to detect tampering.
|
|
169
|
+
*/
|
|
170
|
+
decrypt(encryptedBase64) {
|
|
171
|
+
const key = this.getKey();
|
|
172
|
+
const combined = Buffer.from(encryptedBase64, 'base64');
|
|
173
|
+
if (combined.length < IV_LENGTH + AUTH_TAG_LENGTH) {
|
|
174
|
+
throw new Error('Invalid encrypted data: too short');
|
|
175
|
+
}
|
|
176
|
+
const iv = combined.subarray(0, IV_LENGTH);
|
|
177
|
+
const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
178
|
+
const ciphertext = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
179
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
180
|
+
decipher.setAuthTag(authTag);
|
|
181
|
+
const decrypted = Buffer.concat([
|
|
182
|
+
decipher.update(ciphertext),
|
|
183
|
+
decipher.final(),
|
|
184
|
+
]);
|
|
185
|
+
return decrypted.toString('utf-8');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if data looks like it's already encrypted (base64 with correct prefix length).
|
|
189
|
+
*/
|
|
190
|
+
isEncrypted(data) {
|
|
191
|
+
try {
|
|
192
|
+
const buf = Buffer.from(data, 'base64');
|
|
193
|
+
// Must be at least IV + AuthTag + 1 byte of ciphertext
|
|
194
|
+
return buf.length >= IV_LENGTH + AUTH_TAG_LENGTH + 1 &&
|
|
195
|
+
data === buf.toString('base64'); // Valid base64 round-trip
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ── Input Validation ────────────────────────────────────────────────────
|
|
202
|
+
/**
|
|
203
|
+
* Sanitize user input: trim, enforce max length, strip control characters.
|
|
204
|
+
*/
|
|
205
|
+
sanitizeInput(input, maxLength = limits_1.MAX_MEMORY_CONTENT_LENGTH) {
|
|
206
|
+
if (typeof input !== 'string') {
|
|
207
|
+
throw new Error('Input must be a string');
|
|
208
|
+
}
|
|
209
|
+
// Strip null bytes and dangerous control characters (keep \n, \r, \t)
|
|
210
|
+
let sanitized = input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
211
|
+
// Enforce max length
|
|
212
|
+
if (sanitized.length > maxLength) {
|
|
213
|
+
sanitized = sanitized.substring(0, maxLength);
|
|
214
|
+
}
|
|
215
|
+
return sanitized.trim();
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Validate a query string (shorter max length).
|
|
219
|
+
*/
|
|
220
|
+
sanitizeQuery(query) {
|
|
221
|
+
return this.sanitizeInput(query, limits_1.MAX_QUERY_LENGTH);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Validate a numeric limit parameter.
|
|
225
|
+
*/
|
|
226
|
+
validateLimit(limit, min = 1, max = 1000) {
|
|
227
|
+
if (limit === undefined || limit === null)
|
|
228
|
+
return max;
|
|
229
|
+
const num = Math.floor(Number(limit));
|
|
230
|
+
if (isNaN(num) || num < min)
|
|
231
|
+
return min;
|
|
232
|
+
if (num > max)
|
|
233
|
+
return max;
|
|
234
|
+
return num;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Validate a port number.
|
|
238
|
+
*/
|
|
239
|
+
validatePort(port) {
|
|
240
|
+
const num = Math.floor(Number(port));
|
|
241
|
+
if (isNaN(num) || num < 1024 || num > 65535) {
|
|
242
|
+
throw new Error(`Invalid port: ${port}. Must be between 1024 and 65535.`);
|
|
243
|
+
}
|
|
244
|
+
return num;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Validate an array of related paths.
|
|
248
|
+
*/
|
|
249
|
+
validateRelatedPaths(paths) {
|
|
250
|
+
if (!Array.isArray(paths))
|
|
251
|
+
return [];
|
|
252
|
+
return paths.slice(0, 20).map(p => this.validatePath(String(p)));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Validate an array of related symbols.
|
|
256
|
+
*/
|
|
257
|
+
validateRelatedSymbols(symbols) {
|
|
258
|
+
if (!Array.isArray(symbols))
|
|
259
|
+
return [];
|
|
260
|
+
return symbols.slice(0, 20).map(s => this.sanitizeInput(String(s), 200)).filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Validate a git commit hash.
|
|
264
|
+
*/
|
|
265
|
+
validateCommitHash(hash) {
|
|
266
|
+
if (!hash)
|
|
267
|
+
return undefined;
|
|
268
|
+
const h = this.sanitizeInput(String(hash), 40);
|
|
269
|
+
if (!/^[a-f0-9]{7,40}$/i.test(h))
|
|
270
|
+
return undefined;
|
|
271
|
+
return h.toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Validate memory type against allowed values.
|
|
275
|
+
*/
|
|
276
|
+
validateMemoryType(type) {
|
|
277
|
+
const ALLOWED_TYPES = ['prompt', 'response', 'summary', 'decision', 'architecture', 'bugfix', 'manual', 'commit'];
|
|
278
|
+
const normalized = type.toLowerCase().trim();
|
|
279
|
+
if (!ALLOWED_TYPES.includes(normalized)) {
|
|
280
|
+
return 'manual'; // Default to 'manual' for unknown types
|
|
281
|
+
}
|
|
282
|
+
return normalized;
|
|
283
|
+
}
|
|
284
|
+
// ── Sensitive Data Detection ────────────────────────────────────────────
|
|
285
|
+
/**
|
|
286
|
+
* Check if text contains sensitive patterns (API keys, passwords, etc.).
|
|
287
|
+
* Returns true if the text should NOT be stored.
|
|
288
|
+
*/
|
|
289
|
+
isSensitive(text) {
|
|
290
|
+
return SENSITIVE_PATTERNS.some(pattern => pattern.test(text));
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Redact sensitive parts of text, replacing matched patterns with [REDACTED].
|
|
294
|
+
*/
|
|
295
|
+
redactSensitive(text) {
|
|
296
|
+
let result = text;
|
|
297
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
298
|
+
result = result.replace(pattern, '[REDACTED]');
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
// ── Path Validation ─────────────────────────────────────────────────────
|
|
303
|
+
/**
|
|
304
|
+
* Validate that a path resolves within the repo boundary.
|
|
305
|
+
* Prevents directory traversal attacks.
|
|
306
|
+
*/
|
|
307
|
+
validatePath(inputPath) {
|
|
308
|
+
const resolved = path.resolve(this.repoPath, inputPath);
|
|
309
|
+
// Ensure the resolved path starts with the repo path
|
|
310
|
+
if (!resolved.startsWith(this.repoPath + path.sep) && resolved !== this.repoPath) {
|
|
311
|
+
throw new Error('Path traversal detected: path escapes repository boundary');
|
|
312
|
+
}
|
|
313
|
+
return resolved;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Check if a filename matches sensitive file patterns.
|
|
317
|
+
*/
|
|
318
|
+
isSensitiveFile(filePath) {
|
|
319
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
320
|
+
return exports.SENSITIVE_FILE_PATTERNS.some(pattern => {
|
|
321
|
+
// Convert glob-like pattern to simple check
|
|
322
|
+
const p = pattern.toLowerCase();
|
|
323
|
+
if (p.startsWith('*.')) {
|
|
324
|
+
return basename.endsWith(p.slice(1));
|
|
325
|
+
}
|
|
326
|
+
if (p.endsWith('*')) {
|
|
327
|
+
return basename.startsWith(p.slice(0, -1));
|
|
328
|
+
}
|
|
329
|
+
if (p.includes('.*')) {
|
|
330
|
+
const prefix = p.split('.*')[0];
|
|
331
|
+
return basename.startsWith(prefix);
|
|
332
|
+
}
|
|
333
|
+
return basename === p;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// ── Auth Token ──────────────────────────────────────────────────────────
|
|
337
|
+
/**
|
|
338
|
+
* Generate an HMAC-based auth token for MCP server authentication.
|
|
339
|
+
*/
|
|
340
|
+
generateAuthToken() {
|
|
341
|
+
const key = this.getKey();
|
|
342
|
+
const payload = `contexthub-auth-${Date.now()}`;
|
|
343
|
+
const hmac = crypto.createHmac('sha256', key);
|
|
344
|
+
hmac.update(payload);
|
|
345
|
+
const token = `${Buffer.from(payload).toString('base64')}.${hmac.digest('hex')}`;
|
|
346
|
+
// Store the token
|
|
347
|
+
const tokenPath = path.join(this.repoPath, '.contexthub', '.auth-token');
|
|
348
|
+
fs.writeFileSync(tokenPath, token, { mode: 0o600 });
|
|
349
|
+
return token;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Verify an auth token. Returns true if valid.
|
|
353
|
+
*/
|
|
354
|
+
verifyAuthToken(token) {
|
|
355
|
+
try {
|
|
356
|
+
const tokenPath = path.join(this.repoPath, '.contexthub', '.auth-token');
|
|
357
|
+
if (!fs.existsSync(tokenPath))
|
|
358
|
+
return false;
|
|
359
|
+
const storedToken = fs.readFileSync(tokenPath, 'utf-8').trim();
|
|
360
|
+
// Hash both tokens to fixed length before comparison
|
|
361
|
+
// This prevents timingSafeEqual from crashing on length mismatch
|
|
362
|
+
// and avoids leaking length information
|
|
363
|
+
const hashA = crypto.createHash('sha256').update(token).digest();
|
|
364
|
+
const hashB = crypto.createHash('sha256').update(storedToken).digest();
|
|
365
|
+
return crypto.timingSafeEqual(hashA, hashB);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Check if auth is required (CONTEXTHUB_TOKEN env var is set).
|
|
373
|
+
*/
|
|
374
|
+
isAuthRequired() {
|
|
375
|
+
return !!process.env.CONTEXTHUB_TOKEN;
|
|
376
|
+
}
|
|
377
|
+
// ── File Safety ─────────────────────────────────────────────────────────
|
|
378
|
+
/**
|
|
379
|
+
* Check file size before reading. Throws if too large.
|
|
380
|
+
*/
|
|
381
|
+
checkFileSize(filePath, maxBytes = limits_1.MAX_INGEST_FILE_SIZE) {
|
|
382
|
+
if (!fs.existsSync(filePath))
|
|
383
|
+
return;
|
|
384
|
+
const stats = fs.statSync(filePath);
|
|
385
|
+
if (stats.size > maxBytes) {
|
|
386
|
+
throw new Error(`File too large: ${filePath} (${stats.size} bytes, max ${maxBytes})`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Set secure permissions on a file or directory.
|
|
391
|
+
*/
|
|
392
|
+
setSecurePermissions(targetPath, isDirectory = false) {
|
|
393
|
+
try {
|
|
394
|
+
const mode = isDirectory ? 0o700 : 0o600;
|
|
395
|
+
fs.chmodSync(targetPath, mode);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Some platforms may not support chmod — log but don't fail
|
|
399
|
+
console.error(`Warning: Could not set permissions on ${targetPath}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// ── Constants Accessors ─────────────────────────────────────────────────
|
|
403
|
+
get maxMemoryEntries() { return limits_1.MAX_MEMORIES_TOTAL; }
|
|
404
|
+
get maxInputLength() { return limits_1.MAX_MEMORY_CONTENT_LENGTH; }
|
|
405
|
+
get maxQueryLength() { return limits_1.MAX_QUERY_LENGTH; }
|
|
406
|
+
get maxFileSizeBytes() { return limits_1.MAX_INGEST_FILE_SIZE; }
|
|
407
|
+
}
|
|
408
|
+
exports.SecurityManager = SecurityManager;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imayuur/contexthub-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core ContextHub engine — encrypted memory and security",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/iMayuuR/contexthub.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!dist/__tests__"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"test": "tsc && node --test dist/__tests__/*.test.js",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@imayuur/contexthub-shared-types": "^1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^18.0.0",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"author": "Mayur Dattatray Patil",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/iMayuuR/contexthub/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/iMayuuR/contexthub#readme",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"contexthub",
|
|
43
|
+
"mcp",
|
|
44
|
+
"ai-memory",
|
|
45
|
+
"cursor",
|
|
46
|
+
"claude"
|
|
47
|
+
],
|
|
48
|
+
"exports": {
|
|
49
|
+
".": {
|
|
50
|
+
"types": "./dist/index.d.ts",
|
|
51
|
+
"default": "./dist/index.js"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|