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,272 @@
1
+ /**
2
+ * `ctx-sync init` command.
3
+ *
4
+ * Handles first-time setup (key generation, Git repo init, remote config)
5
+ * and `--restore` flow (key restoration, repo clone, state decryption).
6
+ *
7
+ * @module commands/init
8
+ */
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+ import { VERSION, SYNC_DIR, CONFIG_DIR, STATE_FILES } from '@ctx-sync/shared';
13
+ import { generateKey } from '../core/encryption.js';
14
+ import { decryptState } from '../core/encryption.js';
15
+ import { saveKey, loadKey } from '../core/key-store.js';
16
+ import { initRepo, addRemote, commitState } from '../core/git-sync.js';
17
+ import { validateRemoteUrl } from '../core/transport.js';
18
+ import { withErrorHandler } from '../utils/errors.js';
19
+ /**
20
+ * Get the config directory path.
21
+ * Uses CTX_SYNC_HOME env var for testing, otherwise ~/.config/ctx-sync.
22
+ */
23
+ export function getConfigDir() {
24
+ const home = process.env['CTX_SYNC_HOME'] ?? os.homedir();
25
+ return path.join(home, '.config', CONFIG_DIR);
26
+ }
27
+ /**
28
+ * Get the sync directory path.
29
+ * Uses CTX_SYNC_HOME env var for testing, otherwise ~/.context-sync.
30
+ */
31
+ export function getSyncDir() {
32
+ const home = process.env['CTX_SYNC_HOME'] ?? os.homedir();
33
+ return path.join(home, SYNC_DIR);
34
+ }
35
+ /**
36
+ * Create the initial manifest.json in the sync directory.
37
+ *
38
+ * The manifest is the only plaintext file in the sync repo.
39
+ * It contains only version and timestamps — no sensitive data.
40
+ */
41
+ export function createManifest(syncDir) {
42
+ const now = new Date().toISOString();
43
+ const manifest = {
44
+ version: VERSION,
45
+ lastSync: now,
46
+ files: {},
47
+ };
48
+ fs.writeFileSync(path.join(syncDir, STATE_FILES.MANIFEST), JSON.stringify(manifest, null, 2));
49
+ return manifest;
50
+ }
51
+ /**
52
+ * Execute the fresh init flow.
53
+ *
54
+ * 1. Generate Age key pair.
55
+ * 2. Save private key to config dir (0o600).
56
+ * 3. Display public key.
57
+ * 4. Handle backup (skip in --no-interactive).
58
+ * 5. Prompt for Git remote URL (or use --remote).
59
+ * 6. Validate remote URL.
60
+ * 7. Initialize sync Git repo.
61
+ * 8. Add remote.
62
+ * 9. Create manifest.json.
63
+ * 10. Commit and push.
64
+ */
65
+ export async function executeInit(options) {
66
+ const configDir = getConfigDir();
67
+ const syncDir = getSyncDir();
68
+ // 1. Generate key pair
69
+ const { publicKey, privateKey } = await generateKey();
70
+ // 2. Save private key
71
+ saveKey(configDir, privateKey);
72
+ // 3-4. Display key info (handled by CLI output in registerInitCommand)
73
+ // 5-6. Remote URL
74
+ let remoteUrl;
75
+ if (options.remote) {
76
+ validateRemoteUrl(options.remote);
77
+ remoteUrl = options.remote;
78
+ }
79
+ // 7. Init sync repo
80
+ await initRepo(syncDir);
81
+ // 8. Add remote if provided
82
+ if (remoteUrl) {
83
+ await addRemote(syncDir, remoteUrl);
84
+ }
85
+ // 9. Create manifest
86
+ createManifest(syncDir);
87
+ // 10. Commit
88
+ await commitState(syncDir, [STATE_FILES.MANIFEST], 'chore: initialize context sync');
89
+ return {
90
+ publicKey,
91
+ configDir,
92
+ syncDir,
93
+ remoteUrl,
94
+ manifestCreated: true,
95
+ };
96
+ }
97
+ /**
98
+ * Execute the restore flow.
99
+ *
100
+ * 1. Accept private key (from --stdin or prompt).
101
+ * 2. Save key with 0o600 permissions.
102
+ * 3. Prompt for Git remote URL (or use --remote).
103
+ * 4. Validate remote URL.
104
+ * 5. Clone the sync repo to ~/.context-sync/.
105
+ * 6. Decrypt manifest, list found projects.
106
+ * 7. Print summary.
107
+ */
108
+ export async function executeRestore(options) {
109
+ const configDir = getConfigDir();
110
+ const syncDir = getSyncDir();
111
+ // 1-2. Save provided key
112
+ if (!options.key) {
113
+ throw new Error('Private key is required for restore.\n' +
114
+ 'Use --stdin to pipe the key, or run without --no-interactive for a prompt.');
115
+ }
116
+ // Validate key format
117
+ const trimmedKey = options.key.trim();
118
+ if (!trimmedKey.startsWith('AGE-SECRET-KEY-')) {
119
+ throw new Error('Invalid private key format. Expected key starting with AGE-SECRET-KEY-.\n' +
120
+ 'Check your backup and try again.');
121
+ }
122
+ saveKey(configDir, trimmedKey);
123
+ // 3-4. Remote URL
124
+ let remoteUrl;
125
+ if (options.remote) {
126
+ validateRemoteUrl(options.remote);
127
+ remoteUrl = options.remote;
128
+ }
129
+ // 5. Clone or init repo
130
+ if (remoteUrl) {
131
+ // Clone via simple-git
132
+ const { simpleGit } = await import('simple-git');
133
+ const git = simpleGit();
134
+ await git.clone(remoteUrl, syncDir);
135
+ }
136
+ else {
137
+ // Just init locally (no remote to clone from)
138
+ await initRepo(syncDir);
139
+ }
140
+ // 6. Try to decrypt state and list projects
141
+ let projectCount = 0;
142
+ const projectNames = [];
143
+ const stateFile = path.join(syncDir, STATE_FILES.STATE);
144
+ if (fs.existsSync(stateFile)) {
145
+ try {
146
+ const privateKey = loadKey(configDir);
147
+ const ciphertext = fs.readFileSync(stateFile, 'utf-8');
148
+ const state = await decryptState(ciphertext, privateKey);
149
+ if (state.projects && Array.isArray(state.projects)) {
150
+ projectCount = state.projects.length;
151
+ for (const project of state.projects) {
152
+ if (project.name) {
153
+ projectNames.push(project.name);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ catch {
159
+ // Decryption failed — wrong key or no state yet
160
+ // This is not fatal for restore setup
161
+ }
162
+ }
163
+ return {
164
+ configDir,
165
+ syncDir,
166
+ remoteUrl,
167
+ projectCount,
168
+ projectNames,
169
+ };
170
+ }
171
+ /**
172
+ * Read key from stdin (for piped input).
173
+ */
174
+ export function readKeyFromStdin() {
175
+ return new Promise((resolve, reject) => {
176
+ let data = '';
177
+ if (process.stdin.isTTY) {
178
+ reject(new Error('No data piped to stdin. Use: echo "AGE-SECRET-KEY-..." | ctx-sync init --restore --stdin'));
179
+ return;
180
+ }
181
+ process.stdin.setEncoding('utf-8');
182
+ process.stdin.on('data', (chunk) => {
183
+ data += chunk;
184
+ });
185
+ process.stdin.on('end', () => {
186
+ resolve(data.trim());
187
+ });
188
+ process.stdin.on('error', reject);
189
+ });
190
+ }
191
+ /**
192
+ * Register the `init` command on the given Commander program.
193
+ */
194
+ export function registerInitCommand(program) {
195
+ program
196
+ .command('init')
197
+ .description('Initialize ctx-sync (generate keys, set up sync repo)')
198
+ .option('--restore', 'Restore from backup on a new machine')
199
+ .option('--no-interactive', 'Skip interactive prompts (use defaults)')
200
+ .option('--skip-backup', 'Skip key backup prompt (not recommended)')
201
+ .option('--remote <url>', 'Git remote URL for syncing')
202
+ .option('--stdin', 'Read private key from stdin (for --restore)')
203
+ .action(withErrorHandler(async (opts) => {
204
+ const options = {
205
+ restore: opts['restore'],
206
+ noInteractive: opts['interactive'] === false,
207
+ skipBackup: opts['skipBackup'],
208
+ remote: opts['remote'],
209
+ stdin: opts['stdin'],
210
+ };
211
+ if (options.restore) {
212
+ // Restore flow
213
+ if (options.stdin) {
214
+ options.key = await readKeyFromStdin();
215
+ }
216
+ else if (!options.noInteractive) {
217
+ // Interactive prompt for key
218
+ const Enquirer = (await import('enquirer')).default;
219
+ const enquirer = new Enquirer();
220
+ const response = await enquirer.prompt({
221
+ type: 'password',
222
+ name: 'key',
223
+ message: 'Paste your private key (AGE-SECRET-KEY-...):',
224
+ });
225
+ options.key = response.key;
226
+ }
227
+ else {
228
+ throw new Error('Private key is required for restore.\n' +
229
+ 'Use --stdin to pipe the key, or run without --no-interactive for a prompt.');
230
+ }
231
+ const result = await executeRestore(options);
232
+ const chalk = (await import('chalk')).default;
233
+ console.log(chalk.green('✅ Key restored') + ' (permissions set to 600)');
234
+ console.log(`📂 Sync directory: ${result.syncDir}`);
235
+ if (result.remoteUrl) {
236
+ console.log(chalk.green('✅ Remote configured:') + ` ${result.remoteUrl}`);
237
+ }
238
+ if (result.projectCount > 0) {
239
+ console.log(chalk.green(`✅ Found ${result.projectCount} projects:`));
240
+ for (const name of result.projectNames) {
241
+ console.log(` - ${name}`);
242
+ }
243
+ }
244
+ else {
245
+ console.log('No existing projects found (sync repo may be empty).');
246
+ }
247
+ console.log('\nAll state decrypted! 🎉');
248
+ }
249
+ else {
250
+ // Fresh init flow
251
+ const result = await executeInit(options);
252
+ const chalk = (await import('chalk')).default;
253
+ console.log('\n🔐 Generating encryption key...');
254
+ console.log(chalk.green('✅ Public key: ') + result.publicKey);
255
+ console.log(chalk.green('✅ Private key saved to: ') +
256
+ path.join(result.configDir, 'key.txt'));
257
+ console.log(' Permissions: 600 (owner read/write only)');
258
+ if (!options.skipBackup && !options.noInteractive) {
259
+ console.log(chalk.yellow('\n⚠️ IMPORTANT: Back up your private key NOW!'));
260
+ console.log('Save it to 1Password, Bitwarden, or another password manager.');
261
+ }
262
+ if (result.remoteUrl) {
263
+ console.log(chalk.green('\n✅ Remote configured:') + ` ${result.remoteUrl}`);
264
+ }
265
+ console.log(chalk.green('\n✅ All set!'));
266
+ console.log('\nNow track your first project:');
267
+ console.log(' $ cd ~/projects/my-app');
268
+ console.log(' $ ctx-sync track');
269
+ }
270
+ }));
271
+ }
272
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `ctx-sync key` command group.
3
+ *
4
+ * Manages encryption key lifecycle:
5
+ * - `key show` — display public key (NEVER the private key).
6
+ * - `key verify` — check key file and config directory permissions.
7
+ * - `key rotate` — generate new key, re-encrypt all state, rewrite Git history.
8
+ * - `key update` — restore a rotated key from another machine (stdin/prompt).
9
+ *
10
+ * **Security:**
11
+ * - `key show` never outputs the private key.
12
+ * - `key rotate` rewrites Git history so old encrypted blobs are purged.
13
+ * - `key update` reads the new private key from stdin, never from CLI args.
14
+ *
15
+ * @module commands/key
16
+ */
17
+ import type { Command } from 'commander';
18
+ /** Result of key show */
19
+ export interface KeyShowResult {
20
+ publicKey: string;
21
+ }
22
+ /** Result of key verify */
23
+ export interface KeyVerifyResult {
24
+ valid: boolean;
25
+ keyFileExists: boolean;
26
+ keyFilePerms: number | null;
27
+ configDirPerms: number | null;
28
+ issues: string[];
29
+ }
30
+ /** Options for key rotate */
31
+ export interface KeyRotateOptions {
32
+ /** Skip interactive prompts (for testing / non-interactive mode) */
33
+ noInteractive?: boolean;
34
+ /** Skip force-push to remote (for testing / local-only setups) */
35
+ noForcePush?: boolean;
36
+ }
37
+ /** Result of key rotate */
38
+ export interface KeyRotateResult {
39
+ oldPublicKey: string;
40
+ newPublicKey: string;
41
+ filesReEncrypted: string[];
42
+ gitHistoryRewritten: boolean;
43
+ }
44
+ /** Options for key update */
45
+ export interface KeyUpdateOptions {
46
+ /** Read key from stdin instead of interactive prompt */
47
+ stdin?: boolean;
48
+ /** Override key input for testing */
49
+ keyInput?: string;
50
+ }
51
+ /** Result of key update */
52
+ export interface KeyUpdateResult {
53
+ publicKey: string;
54
+ configDir: string;
55
+ }
56
+ /**
57
+ * Execute `ctx-sync key show`.
58
+ *
59
+ * Loads the private key and derives the public key from it.
60
+ * NEVER outputs or returns the private key.
61
+ */
62
+ export declare function executeKeyShow(): Promise<KeyShowResult>;
63
+ /**
64
+ * Execute `ctx-sync key verify`.
65
+ *
66
+ * Checks that the key file and config directory have correct permissions.
67
+ */
68
+ export declare function executeKeyVerify(): KeyVerifyResult;
69
+ /**
70
+ * Execute `ctx-sync key rotate`.
71
+ *
72
+ * 1. Generate a new key pair.
73
+ * 2. Decrypt ALL .age files with the old key.
74
+ * 3. Re-encrypt ALL with the new key.
75
+ * 4. Save the new private key (0o600).
76
+ * 5. Rewrite Git history to remove old encrypted blobs.
77
+ * 6. Optionally force-push to remote.
78
+ * 7. Return result for display.
79
+ */
80
+ export declare function executeKeyRotate(_options?: KeyRotateOptions): Promise<KeyRotateResult>;
81
+ /**
82
+ * Execute `ctx-sync key update`.
83
+ *
84
+ * Prompts for (or reads from stdin) a new private key, validates it,
85
+ * and saves it with correct permissions.
86
+ */
87
+ export declare function executeKeyUpdate(options?: KeyUpdateOptions): Promise<KeyUpdateResult>;
88
+ /**
89
+ * Register the `ctx-sync key` command group on the given program.
90
+ */
91
+ export declare function registerKeyCommand(program: Command): void;
92
+ //# sourceMappingURL=key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/commands/key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC,yBAAyB;AACzB,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2BAA2B;AAC3B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,6BAA6B;AAC7B,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kEAAkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,2BAA2B;AAC3B,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,6BAA6B;AAC7B,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2BAA2B;AAC3B,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;;GAKG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CAK7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,eAAe,CAGlD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,GAAE,gBAAqB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAgDD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CA+B1B;AAuCD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwEzD"}
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `ctx-sync key` command group.
3
+ *
4
+ * Manages encryption key lifecycle:
5
+ * - `key show` — display public key (NEVER the private key).
6
+ * - `key verify` — check key file and config directory permissions.
7
+ * - `key rotate` — generate new key, re-encrypt all state, rewrite Git history.
8
+ * - `key update` — restore a rotated key from another machine (stdin/prompt).
9
+ *
10
+ * **Security:**
11
+ * - `key show` never outputs the private key.
12
+ * - `key rotate` rewrites Git history so old encrypted blobs are purged.
13
+ * - `key update` reads the new private key from stdin, never from CLI args.
14
+ *
15
+ * @module commands/key
16
+ */
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import * as readline from 'node:readline';
20
+ import { withErrorHandler } from '../utils/errors.js';
21
+ import { identityToRecipient } from 'age-encryption';
22
+ import { generateKey, decryptState, encryptState } from '../core/encryption.js';
23
+ import { saveKey, loadKey, verifyPermissions, KEY_FILE_PERMS, } from '../core/key-store.js';
24
+ import { listStateFiles, readManifest, writeManifest, } from '../core/state-manager.js';
25
+ import { getConfigDir, getSyncDir } from './init.js';
26
+ // ─── Core Logic ───────────────────────────────────────────────────────────
27
+ /**
28
+ * Execute `ctx-sync key show`.
29
+ *
30
+ * Loads the private key and derives the public key from it.
31
+ * NEVER outputs or returns the private key.
32
+ */
33
+ export async function executeKeyShow() {
34
+ const configDir = getConfigDir();
35
+ const privateKey = loadKey(configDir);
36
+ const publicKey = await identityToRecipient(privateKey);
37
+ return { publicKey };
38
+ }
39
+ /**
40
+ * Execute `ctx-sync key verify`.
41
+ *
42
+ * Checks that the key file and config directory have correct permissions.
43
+ */
44
+ export function executeKeyVerify() {
45
+ const configDir = getConfigDir();
46
+ return verifyPermissions(configDir);
47
+ }
48
+ /**
49
+ * Execute `ctx-sync key rotate`.
50
+ *
51
+ * 1. Generate a new key pair.
52
+ * 2. Decrypt ALL .age files with the old key.
53
+ * 3. Re-encrypt ALL with the new key.
54
+ * 4. Save the new private key (0o600).
55
+ * 5. Rewrite Git history to remove old encrypted blobs.
56
+ * 6. Optionally force-push to remote.
57
+ * 7. Return result for display.
58
+ */
59
+ export async function executeKeyRotate(_options = {}) {
60
+ const configDir = getConfigDir();
61
+ const syncDir = getSyncDir();
62
+ // 1. Load old key
63
+ const oldPrivateKey = loadKey(configDir);
64
+ const oldPublicKey = await identityToRecipient(oldPrivateKey);
65
+ // 2. Generate new key pair
66
+ const { publicKey: newPublicKey, privateKey: newPrivateKey } = await generateKey();
67
+ // 3. Re-encrypt all .age files
68
+ const ageFiles = listStateFiles(syncDir);
69
+ const filesReEncrypted = [];
70
+ for (const filename of ageFiles) {
71
+ const filePath = path.join(syncDir, filename);
72
+ const ciphertext = fs.readFileSync(filePath, 'utf-8');
73
+ if (!ciphertext.trim()) {
74
+ continue; // Skip empty files
75
+ }
76
+ // Decrypt with old key, re-encrypt with new key
77
+ const plainData = await decryptState(ciphertext, oldPrivateKey);
78
+ const newCiphertext = await encryptState(plainData, newPublicKey);
79
+ fs.writeFileSync(filePath, newCiphertext, 'utf-8');
80
+ filesReEncrypted.push(filename);
81
+ }
82
+ // 4. Save the new private key
83
+ saveKey(configDir, newPrivateKey);
84
+ // 5. Update manifest
85
+ const manifest = readManifest(syncDir);
86
+ if (manifest) {
87
+ manifest.lastSync = new Date().toISOString();
88
+ writeManifest(syncDir, manifest);
89
+ }
90
+ // 6. Rewrite Git history to remove old encrypted blobs
91
+ let gitHistoryRewritten = false;
92
+ const gitDir = path.join(syncDir, '.git');
93
+ if (fs.existsSync(gitDir)) {
94
+ gitHistoryRewritten = await rewriteGitHistory(syncDir);
95
+ }
96
+ return {
97
+ oldPublicKey,
98
+ newPublicKey,
99
+ filesReEncrypted,
100
+ gitHistoryRewritten,
101
+ };
102
+ }
103
+ /**
104
+ * Rewrite Git history to remove old encrypted blobs.
105
+ *
106
+ * Uses `git checkout --orphan` + fresh commit to create a clean history
107
+ * with only the current (re-encrypted) files. This is safer and more
108
+ * portable than `git filter-branch`.
109
+ */
110
+ async function rewriteGitHistory(syncDir) {
111
+ const { simpleGit } = await import('simple-git');
112
+ const git = simpleGit(syncDir);
113
+ try {
114
+ // Create an orphan branch with only current files
115
+ const orphanBranch = `_key-rotation-${Date.now()}`;
116
+ await git.checkout(['--orphan', orphanBranch]);
117
+ // Stage all current files
118
+ await git.add('.');
119
+ // Commit
120
+ await git.commit('key: rotate — re-encrypted all state with new key');
121
+ // Delete old main/master branch, rename orphan
122
+ const branches = await git.branchLocal();
123
+ const mainBranch = branches.all.find((b) => b === 'main' || b === 'master') ?? 'main';
124
+ // Only delete the old branch if it's different from our orphan
125
+ if (branches.all.includes(mainBranch) && mainBranch !== orphanBranch) {
126
+ await git.branch(['-D', mainBranch]);
127
+ }
128
+ await git.branch(['-m', mainBranch]);
129
+ // Clean up old objects
130
+ await git.raw(['reflog', 'expire', '--expire=now', '--all']);
131
+ await git.raw(['gc', '--prune=now', '--aggressive']);
132
+ return true;
133
+ }
134
+ catch {
135
+ // If history rewrite fails, the rotation still succeeded
136
+ // (files are re-encrypted), just old history remains
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * Execute `ctx-sync key update`.
142
+ *
143
+ * Prompts for (or reads from stdin) a new private key, validates it,
144
+ * and saves it with correct permissions.
145
+ */
146
+ export async function executeKeyUpdate(options = {}) {
147
+ const configDir = getConfigDir();
148
+ let keyInput;
149
+ if (options.keyInput !== undefined) {
150
+ // Direct input (for testing)
151
+ keyInput = options.keyInput;
152
+ }
153
+ else if (options.stdin) {
154
+ // Read from stdin
155
+ keyInput = await readKeyFromStdin();
156
+ }
157
+ else {
158
+ // Interactive prompt
159
+ keyInput = await readKeyFromPrompt();
160
+ }
161
+ const trimmedKey = keyInput.trim();
162
+ // Validate the key looks like an Age private key
163
+ if (!trimmedKey.startsWith('AGE-SECRET-KEY-')) {
164
+ throw new Error('Invalid key format. Expected an Age private key starting with AGE-SECRET-KEY-');
165
+ }
166
+ // Derive public key to verify it's valid
167
+ const publicKey = await identityToRecipient(trimmedKey);
168
+ // Save with secure permissions
169
+ saveKey(configDir, trimmedKey);
170
+ return { publicKey, configDir };
171
+ }
172
+ /**
173
+ * Read a private key from stdin (pipe mode).
174
+ */
175
+ function readKeyFromStdin() {
176
+ return new Promise((resolve, reject) => {
177
+ let data = '';
178
+ process.stdin.setEncoding('utf-8');
179
+ process.stdin.on('data', (chunk) => {
180
+ data += chunk;
181
+ });
182
+ process.stdin.on('end', () => resolve(data));
183
+ process.stdin.on('error', reject);
184
+ });
185
+ }
186
+ /**
187
+ * Read a private key from an interactive prompt.
188
+ */
189
+ function readKeyFromPrompt() {
190
+ return new Promise((resolve, reject) => {
191
+ const rl = readline.createInterface({
192
+ input: process.stdin,
193
+ output: process.stdout,
194
+ });
195
+ rl.question('Paste your private key (AGE-SECRET-KEY-...): ', (answer) => {
196
+ rl.close();
197
+ if (!answer) {
198
+ reject(new Error('No key provided.'));
199
+ }
200
+ else {
201
+ resolve(answer);
202
+ }
203
+ });
204
+ });
205
+ }
206
+ // ─── Commander Registration ───────────────────────────────────────────────
207
+ /**
208
+ * Register the `ctx-sync key` command group on the given program.
209
+ */
210
+ export function registerKeyCommand(program) {
211
+ const keyCmd = program
212
+ .command('key')
213
+ .description('Manage encryption keys');
214
+ // ── key show ──────────────────────────────────────────────────────
215
+ keyCmd
216
+ .command('show')
217
+ .description('Display your public key (never shows private key)')
218
+ .action(withErrorHandler(async () => {
219
+ const result = await executeKeyShow();
220
+ console.log(`Public key: ${result.publicKey}`);
221
+ }));
222
+ // ── key verify ────────────────────────────────────────────────────
223
+ keyCmd
224
+ .command('verify')
225
+ .description('Verify key file and config directory permissions')
226
+ .action(withErrorHandler(async () => {
227
+ const result = executeKeyVerify();
228
+ if (result.valid) {
229
+ console.log('✓ Key verification passed');
230
+ console.log(` Key file: permissions ${result.keyFilePerms?.toString(8) ?? 'n/a'}`);
231
+ console.log(` Config dir: permissions ${result.configDirPerms?.toString(8) ?? 'n/a'}`);
232
+ }
233
+ else {
234
+ console.error('✗ Key verification failed:');
235
+ for (const issue of result.issues) {
236
+ console.error(` - ${issue}`);
237
+ }
238
+ process.exit(1);
239
+ }
240
+ }));
241
+ // ── key rotate ────────────────────────────────────────────────────
242
+ keyCmd
243
+ .command('rotate')
244
+ .description('Rotate encryption key — re-encrypts all state')
245
+ .option('-n, --no-interactive', 'Skip confirmation prompts')
246
+ .action(withErrorHandler(async (opts) => {
247
+ const result = await executeKeyRotate({
248
+ noInteractive: !opts.interactive,
249
+ });
250
+ console.log('✓ Key rotation complete');
251
+ console.log(` Old public key: ${result.oldPublicKey}`);
252
+ console.log(` New public key: ${result.newPublicKey}`);
253
+ console.log(` Files re-encrypted: ${String(result.filesReEncrypted.length)}`);
254
+ if (result.gitHistoryRewritten) {
255
+ console.log(' Git history: rewritten (old blobs purged)');
256
+ }
257
+ console.log('\n⚠ IMPORTANT: All other machines must run:\n' +
258
+ ' ctx-sync key update\n' +
259
+ ' Then paste the new private key.');
260
+ }));
261
+ // ── key update ────────────────────────────────────────────────────
262
+ keyCmd
263
+ .command('update')
264
+ .description('Update private key on this machine (after rotation elsewhere)')
265
+ .option('--stdin', 'Read key from stdin')
266
+ .action(withErrorHandler(async (opts) => {
267
+ const result = await executeKeyUpdate({ stdin: opts.stdin });
268
+ console.log('✓ Key updated');
269
+ console.log(` Public key: ${result.publicKey}`);
270
+ console.log(` Saved to: ${result.configDir}`);
271
+ console.log(` Permissions: ${KEY_FILE_PERMS.toString(8)}`);
272
+ }));
273
+ }
274
+ //# sourceMappingURL=key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.js","sourceRoot":"","sources":["../../src/commands/key.ts"],"names":[],"mappings":"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"}