ctx-sync 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.
Files changed (145) hide show
  1. package/dist/commands/audit.d.ts +76 -0
  2. package/dist/commands/audit.d.ts.map +1 -0
  3. package/dist/commands/audit.js +367 -0
  4. package/dist/commands/audit.js.map +1 -0
  5. package/dist/commands/config.d.ts +58 -0
  6. package/dist/commands/config.d.ts.map +1 -0
  7. package/dist/commands/config.js +114 -0
  8. package/dist/commands/config.js.map +1 -0
  9. package/dist/commands/dir.d.ts +56 -0
  10. package/dist/commands/dir.d.ts.map +1 -0
  11. package/dist/commands/dir.js +172 -0
  12. package/dist/commands/dir.js.map +1 -0
  13. package/dist/commands/docker.d.ts +140 -0
  14. package/dist/commands/docker.d.ts.map +1 -0
  15. package/dist/commands/docker.js +380 -0
  16. package/dist/commands/docker.js.map +1 -0
  17. package/dist/commands/env.d.ts +96 -0
  18. package/dist/commands/env.d.ts.map +1 -0
  19. package/dist/commands/env.js +352 -0
  20. package/dist/commands/env.js.map +1 -0
  21. package/dist/commands/init.d.ts +89 -0
  22. package/dist/commands/init.d.ts.map +1 -0
  23. package/dist/commands/init.js +272 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/key.d.ts +92 -0
  26. package/dist/commands/key.d.ts.map +1 -0
  27. package/dist/commands/key.js +274 -0
  28. package/dist/commands/key.js.map +1 -0
  29. package/dist/commands/list.d.ts +38 -0
  30. package/dist/commands/list.d.ts.map +1 -0
  31. package/dist/commands/list.js +84 -0
  32. package/dist/commands/list.js.map +1 -0
  33. package/dist/commands/note.d.ts +151 -0
  34. package/dist/commands/note.d.ts.map +1 -0
  35. package/dist/commands/note.js +411 -0
  36. package/dist/commands/note.js.map +1 -0
  37. package/dist/commands/pull.d.ts +47 -0
  38. package/dist/commands/pull.d.ts.map +1 -0
  39. package/dist/commands/pull.js +94 -0
  40. package/dist/commands/pull.js.map +1 -0
  41. package/dist/commands/push.d.ts +40 -0
  42. package/dist/commands/push.d.ts.map +1 -0
  43. package/dist/commands/push.js +94 -0
  44. package/dist/commands/push.js.map +1 -0
  45. package/dist/commands/restore.d.ts +116 -0
  46. package/dist/commands/restore.d.ts.map +1 -0
  47. package/dist/commands/restore.js +336 -0
  48. package/dist/commands/restore.js.map +1 -0
  49. package/dist/commands/service.d.ts +83 -0
  50. package/dist/commands/service.d.ts.map +1 -0
  51. package/dist/commands/service.js +259 -0
  52. package/dist/commands/service.js.map +1 -0
  53. package/dist/commands/show.d.ts +63 -0
  54. package/dist/commands/show.d.ts.map +1 -0
  55. package/dist/commands/show.js +243 -0
  56. package/dist/commands/show.js.map +1 -0
  57. package/dist/commands/status.d.ts +53 -0
  58. package/dist/commands/status.d.ts.map +1 -0
  59. package/dist/commands/status.js +150 -0
  60. package/dist/commands/status.js.map +1 -0
  61. package/dist/commands/sync.d.ts +105 -0
  62. package/dist/commands/sync.d.ts.map +1 -0
  63. package/dist/commands/sync.js +243 -0
  64. package/dist/commands/sync.js.map +1 -0
  65. package/dist/commands/team.d.ts +79 -0
  66. package/dist/commands/team.d.ts.map +1 -0
  67. package/dist/commands/team.js +233 -0
  68. package/dist/commands/team.js.map +1 -0
  69. package/dist/commands/track.d.ts +109 -0
  70. package/dist/commands/track.d.ts.map +1 -0
  71. package/dist/commands/track.js +406 -0
  72. package/dist/commands/track.js.map +1 -0
  73. package/dist/core/command-validator.d.ts +100 -0
  74. package/dist/core/command-validator.d.ts.map +1 -0
  75. package/dist/core/command-validator.js +299 -0
  76. package/dist/core/command-validator.js.map +1 -0
  77. package/dist/core/config-store.d.ts +76 -0
  78. package/dist/core/config-store.d.ts.map +1 -0
  79. package/dist/core/config-store.js +148 -0
  80. package/dist/core/config-store.js.map +1 -0
  81. package/dist/core/directories-handler.d.ts +116 -0
  82. package/dist/core/directories-handler.d.ts.map +1 -0
  83. package/dist/core/directories-handler.js +199 -0
  84. package/dist/core/directories-handler.js.map +1 -0
  85. package/dist/core/docker-handler.d.ts +183 -0
  86. package/dist/core/docker-handler.d.ts.map +1 -0
  87. package/dist/core/docker-handler.js +515 -0
  88. package/dist/core/docker-handler.js.map +1 -0
  89. package/dist/core/encryption.d.ts +79 -0
  90. package/dist/core/encryption.d.ts.map +1 -0
  91. package/dist/core/encryption.js +111 -0
  92. package/dist/core/encryption.js.map +1 -0
  93. package/dist/core/env-handler.d.ts +128 -0
  94. package/dist/core/env-handler.d.ts.map +1 -0
  95. package/dist/core/env-handler.js +272 -0
  96. package/dist/core/env-handler.js.map +1 -0
  97. package/dist/core/git-sync.d.ts +88 -0
  98. package/dist/core/git-sync.d.ts.map +1 -0
  99. package/dist/core/git-sync.js +143 -0
  100. package/dist/core/git-sync.js.map +1 -0
  101. package/dist/core/key-store.d.ts +51 -0
  102. package/dist/core/key-store.d.ts.map +1 -0
  103. package/dist/core/key-store.js +108 -0
  104. package/dist/core/key-store.js.map +1 -0
  105. package/dist/core/log-sanitizer.d.ts +72 -0
  106. package/dist/core/log-sanitizer.d.ts.map +1 -0
  107. package/dist/core/log-sanitizer.js +202 -0
  108. package/dist/core/log-sanitizer.js.map +1 -0
  109. package/dist/core/path-validator.d.ts +37 -0
  110. package/dist/core/path-validator.d.ts.map +1 -0
  111. package/dist/core/path-validator.js +127 -0
  112. package/dist/core/path-validator.js.map +1 -0
  113. package/dist/core/recipients.d.ts +99 -0
  114. package/dist/core/recipients.d.ts.map +1 -0
  115. package/dist/core/recipients.js +206 -0
  116. package/dist/core/recipients.js.map +1 -0
  117. package/dist/core/services-handler.d.ts +113 -0
  118. package/dist/core/services-handler.d.ts.map +1 -0
  119. package/dist/core/services-handler.js +176 -0
  120. package/dist/core/services-handler.js.map +1 -0
  121. package/dist/core/state-manager.d.ts +96 -0
  122. package/dist/core/state-manager.d.ts.map +1 -0
  123. package/dist/core/state-manager.js +165 -0
  124. package/dist/core/state-manager.js.map +1 -0
  125. package/dist/core/transport.d.ts +28 -0
  126. package/dist/core/transport.d.ts.map +1 -0
  127. package/dist/core/transport.js +79 -0
  128. package/dist/core/transport.js.map +1 -0
  129. package/dist/index.d.ts +20 -0
  130. package/dist/index.d.ts.map +1 -0
  131. package/dist/index.js +80 -0
  132. package/dist/index.js.map +1 -0
  133. package/dist/types/index.d.ts +5 -0
  134. package/dist/types/index.d.ts.map +1 -0
  135. package/dist/types/index.js +2 -0
  136. package/dist/types/index.js.map +1 -0
  137. package/dist/utils/errors.d.ts +81 -0
  138. package/dist/utils/errors.d.ts.map +1 -0
  139. package/dist/utils/errors.js +191 -0
  140. package/dist/utils/errors.js.map +1 -0
  141. package/dist/utils/secure-memory.d.ts +65 -0
  142. package/dist/utils/secure-memory.d.ts.map +1 -0
  143. package/dist/utils/secure-memory.js +86 -0
  144. package/dist/utils/secure-memory.js.map +1 -0
  145. package/package.json +58 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Command validator module.
