@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.
@@ -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
+ }
@@ -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
+ }