3
+ *
4
+ * Validates shell commands for suspicious patterns before execution.
5
+ * Used by the `restore` command to prevent remote code execution (RCE)
6
+ * via a compromised Git repo injecting malicious commands into the
7
+ * encrypted state.
8
+ *
9
+ * **Critical security property:** No command is ever executed without
10
+ * explicit user confirmation. There is no `--yes` or `--no-confirm`
11
+ * flag — command confirmation cannot be bypassed.
12
+ *
13
+ * @module core/command-validator
14
+ */
15
+ /**
16
+ * Suspicious command patterns — each entry is a regex and a description
17
+ * of the threat it detects.
18
+ */
19
+ const SUSPICIOUS_PATTERNS = [
20
+ {
21
+ pattern: /\b(curl|wget)\b.*\|\s*(sh|bash|zsh|ksh|dash|csh)\b/i,
22
+ reason: 'Pipes remote content to a shell — potential remote code execution.',
23
+ },
24
+ {
25
+ pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-[a-zA-Z]*f[a-zA-Z]*r)\b/i,
26
+ reason: 'Recursive force-delete — destructive operation.',
27
+ },
28
+ {
29
+ pattern: /\brm\s+-rf\s+\/\s*$/i,
30
+ reason: 'Attempts to delete root filesystem — catastrophic operation.',
31
+ },
32
+ {
33
+ pattern: /\bnc\b.*-[a-zA-Z]*e\b/i,
34
+ reason: 'Netcat with execute flag — potential reverse shell.',
35
+ },
36
+ {
37
+ pattern: /\bpython[23]?\s+-c\b/i,
38
+ reason: 'Inline Python execution — potential arbitrary code execution.',
39
+ },
40
+ {
41
+ pattern: /\bperl\s+-e\b/i,
42
+ reason: 'Inline Perl execution — potential arbitrary code execution.',
43
+ },
44
+ {
45
+ pattern: /\bruby\s+-e\b/i,
46
+ reason: 'Inline Ruby execution — potential arbitrary code execution.',
47
+ },
48
+ {
49
+ pattern: /\bnode\s+-e\b/i,
50
+ reason: 'Inline Node.js execution — potential arbitrary code execution.',
51
+ },
52
+ {
53
+ pattern: /\$\(.*\)/,
54
+ reason: 'Command substitution — embedded command may execute arbitrary code.',
55
+ },
56
+ {
57
+ pattern: /`[^`]+`/,
58
+ reason: 'Backtick command substitution — embedded command may execute arbitrary code.',
59
+ },
60
+ {
61
+ pattern: /\beval\b/i,
62
+ reason: 'eval — executes arbitrary string as code.',
63
+ },
64
+ {
65
+ pattern: /\bexec\b/i,
66
+ reason: 'exec — replaces the current process with another command.',
67
+ },
68
+ {
69
+ pattern: /\b(bash|sh|zsh)\s+-[a-zA-Z]*c\b/i,
70
+ reason: 'Shell with -c flag — executes inline command string.',
71
+ },
72
+ {
73
+ pattern: /\/dev\/(tcp|udp)\//i,
74
+ reason: 'Bash /dev/tcp or /dev/udp — potential reverse shell.',
75
+ },
76
+ {
77
+ pattern: /\bmkfifo\b/i,
78
+ reason: 'Named pipe creation — often used in reverse shell patterns.',
79
+ },
80
+ {
81
+ pattern: /\bchmod\s+[0-7]*[4-7][0-7]{2}\b/i,
82
+ reason: 'Changing file permissions to world-readable — potential security issue.',
83
+ },
84
+ {
85
+ pattern: /\bchown\b/i,
86
+ reason: 'Changing file ownership — potential privilege escalation.',
87
+ },
88
+ {
89
+ pattern: />\s*\/etc\//i,
90
+ reason: 'Writing to /etc/ — modifying system configuration.',
91
+ },
92
+ {
93
+ pattern: /\bsudo\b/i,
94
+ reason: 'sudo — elevated privilege execution.',
95
+ },
96
+ {
97
+ pattern: /\bsu\s+-?\s*\w/i,
98
+ reason: 'su — switching user context.',
99
+ },
100
+ {
101
+ pattern: /\bcrontab\b/i,
102
+ reason: 'crontab — scheduling persistent tasks.',
103
+ },
104
+ {
105
+ pattern: /&&\s*(curl|wget)\b/i,
106
+ reason: 'Chained remote download — may be part of a multi-stage attack.',
107
+ },
108
+ ];
109
+ /**
110
+ * Validate a shell command for suspicious patterns.
111
+ *
112
+ * Checks the command string against known dangerous patterns. This is a
113
+ * defence-in-depth measure — the primary protection is the mandatory
114
+ * user confirmation before any command executes.
115
+ *
116
+ * @param cmd - The shell command string to validate.
117
+ * @returns Validation result with suspicious flag and reason.
118
+ */
119
+ export function validateCommand(cmd) {
120
+ if (!cmd || typeof cmd !== 'string' || cmd.trim().length === 0) {
121
+ return { suspicious: false, reason: '' };
122
+ }
123
+ const trimmed = cmd.trim();
124
+ for (const { pattern, reason } of SUSPICIOUS_PATTERNS) {
125
+ if (pattern.test(trimmed)) {
126
+ return { suspicious: true, reason };
127
+ }
128
+ }
129
+ return { suspicious: false, reason: '' };
130
+ }
131
+ /**
132
+ * Validate a Docker image name for suspicious patterns.
133
+ *
134
+ * Flags images from unofficial registries or suspicious names.
135
+ * Trusted images come from well-known publishers (e.g. `postgres`,
136
+ * `redis`, `node`) without a registry prefix, or from trusted registries
137
+ * like `docker.io`.
138
+ *
139
+ * @param image - The Docker image name (e.g. 'postgres:15', 'evil.com/malware:latest').
140
+ * @returns Validation result with warning flag and reason.
141
+ */
142
+ export function validateDockerImage(image) {
143
+ if (!image || typeof image !== 'string' || image.trim().length === 0) {
144
+ return { suspicious: false, reason: '' };
145
+ }
146
+ const trimmed = image.trim();
147
+ // Images with a registry prefix that isn't docker.io/library are suspicious
148
+ // Official images: 'postgres:15', 'redis:7-alpine', 'node:20'
149
+ // Suspicious images: 'evil.com/postgres:latest', 'attacker/redis:backdoored'
150
+ const hasSlash = trimmed.includes('/');
151
+ if (hasSlash) {
152
+ // Check if it's from a known official source
153
+ const officialPrefixes = [
154
+ 'docker.io/library/',
155
+ 'docker.io/',
156
+ 'library/',
157
+ 'ghcr.io/',
158
+ 'gcr.io/',
159
+ 'mcr.microsoft.com/',
160
+ 'public.ecr.aws/',
161
+ ];
162
+ const isOfficial = officialPrefixes.some((prefix) => trimmed.startsWith(prefix));
163
+ if (!isOfficial) {
164
+ return {
165
+ suspicious: true,
166
+ reason: `Non-official Docker image registry: ${trimmed}. Verify this image is trusted.`,
167
+ };
168
+ }
169
+ }
170
+ return { suspicious: false, reason: '' };
171
+ }
172
+ /**
173
+ * Format commands for display to the user before execution.
174
+ *
175
+ * Groups commands by category (Docker services, auto-start services)
176
+ * and adds warning indicators for suspicious commands.
177
+ *
178
+ * @param commands - The list of commands pending approval.
179
+ * @returns Formatted string for terminal display.
180
+ */
181
+ export function formatCommandsForDisplay(commands) {
182
+ if (commands.length === 0) {
183
+ return '';
184
+ }
185
+ const lines = [];
186
+ lines.push('┌────────────────────────────────────────────┐');
187
+ // Group by label
188
+ const groups = new Map();
189
+ for (const cmd of commands) {
190
+ const existing = groups.get(cmd.label) ?? [];
191
+ existing.push(cmd);
192
+ groups.set(cmd.label, existing);
193
+ }
194
+ let index = 1;
195
+ for (const [label, groupCommands] of groups) {
196
+ lines.push(`│ ${label}:`);
197
+ for (const cmd of groupCommands) {
198
+ const validation = validateCommand(cmd.command);
199
+ const imageValidation = cmd.image
200
+ ? validateDockerImage(cmd.image)
201
+ : null;
202
+ const prefix = `│ ${index}.`;
203
+ lines.push(`${prefix} ${cmd.command}`);
204
+ if (cmd.image) {
205
+ lines.push(`│ Image: ${cmd.image}`);
206
+ }
207
+ if (cmd.port) {
208
+ lines.push(`│ Port: ${cmd.port}`);
209
+ }
210
+ if (cmd.cwd) {
211
+ lines.push(`│ Working dir: ${cmd.cwd}`);
212
+ }
213
+ if (validation.suspicious) {
214
+ lines.push(`│ ⚠️ WARNING: ${validation.reason}`);
215
+ }
216
+ if (imageValidation?.suspicious) {
217
+ lines.push(`│ ⚠️ WARNING: ${imageValidation.reason}`);
218
+ }
219
+ lines.push('│');
220
+ index++;
221
+ }
222
+ }
223
+ lines.push('│ Review each command carefully! │');
224
+ lines.push('└────────────────────────────────────────────┘');
225
+ return lines.join('\n');
226
+ }
227
+ /**
228
+ * Present commands for user approval.
229
+ *
230
+ * In interactive mode, displays commands and prompts the user to approve
231
+ * all, reject all, or select individually.
232
+ *
233
+ * In non-interactive mode, displays commands but does NOT execute any —
234
+ * this is the safe default.
235
+ *
236
+ * **Security:** There is no `--yes` or `--no-confirm` flag. Command
237
+ * confirmation cannot be bypassed programmatically.
238
+ *
239
+ * @param commands - The list of commands to present.
240
+ * @param options - Display options.
241
+ * @param options.interactive - Whether to prompt for approval (false = show only).
242
+ * @param options.promptFn - Optional override for the approval prompt (for testing).
243
+ * @returns The approval result (which commands were approved/rejected).
244
+ */
245
+ export async function presentCommandsForApproval(commands, options = {}) {
246
+ const result = {
247
+ approved: [],
248
+ rejected: [],
249
+ skippedAll: false,
250
+ };
251
+ if (commands.length === 0) {
252
+ return result;
253
+ }
254
+ // Non-interactive mode: show commands but skip execution
255
+ if (!options.interactive) {
256
+ result.skippedAll = true;
257
+ result.rejected = [...commands];
258
+ return result;
259
+ }
260
+ // Interactive mode: prompt for approval
261
+ const promptFn = options.promptFn;
262
+ if (!promptFn) {
263
+ // Default: skip all if no prompt function (safety fallback)
264
+ result.skippedAll = true;
265
+ result.rejected = [...commands];
266
+ return result;
267
+ }
268
+ const choice = await promptFn(commands);
269
+ switch (choice) {
270
+ case 'all':
271
+ result.approved = [...commands];
272
+ break;
273
+ case 'none':
274
+ result.rejected = [...commands];
275
+ break;
276
+ case 'select': {
277
+ const selectFn = options.selectFn;
278
+ if (!selectFn) {
279
+ result.rejected = [...commands];
280
+ break;
281
+ }
282
+ for (let i = 0; i < commands.length; i++) {
283
+ const cmd = commands[i];
284
+ if (!cmd)
285
+ continue;
286
+ const approved = await selectFn(cmd, i + 1);
287
+ if (approved) {
288
+ result.approved.push(cmd);
289
+ }
290
+ else {
291
+ result.rejected.push(cmd);
292
+ }
293
+ }
294
+ break;
295
+ }
296
+ }
297
+ return result;
298
+ }
299
+ //# sourceMappingURL=command-validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-validator.js","sourceRoot":"","sources":["../../src/core/command-validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Local user configuration store.
3
+ *
4
+ * Manages `config.json` in the config directory (~/.config/ctx-sync/).
5
+ * This file is NEVER synced to Git — it holds local preferences only.
6
+ *
7
+ * Primary use: custom safe-list additions that merge with DEFAULT_SAFE_LIST.
8
+ *
9
+ * @module core/config-store
10
+ */
11
+ import type { UserConfig } from '@ctx-sync/shared';
12
+ /** Config file name (stored in config dir, never synced) */
13
+ export declare const CONFIG_FILE = "config.json";
14
+ /**
15
+ * Load the user config from disk.
16
+ *
17
+ * @param configDir - The config directory path (~/.config/ctx-sync).
18
+ * @returns The parsed UserConfig, or `null` if the file does not exist.
19
+ */
20
+ export declare function getUserConfig(configDir: string): UserConfig | null;
21
+ /**
22
+ * Save the user config to disk.
23
+ *
24
+ * @param configDir - The config directory path.
25
+ * @param config - The UserConfig to persist.
26
+ */
27
+ export declare function saveUserConfig(configDir: string, config: UserConfig): void;
28
+ /**
29
+ * Get the effective safe-list: DEFAULT_SAFE_LIST merged with user additions.
30
+ *
31
+ * User keys are uppercased and deduplicated against the defaults.
32
+ *
33
+ * @param configDir - The config directory path.
34
+ * @returns The merged safe-list (all uppercase).
35
+ */
36
+ export declare function getEffectiveSafeList(configDir: string): string[];
37
+ /**
38
+ * Add a key to the user's custom safe-list.
39
+ *
40
+ * The key is normalised to uppercase. Duplicate additions (already in
41
+ * default or custom list) are detected and reported.
42
+ *
43
+ * @param configDir - The config directory path.
44
+ * @param key - The env var key to add.
45
+ * @returns An object indicating whether the key was added and a message.
46
+ */
47
+ export declare function addToSafeList(configDir: string, key: string): {
48
+ added: boolean;
49
+ message: string;
50
+ };
51
+ /**
52
+ * Remove a key from the user's custom safe-list.
53
+ *
54
+ * Keys that are part of DEFAULT_SAFE_LIST cannot be removed (they are
55
+ * built-in). Only user-added custom keys can be removed.
56
+ *
57
+ * @param configDir - The config directory path.
58
+ * @param key - The env var key to remove.
59
+ * @returns An object indicating whether the key was removed and a message.
60
+ */
61
+ export declare function removeFromSafeList(configDir: string, key: string): {
62
+ removed: boolean;
63
+ message: string;
64
+ };
65
+ /**
66
+ * List the current safe-list, split into defaults and custom additions.
67
+ *
68
+ * @param configDir - The config directory path.
69
+ * @returns Object with default keys, custom keys, and the full effective list.
70
+ */
71
+ export declare function listSafeList(configDir: string): {
72
+ defaults: readonly string[];
73
+ custom: string[];
74
+ effective: string[];
75
+ };
76
+ //# sourceMappingURL=config-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../../src/core/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,4DAA4D;AAC5D,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAEzC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAclE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAS1E;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAWhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4BrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA8BvC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG;IAC/C,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CASA"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Local user configuration store.
3
+ *
4
+ * Manages `config.json` in the config directory (~/.config/ctx-sync/).
5
+ * This file is NEVER synced to Git — it holds local preferences only.
6
+ *
7
+ * Primary use: custom safe-list additions that merge with DEFAULT_SAFE_LIST.
8
+ *
9
+ * @module core/config-store
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { DEFAULT_SAFE_LIST } from '@ctx-sync/shared';
14
+ /** Config file name (stored in config dir, never synced) */
15
+ export const CONFIG_FILE = 'config.json';
16
+ /**
17
+ * Load the user config from disk.
18
+ *
19
+ * @param configDir - The config directory path (~/.config/ctx-sync).
20
+ * @returns The parsed UserConfig, or `null` if the file does not exist.
21
+ */
22
+ export function getUserConfig(configDir) {
23
+ const filePath = path.join(configDir, CONFIG_FILE);
24
+ if (!fs.existsSync(filePath)) {
25
+ return null;
26
+ }
27
+ const content = fs.readFileSync(filePath, 'utf-8');
28
+ if (!content.trim()) {
29
+ return null;
30
+ }
31
+ return JSON.parse(content);
32
+ }
33
+ /**
34
+ * Save the user config to disk.
35
+ *
36
+ * @param configDir - The config directory path.
37
+ * @param config - The UserConfig to persist.
38
+ */
39
+ export function saveUserConfig(configDir, config) {
40
+ const filePath = path.join(configDir, CONFIG_FILE);
41
+ // Ensure the config directory exists
42
+ if (!fs.existsSync(configDir)) {
43
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
44
+ }
45
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
46
+ }
47
+ /**
48
+ * Get the effective safe-list: DEFAULT_SAFE_LIST merged with user additions.
49
+ *
50
+ * User keys are uppercased and deduplicated against the defaults.
51
+ *
52
+ * @param configDir - The config directory path.
53
+ * @returns The merged safe-list (all uppercase).
54
+ */
55
+ export function getEffectiveSafeList(configDir) {
56
+ const config = getUserConfig(configDir);
57
+ const userKeys = config?.safeList ?? [];
58
+ // Merge: defaults + user additions (uppercased, deduplicated)
59
+ const merged = new Set(DEFAULT_SAFE_LIST);
60
+ for (const key of userKeys) {
61
+ merged.add(key.toUpperCase());
62
+ }
63
+ return [...merged];
64
+ }
65
+ /**
66
+ * Add a key to the user's custom safe-list.
67
+ *
68
+ * The key is normalised to uppercase. Duplicate additions (already in
69
+ * default or custom list) are detected and reported.
70
+ *
71
+ * @param configDir - The config directory path.
72
+ * @param key - The env var key to add.
73
+ * @returns An object indicating whether the key was added and a message.
74
+ */
75
+ export function addToSafeList(configDir, key) {
76
+ const normKey = key.toUpperCase();
77
+ // Check if already in default safe-list
78
+ if (DEFAULT_SAFE_LIST.includes(normKey)) {
79
+ return {
80
+ added: false,
81
+ message: `${normKey} is already in the default safe-list.`,
82
+ };
83
+ }
84
+ const config = getUserConfig(configDir) ?? {};
85
+ const existing = (config.safeList ?? []).map((k) => k.toUpperCase());
86
+ if (existing.includes(normKey)) {
87
+ return {
88
+ added: false,
89
+ message: `${normKey} is already in your custom safe-list.`,
90
+ };
91
+ }
92
+ config.safeList = [...(config.safeList ?? []), normKey];
93
+ saveUserConfig(configDir, config);
94
+ return {
95
+ added: true,
96
+ message: `Added ${normKey} to the safe-list.`,
97
+ };
98
+ }
99
+ /**
100
+ * Remove a key from the user's custom safe-list.
101
+ *
102
+ * Keys that are part of DEFAULT_SAFE_LIST cannot be removed (they are
103
+ * built-in). Only user-added custom keys can be removed.
104
+ *
105
+ * @param configDir - The config directory path.
106
+ * @param key - The env var key to remove.
107
+ * @returns An object indicating whether the key was removed and a message.
108
+ */
109
+ export function removeFromSafeList(configDir, key) {
110
+ const normKey = key.toUpperCase();
111
+ // Cannot remove default keys
112
+ if (DEFAULT_SAFE_LIST.includes(normKey)) {
113
+ return {
114
+ removed: false,
115
+ message: `${normKey} is a built-in default and cannot be removed. It will always be on the safe-list.`,
116
+ };
117
+ }
118
+ const config = getUserConfig(configDir) ?? {};
119
+ const existing = (config.safeList ?? []).map((k) => k.toUpperCase());
120
+ if (!existing.includes(normKey)) {
121
+ return {
122
+ removed: false,
123
+ message: `${normKey} is not in your custom safe-list.`,
124
+ };
125
+ }
126
+ config.safeList = (config.safeList ?? []).filter((k) => k.toUpperCase() !== normKey);
127
+ saveUserConfig(configDir, config);
128
+ return {
129
+ removed: true,
130
+ message: `Removed ${normKey} from the safe-list. It will be encrypted on next import.`,
131
+ };
132
+ }
133
+ /**
134
+ * List the current safe-list, split into defaults and custom additions.
135
+ *
136
+ * @param configDir - The config directory path.
137
+ * @returns Object with default keys, custom keys, and the full effective list.
138
+ */
139
+ export function listSafeList(configDir) {
140
+ const config = getUserConfig(configDir);
141
+ const custom = (config?.safeList ?? []).map((k) => k.toUpperCase());
142
+ return {
143
+ defaults: DEFAULT_SAFE_LIST,
144
+ custom,
145
+ effective: getEffectiveSafeList(configDir),
146
+ };
147
+ }
148
+ //# sourceMappingURL=config-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.js","sourceRoot":"","sources":["../../src/core/config-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Directories handler module.
3
+ *
4
+ * Tracks recent working directories and pinned directories associated
5
+ * with a user's development workflow. State is persisted in
6
+ * `directories.age` (encrypted).
7
+ *
8
+ * **Path validation:** All directory paths are validated through the
9
+ * path-validator module to prevent path traversal attacks.
10
+ *
11
+ * @module core/directories-handler
12
+ */
13
+ import type { RecentDirectory, DirectoryState } from '@ctx-sync/shared';
14
+ /**
15
+ * Maximum number of recent directories to retain.
16
+ * Older, less-frequent entries are pruned when this limit is exceeded.
17
+ */
18
+ export declare const MAX_RECENT_DIRS = 50;
19
+ /**
20
+ * Load directory state from encrypted storage.
21
+ *
22
+ * @param syncDir - The sync directory (e.g. ~/.context-sync).
23
+ * @param privateKey - Age private key for decryption.
24
+ * @returns The decrypted `DirectoryState`, or an empty state if the
25
+ * file does not exist.
26
+ */
27
+ export declare function loadDirectories(syncDir: string, privateKey: string): Promise<DirectoryState>;
28
+ /**
29
+ * Save (overwrite) the entire directory state.
30
+ *
31
+ * @param syncDir - The sync directory.
32
+ * @param state - The complete `DirectoryState` to persist.
33
+ * @param publicKey - Age public key for encryption.
34
+ */
35
+ export declare function saveDirectories(syncDir: string, state: DirectoryState, publicKey: string): Promise<void>;
36
+ /**
37
+ * Record a directory visit.
38
+ *
39
+ * - If the directory already exists in the recent list, its frequency is
40
+ * incremented and its lastVisit timestamp updated.
41
+ * - If new, it is added with frequency 1.
42
+ * - The list is pruned to `MAX_RECENT_DIRS` after insertion.
43
+ * - The path is validated against the path-validator to reject traversal
44
+ * attacks.
45
+ *
46
+ * @param syncDir - The sync directory.
47
+ * @param dirPath - The directory path to record (will be validated).
48
+ * @param publicKey - Age public key for encryption.
49
+ * @param privateKey - Age private key for decryption.
50
+ * @throws If the path fails validation.
51
+ */
52
+ export declare function visitDirectory(syncDir: string, dirPath: string, publicKey: string, privateKey: string): Promise<void>;
53
+ /**
54
+ * Pin a directory.
55
+ *
56
+ * Pinned directories are always shown at the top of directory listings
57
+ * and are not subject to the recent-directory pruning limit.
58
+ *
59
+ * @param syncDir - The sync directory.
60
+ * @param dirPath - The directory path to pin (will be validated).
61
+ * @param publicKey - Age public key for encryption.
62
+ * @param privateKey - Age private key for decryption.
63
+ * @returns `true` if the directory was newly pinned, `false` if already pinned.
64
+ * @throws If the path fails validation.
65
+ */
66
+ export declare function pinDirectory(syncDir: string, dirPath: string, publicKey: string, privateKey: string): Promise<boolean>;
67
+ /**
68
+ * Unpin a directory.
69
+ *
70
+ * @param syncDir - The sync directory.
71
+ * @param dirPath - The directory path to unpin (will be validated).
72
+ * @param publicKey - Age public key for encryption.
73
+ * @param privateKey - Age private key for decryption.
74
+ * @returns `true` if the directory was unpinned, `false` if it was not pinned.
75
+ * @throws If the path fails validation.
76
+ */
77
+ export declare function unpinDirectory(syncDir: string, dirPath: string, publicKey: string, privateKey: string): Promise<boolean>;
78
+ /**
79
+ * Remove a directory from the recent list (does not affect pinned).
80
+ *
81
+ * @param syncDir - The sync directory.
82
+ * @param dirPath - The directory path to remove.
83
+ * @param publicKey - Age public key for encryption.
84
+ * @param privateKey - Age private key for decryption.
85
+ * @returns `true` if the directory was removed, `false` if not found.
86
+ */
87
+ export declare function removeRecentDirectory(syncDir: string, dirPath: string, publicKey: string, privateKey: string): Promise<boolean>;
88
+ /**
89
+ * Get the most-visited recent directories.
90
+ *
91
+ * @param syncDir - The sync directory.
92
+ * @param privateKey - Age private key.
93
+ * @param limit - Maximum number to return (default: 10).
94
+ * @returns Sorted recent directories (most-visited first).
95
+ */
96
+ export declare function getTopDirectories(syncDir: string, privateKey: string, limit?: number): Promise<RecentDirectory[]>;
97
+ /**
98
+ * Get all pinned directories.
99
+ *
100
+ * @param syncDir - The sync directory.
101
+ * @param privateKey - Age private key.
102
+ * @returns Array of pinned directory paths.
103
+ */
104
+ export declare function getPinnedDirectories(syncDir: string, privateKey: string): Promise<string[]>;
105
+ /**
106
+ * Validate a directory path for use in directory state.
107
+ *
108
+ * Thin wrapper around `validateProjectPath` exposed for external callers
109
+ * who want to validate before calling other functions.
110
+ *
111
+ * @param dirPath - The directory path to validate.
112
+ * @returns The canonicalised, validated path.
113
+ * @throws If the path is invalid.
114
+ */
115
+ export declare function validateDirectoryPath(dirPath: string): string;
116
+ //# sourceMappingURL=directories-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directories-handler.d.ts","sourceRoot":"","sources":["../../src/core/directories-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMxE;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAwBlC;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,cAAc,CAAC,CAOzB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,SAAK,GACT,OAAO,CAAC,eAAe,EAAE,CAAC,CAG5B;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7D"}