git-userhub 3.0.10 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -127,7 +127,7 @@ remove <name> Delete an identity
127
127
  edit <name> <email> Update email
128
128
  bind <name> Link an SSH key
129
129
  pubkey Show public key of active identity
130
- passphrase Change passphrase for active identity
130
+ passphrase Add, change, or remove (--remove) passphrase for active identity
131
131
  rekey <name> Rotate SSH key
132
132
  fix-remote Convert HTTPS remotes to SSH
133
133
  session start [--ttl <d>] Load SSH key into ssh-agent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-userhub",
3
- "version": "3.0.10",
3
+ "version": "3.1.1",
4
4
  "description": "Switch Git accounts in one command. No config editing. No SSH key chaos.",
5
5
  "bin": {
6
6
  "git-user": "bin/git-user.js"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Divyo Argha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,524 @@
1
+ <div align="center">
2
+ <br />
3
+ <img src="img/git-user-logo-clean.png" alt="git-user" width="120" height="120" style="border-radius:26px" />
4
+ <!-- <br /><br /> -->
5
+ <h1>git-user</h1>
6
+
7
+ <p>
8
+ <strong>One command to rule all your Git identities.</strong><br />
9
+ Stop committing as the wrong person. Stop juggling SSH keys. Stop editing config files.
10
+ </p>
11
+
12
+ <p>
13
+ <a href="https://github.com/divyo-argha/git-user/releases"><img src="https://img.shields.io/github/v/release/divyo-argha/git-user?style=flat&color=00FFAA&label=latest" alt="Latest Release" /></a>
14
+ <a href="https://github.com/divyo-argha/git-user/releases"><img src="https://img.shields.io/github/downloads/divyo-argha/git-user/total?style=flat&color=00FFAA&label=gh%20downloads" alt="GitHub Downloads" /></a>
15
+ <a href="https://www.npmjs.com/package/git-userhub"><img src="https://img.shields.io/npm/v/git-userhub?style=flat&color=CB3837&logo=npm&logoColor=white&label=npm" alt="npm" /></a>
16
+ <a href="https://www.npmjs.com/package/git-userhub"><img src="https://img.shields.io/npm/dt/git-userhub?style=flat&color=CB3837&logo=npm&logoColor=white&label=total%20downloads" alt="total downloads" /></a>
17
+ <a href="https://github.com/divyo-argha/git-user"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/divyo-argha/git-user/main/badges/total-downloads.json&style=flat" alt="Combined Downloads" /></a>
18
+ <a href="https://pkg.go.dev/github.com/divyo-argha/git-user"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go" /></a>
19
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat" alt="MIT" /></a>
20
+ </p>
21
+
22
+ <p>
23
+ <a href="#-the-problem">The Problem</a> ·
24
+ <a href="#-install">Install</a> ·
25
+ <a href="#-quick-start">Quick Start</a> ·
26
+ <a href="#-why-git-user">Why git-user</a> ·
27
+ <a href="#-features">Features</a> ·
28
+ <a href="#-commands">Commands</a> ·
29
+ <a href="#-security">Security</a> ·
30
+ <a href="#-contributing">Contributing</a>
31
+ </p>
32
+
33
+ <br />
34
+
35
+ <img src="https://img.shields.io/badge/GitHub-supported-181717?style=flat&logo=github&logoColor=white" alt="GitHub" />
36
+ <img src="https://img.shields.io/badge/GitLab-supported-FC6D26?style=flat&logo=gitlab&logoColor=white" alt="GitLab" />
37
+ <img src="https://img.shields.io/badge/Bitbucket-supported-0052CC?style=flat&logo=bitbucket&logoColor=white" alt="Bitbucket" />
38
+ <img src="https://img.shields.io/badge/macOS-supported-000000?style=flat&logo=apple&logoColor=white" alt="macOS" />
39
+ <img src="https://img.shields.io/badge/Linux-supported-FCC624?style=flat&logo=linux&logoColor=black" alt="Linux" />
40
+ <img src="https://img.shields.io/badge/Windows-supported-0078D4?style=flat&logo=windows&logoColor=white" alt="Windows" />
41
+
42
+ <br /><br />
43
+
44
+ </div>
45
+
46
+ ---
47
+
48
+ ## 😤 The Problem
49
+
50
+ You're a developer with multiple lives — work, personal, freelance, open source. Each one has its own Git account, its own SSH key, its own email.
51
+
52
+ And every few weeks, this happens:
53
+
54
+ ```
55
+ # You just pushed 3 commits to your client's repo.
56
+ # Then you check the author.
57
+
58
+ Author: you@personal.com ← 💀 wrong account. again.
59
+
60
+ # Or your work email leaked onto your public GitHub profile.
61
+ # Or your client can see your personal email in their repo history.
62
+ ```
63
+
64
+ You've tried everything:
65
+
66
+ | Attempt | Result |
67
+ |---------|--------|
68
+ | Editing `~/.gitconfig` manually | You forget. Every time. |
69
+ | Per-repo `.git/config` overrides | Works until you clone a new repo |
70
+ | Multiple terminal profiles | Still mix them up |
71
+ | SSH config `Host` aliases | Breaks half your existing remotes |
72
+ | Remembering which key goes where | Not a real solution |
73
+
74
+ **git-user is the permanent fix.** Register your identities once. Switch with one command. Everything — git config, SSH key, remote verification — updates automatically in under a second.
75
+
76
+ ---
77
+
78
+ ## 📦 Install
79
+
80
+ <table>
81
+ <tr>
82
+ <td width="33%" valign="top">
83
+
84
+ ### One-line
85
+ ```bash
86
+ curl -sSfL https://raw.githubusercontent.com/divyo-argha/git-user/main/install.sh | bash
87
+ ```
88
+ Restart your terminal. PATH is configured automatically.
89
+
90
+ </td>
91
+ <td width="33%" valign="top">
92
+
93
+ ### npm
94
+ ```bash
95
+ npm install -g git-userhub
96
+ ```
97
+ > Published as `git-userhub` on npm.
98
+ > After install, the command is `git-user`.
99
+
100
+ </td>
101
+ <td width="33%" valign="top">
102
+
103
+ ### Go
104
+ ```bash
105
+ go install github.com/divyo-argha/git-user@latest
106
+ ```
107
+
108
+ ### Self-update
109
+ ```bash
110
+ git-user --update
111
+ ```
112
+
113
+ </td>
114
+ </tr>
115
+ </table>
116
+
117
+ **Requirements:** ![Git](https://img.shields.io/badge/Git-required-F05032?style=flat&logo=git&logoColor=white) · ssh-keygen (optional, for SSH key generation)
118
+
119
+ ---
120
+
121
+ ## ⚡ Quick Start
122
+
123
+ Two minutes to set up. One second to switch forever after.
124
+
125
+ ```bash
126
+ # Step 1 — register your identities (guided, interactive)
127
+ git-user register # → name: work, email: you@company.com
128
+ git-user register # → name: personal, email: you@gmail.com
129
+ git-user register # → name: client-a, email: you@client.com
130
+
131
+ # Step 2 — switch
132
+ git-user switch work
133
+
134
+ # Step 3 — push. that's it.
135
+ git push # ← commits as you@company.com ✓
136
+ ```
137
+
138
+ ```bash
139
+ # Create and switch in one command
140
+ git-user switch -c freelance me@freelance.com
141
+
142
+ # Always know who you are
143
+ git-user current
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 🏆 Why git-user?
149
+
150
+ There are other tools that try to solve this. Here's how git-user is different:
151
+
152
+ | Feature | git-user | direnv / per-dir config | SSH `Host` aliases | Manual `~/.gitconfig` |
153
+ |---------|:--------:|:----------------------:|:------------------:|:---------------------:|
154
+ | One command to switch everything | ✅ | ❌ | ❌ | ❌ |
155
+ | SSH key managed automatically | ✅ | ❌ | ⚠️ partial | ❌ |
156
+ | Works across all repos, not just one | ✅ | ❌ | ✅ | ✅ |
157
+ | SSH connection verified on switch | ✅ | ❌ | ❌ | ❌ |
158
+ | Clean logout/sign-out to void state | ✅ | ❌ | ❌ | ❌ |
159
+ | Encrypted export/import | ✅ | ❌ | ❌ | ❌ |
160
+ | Pre-commit identity guard | ✅ | ❌ | ❌ | ❌ |
161
+ | Security audit built-in | ✅ | ❌ | ❌ | ❌ |
162
+ | Interactive TUI | ✅ | ❌ | ❌ | ❌ |
163
+ | Shell completions | ✅ | ❌ | ❌ | ❌ |
164
+ | Zero config files to edit manually | ✅ | ❌ | ❌ | ❌ |
165
+
166
+ > **The key difference:** git-user manages the *whole identity* — name, email, SSH key, and passphrase protection — as a single atomic unit. Other approaches only solve part of the problem, leaving you to manually wire the rest.
167
+
168
+ ---
169
+
170
+ ## ✨ Features
171
+
172
+ <table>
173
+ <tr>
174
+ <td width="50%" valign="top">
175
+
176
+ ### 🔑 Identity Management
177
+ - Register unlimited identities — name, email, SSH key
178
+ - Switch in one command, git config updates instantly
179
+ - `switch -c <name>` — create and switch in one step
180
+ - Edit email without re-registering
181
+ - Remove identities safely, with active-identity guard
182
+
183
+ </td>
184
+ <td width="50%" valign="top">
185
+
186
+ ### 🔐 SSH Key Handling
187
+ - Auto-generate ed25519 keys per identity
188
+ - `pubkey` — print active identity's public key (add to GitHub, GitLab, Bitbucket)
189
+ - Bind any existing key to any identity
190
+ - `rekey` rotates keys with automatic backup and rollback
191
+ - `IdentitiesOnly yes` — SSH never leaks the wrong key
192
+
193
+ </td>
194
+ </tr>
195
+ <tr>
196
+ <td width="50%" valign="top">
197
+
198
+ ### 🛡️ Security & Passphrases
199
+ - Passphrase-protected keys enforced by default
200
+ - `security` audits every identity: permissions, passphrase, key existence
201
+ - `passphrase` add, change, or remove (`--remove`) passphrase security for the active identity
202
+ - All config writes are atomic (temp file + rename) — crash-safe
203
+ - All files stored at `0600` permissions
204
+
205
+ </td>
206
+ <td width="50%" valign="top">
207
+
208
+ ### 🔒 Passphrase-Gated Switching
209
+ - Gated switch: switching to a passphrase-protected profile requires entering the passphrase to unlock the SSH key
210
+ - Seamless ssh-agent management: the SSH key is added automatically on switch
211
+ - Security by default: you cannot act as an identity without verifying the passphrase first
212
+ - Clean logout: sign out at any time to clear active user config completely
213
+
214
+ </td>
215
+ </tr>
216
+ <tr>
217
+ <td width="50%" valign="top">
218
+
219
+ ### 🚀 Passwordless Push
220
+ - Detects HTTPS remotes on `switch` and offers to convert
221
+ - `fix-remote` converts all remotes HTTPS → SSH instantly
222
+ - Works with GitHub, GitLab, Bitbucket, and any Git host
223
+
224
+ </td>
225
+ <td width="50%" valign="top">
226
+
227
+ ### 🖥️ Developer Experience
228
+ - Interactive TUI menu (`git-user tui`)
229
+ - Shell completions for bash, zsh, fish
230
+ - Pre-commit hooks to block wrong-identity commits
231
+ - `doctor` diagnoses your entire setup in one command
232
+ - Encrypted export/import for moving to a new machine
233
+
234
+ </td>
235
+ </tr>
236
+ </table>
237
+
238
+ ---
239
+
240
+ ## 🔄 How It Works
241
+
242
+ ### Under the hood — one switch
243
+
244
+ ```
245
+ git-user switch work
246
+
247
+
248
+ 1. Looks up "work" in ~/.git-users/config.json
249
+ 2. Sets ~/.gitconfig → user.name, user.email
250
+ 3. Sets ~/.gitconfig → core.sshCommand (points to your key)
251
+ 4. Verifies SSH connection to GitHub/GitLab/Bitbucket
252
+ 5. ✅ Switched to "work" (you@company.com)
253
+
254
+ git push ← just works, every time
255
+ ```
256
+
257
+ ### A real day with multiple accounts
258
+
259
+ ```
260
+ 9:00 AM — starting work
261
+ ──────────────────────────────────────────────────────────
262
+ $ git-user switch work
263
+ ✅ Switched to work (you@company.com)
264
+ $ git push ← commits as you@company.com ✓
265
+
266
+ 1:00 PM — open source on lunch break
267
+ ──────────────────────────────────────────────────────────
268
+ $ git-user switch personal
269
+ ✅ Switched to personal (you@gmail.com)
270
+ $ git push ← commits as you@gmail.com ✓
271
+
272
+ 5:00 PM — freelance client work
273
+ ──────────────────────────────────────────────────────────
274
+ $ git-user switch client-a
275
+ ✅ Switched to client-a (you@client-a.com)
276
+ $ git push ← commits as you@client-a.com ✓
277
+ ```
278
+
279
+ Each switch: under one second. No config editing. No SSH juggling.
280
+
281
+ ---
282
+
283
+ ## 🚪 Logout / Void State
284
+
285
+ When you are done with your work or leaving a shared machine, you can sign out to clear your active Git identity completely:
286
+
287
+ ```bash
288
+ git-user logout
289
+ ```
290
+
291
+ What happens:
292
+ - Unloads the active SSH key from `ssh-agent`
293
+ - Clears the global `user.name` and `user.email` from `~/.gitconfig`
294
+ - Clears `core.sshCommand` from `~/.gitconfig`
295
+ - Puts the terminal into a clean "void" state (no git user configured), preventing accidental commits under your identity by other users.
296
+
297
+ ---
298
+
299
+ ## 📋 Commands
300
+
301
+ | Command | Description |
302
+ |---------|-------------|
303
+ | `register` | Create a new identity (guided setup with SSH) |
304
+ | `switch <name>` | Switch to an identity |
305
+ | `switch -c <name> [email]` | Create and switch in one command |
306
+ | `list` | Show all identities |
307
+ | `current` | Show active identity |
308
+ | `remove <name>` | Delete an identity |
309
+ | `edit <name> <email>` | Update email |
310
+ | `bind <name> [--ssh-key <path>]` | Link an SSH key to an identity |
311
+ | `pubkey` | Show the public key of the active identity |
312
+ | `passphrase` | Add, change, or remove (`--remove`) passphrase for the active, unlocked identity |
313
+ | `rekey <name>` | Rotate SSH key (with rollback safety) |
314
+ | `fix-remote` | Convert HTTPS remotes to SSH |
315
+ | `logout` | Sign out, clearing the active identity and restoring a void state |
316
+ | `security` | Audit all identities for security issues |
317
+ | `export --all` | Export all identities + SSH keys (AES-256 encrypted) |
318
+ | `export <name> [name...]` | Export specific identities |
319
+ | `import <file>` | Import from an encrypted bundle |
320
+ | `doctor` | Run a full health check |
321
+ | `tui` | Interactive menu |
322
+ | `completion <shell>` | Shell completions (bash/zsh/fish) |
323
+ | `hook <install\|uninstall>` | Pre-commit hook to verify identity |
324
+ | `--update` | Update to the latest version |
325
+ | `--version` / `-v` | Show version |
326
+
327
+ **Aliases:** `ls` → `list` · `sw` → `switch` · `rm` → `remove`
328
+
329
+ ---
330
+
331
+ ## 🛡️ Security
332
+
333
+ <table>
334
+ <tr>
335
+ <td width="50%" valign="top">
336
+
337
+ **What git-user does**
338
+ - Private keys stay on your machine at `0600` permissions
339
+ - Config writes are atomic (temp file + rename) — crash-safe
340
+ - `IdentitiesOnly yes` in SSH config — no key leakage
341
+ - Passphrase protection audited by `security` command
342
+ - Export bundles encrypted with AES-256-GCM, passphrase stretched with scrypt (N=2¹⁷)
343
+ - Passphrases are never passed as CLI arguments — entered directly into the terminal
344
+ - `pubkey` only shows the active identity's key — other identities' keys are never exposed
345
+
346
+ </td>
347
+ <td width="50%" valign="top">
348
+
349
+ **What git-user never does**
350
+ - Never stores passphrases
351
+ - Never sends keys or config anywhere
352
+ - Never modifies your repositories
353
+ - Never overwrites existing identities on import
354
+ - `logout` command cleanly clears all gitconfig references and unloads loaded keys
355
+
356
+ </td>
357
+ </tr>
358
+ </table>
359
+
360
+ ### Run a security audit
361
+
362
+ ```bash
363
+ git-user security
364
+ ```
365
+
366
+ ```
367
+ ✔ Config file permissions OK (0600)
368
+
369
+ ℹ work (you@company.com)
370
+ ✔ Permissions OK: git_work
371
+ ✔ Passphrase protected
372
+
373
+ ℹ personal (you@gmail.com)
374
+ ✔ Permissions OK: git_personal
375
+ ⚠ No passphrase detected
376
+ Fix: ssh-keygen -p -f ~/.ssh/git_personal
377
+ ```
378
+
379
+ ---
380
+
381
+ ## 🚚 Moving to a New Machine
382
+
383
+ ```bash
384
+ # On your current machine
385
+ git-user export --all
386
+ # → ~/git-user-export-2026-05-29.bundle (AES-256 encrypted)
387
+
388
+ # Transfer the file, then on the new machine
389
+ git-user import ~/git-user-export-2026-05-29.bundle
390
+ # ✅ Imported: work (you@company.com) → ~/.ssh/git_work
391
+ # ✅ Imported: personal (you@gmail.com) → ~/.ssh/git_personal
392
+
393
+ git-user switch work
394
+ # Ready to push immediately
395
+ ```
396
+
397
+ ---
398
+
399
+ ## 🔧 Troubleshooting
400
+
401
+ ```bash
402
+ git-user doctor
403
+ ```
404
+
405
+ ```
406
+ ✅ git installed (2.43.0)
407
+ ✅ ssh-keygen available
408
+ ✅ Active identity: work (you@company.com)
409
+ ✅ SSH key exists at ~/.ssh/git_work
410
+ ✅ Key permissions OK (0600)
411
+ ✅ GitHub connection verified — Hi alice-corp!
412
+ ──────────────────────────────────────────────
413
+ Everything looks good.
414
+ ```
415
+
416
+ **Common issues:**
417
+
418
+ | Symptom | Fix |
419
+ |---------|-----|
420
+ | `git-user: command not found` | Restart terminal or `source ~/.zshrc` |
421
+ | SSH verification failed | Key not added to platform yet — run `git-user pubkey` to copy the public key |
422
+ | `Permission denied` during install | Expected — installer needs sudo for `/usr/local/bin` |
423
+ | Git asks for credentials on push | Run `git-user fix-remote` to convert HTTPS → SSH |
424
+
425
+ ---
426
+
427
+ ## 🐚 Shell Completions
428
+
429
+ ```bash
430
+ # Bash
431
+ git-user completion bash | sudo tee /etc/bash_completion.d/git-user
432
+
433
+ # Zsh
434
+ git-user completion zsh > "${fpath[1]}/_git-user"
435
+
436
+ # Fish
437
+ git-user completion fish > ~/.config/fish/completions/git-user.fish
438
+ ```
439
+
440
+ ```bash
441
+ git-user sw<TAB> # → git-user switch
442
+ git-user switch <TAB> # → work personal client-a
443
+ git-user remove <TAB> # → your identity names
444
+ ```
445
+
446
+ ---
447
+
448
+ ## 🎨 Terminal Prompt Integration
449
+
450
+ You can display your active `git-user` profile directly in your terminal prompt (like Starship, Powerlevel10k, Zsh, or Fish). The `git-user prompt` command is extremely fast and will only output your profile name if you are currently inside a git repository, making it perfect for custom prompt segments!
451
+
452
+ To avoid automatically modifying your personal shell configurations, we've provided simple, copy-paste instructions for all the popular shells.
453
+
454
+ 👉 **[View the Terminal Integration Guide](./TERMINAL-INTEGRATION.md)**
455
+
456
+ ---
457
+
458
+ ## 🪝 Pre-commit Hooks
459
+
460
+ ```bash
461
+ git-user hook install # in any repo where identity matters
462
+ ```
463
+
464
+ ```bash
465
+ git commit -m "Add feature"
466
+
467
+ # ✖ Identity mismatch!
468
+ # Expected: work (you@company.com)
469
+ # Git config: you@gmail.com
470
+ # Run: git-user switch work
471
+ ```
472
+
473
+ ---
474
+
475
+ ## 📁 What Gets Modified
476
+
477
+ ```
478
+ ~/.git-users/
479
+ └── config.json ← your identities (names, emails, key paths)
480
+
481
+ ~/.gitconfig ← updated on every switch/logout (name, email, sshCommand)
482
+ ~/.ssh/git_<name> ← private key (never leaves your machine)
483
+ ~/.ssh/git_<name>.pub ← public key (what you add to GitHub/GitLab)
484
+ ```
485
+
486
+ Your repositories are never touched. Only global git config changes.
487
+
488
+ ---
489
+
490
+ ## 🤝 Contributing
491
+
492
+ Issues and pull requests are welcome. If something's broken, open an issue. If something's confusing — even just "I didn't understand what this command does" — that's worth filing too.
493
+
494
+ ```bash
495
+ git clone https://github.com/divyo-argha/git-user.git
496
+ cd git-user
497
+ make build # build binary
498
+ make test # run tests
499
+ ```
500
+
501
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
502
+
503
+ ---
504
+
505
+ ## 📄 License
506
+
507
+ MIT — see [LICENSE](LICENSE).
508
+
509
+ ---
510
+
511
+ <div align="center">
512
+
513
+ **Made for developers who just want their Git to work.**
514
+
515
+ <br />
516
+
517
+ [![GitHub](https://img.shields.io/badge/Star%20on%20GitHub-181717?style=flat&logo=github&logoColor=white)](https://github.com/divyo-argha/git-user)
518
+ [![npm](https://img.shields.io/badge/Install%20via%20npm-CB3837?style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/git-userhub)
519
+
520
+ <br />
521
+
522
+ <sub>If git-user saved you from a wrong-account commit, consider giving it a ⭐</sub>
523
+
524
+ </div>
@@ -0,0 +1,103 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const https = require('https');
4
+ const crypto = require('crypto');
5
+ const { spawnSync } = require('child_process');
6
+ const PKG_JSON = require('../package.json');
7
+
8
+ const REPO = 'divyo-argha/git-user';
9
+ const VERSION = `v${PKG_JSON.version}`;
10
+
11
+ const ASSETS = [
12
+ { name: `git-user_darwin_arm64.tar.gz`, bin: `git-user` },
13
+ { name: `git-user_darwin_x86_64.tar.gz`, bin: `git-user` },
14
+ { name: `git-user_linux_arm64.tar.gz`, bin: `git-user` },
15
+ { name: `git-user_linux_x86_64.tar.gz`, bin: `git-user` },
16
+ { name: `git-user_windows_x86_64.tar.gz`, bin: `git-user.exe` }
17
+ ];
18
+
19
+ function fetchFile(url, dest) {
20
+ return new Promise((resolve, reject) => {
21
+ https.get(url, { headers: { 'User-Agent': 'node' } }, (res) => {
22
+ if (res.statusCode === 301 || res.statusCode === 302) {
23
+ return fetchFile(res.headers.location, dest).then(resolve).catch(reject);
24
+ }
25
+ if (res.statusCode !== 200) return reject(new Error(`Download Error ${res.statusCode}`));
26
+ const file = fs.createWriteStream(dest);
27
+ res.pipe(file);
28
+ file.on('finish', () => { file.close(); resolve(); });
29
+ }).on('error', reject);
30
+ });
31
+ }
32
+
33
+ function computeHash(file) {
34
+ return new Promise((resolve, reject) => {
35
+ const hash = crypto.createHash('sha256');
36
+ const stream = fs.createReadStream(file);
37
+ stream.on('data', d => hash.update(d));
38
+ stream.on('end', () => resolve(hash.digest('hex')));
39
+ stream.on('error', reject);
40
+ });
41
+ }
42
+
43
+ async function run() {
44
+ console.log(`🔒 Pinning cryptographic hashes for ${VERSION}...`);
45
+ const hashes = {};
46
+
47
+ try {
48
+ for (const asset of ASSETS) {
49
+ console.log(`Downloading ${asset.name}...`);
50
+ const url = `https://github.com/${REPO}/releases/download/${VERSION}/${asset.name}`;
51
+ const archivePath = path.join(__dirname, asset.name);
52
+
53
+ await fetchFile(url, archivePath);
54
+
55
+ const archiveHash = await computeHash(archivePath);
56
+
57
+ // Extract to get the binary hash
58
+ const result = spawnSync('tar', ['-xzf', archivePath, '-C', __dirname]);
59
+ if (result.error || result.status !== 0) {
60
+ throw new Error("Failed to extract tar archive: " + archivePath);
61
+ }
62
+ const extractedPath = path.join(__dirname, asset.bin);
63
+
64
+ const binaryHash = await computeHash(extractedPath);
65
+
66
+ hashes[`${asset.name}`] = {
67
+ archive: archiveHash,
68
+ binary: binaryHash
69
+ };
70
+
71
+ // Cleanup
72
+ fs.unlinkSync(archivePath);
73
+ fs.unlinkSync(extractedPath);
74
+ }
75
+
76
+ const targetFile = path.join(__dirname, 'install.js');
77
+ let code = fs.readFileSync(targetFile, 'utf8');
78
+
79
+ const hashesJson = JSON.stringify(hashes, null, 2).replace(/\n/g, '\n ');
80
+
81
+ const startMarker = '// --- START PINNED HASHES ---';
82
+ const endMarker = '// --- END PINNED HASHES ---';
83
+
84
+ const startIndex = code.indexOf(startMarker);
85
+ const endIndex = code.indexOf(endMarker);
86
+
87
+ if (startIndex === -1 || endIndex === -1) {
88
+ throw new Error("Could not find PINNED HASHES markers in scripts/install.js");
89
+ }
90
+
91
+ const newCode = code.substring(0, startIndex + startMarker.length) +
92
+ '\nconst PINNED_HASHES = ' + hashesJson + ';\n' +
93
+ code.substring(endIndex);
94
+
95
+ fs.writeFileSync(targetFile, newCode);
96
+ console.log(`✅ Successfully injected cryptographic pins into scripts/install.js`);
97
+ } catch (err) {
98
+ console.error(`❌ Failed to inject hashes: ${err.message}`);
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ run();
@@ -0,0 +1,136 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const https = require('https');
5
+ const crypto = require('crypto');
6
+ const { spawnSync } = require('child_process');
7
+ const PKG_JSON = require('../package.json');
8
+
9
+ const REPO = 'divyo-argha/git-user';
10
+ const VERSION = `v${PKG_JSON.version}`;
11
+
12
+ // --- START PINNED HASHES ---
13
+ const PINNED_HASHES = {
14
+ "git-user_darwin_arm64.tar.gz": {
15
+ "archive": "8f9e59d21977a7f923c027d3312c01ce81771efe3e2bb1dabf837c384504b20c",
16
+ "binary": "0cd7c9be0be6bd22a5dc8adb6c82ca73a5718b182fa3aba0d7e41a2c3ec08d18"
17
+ },
18
+ "git-user_darwin_x86_64.tar.gz": {
19
+ "archive": "4abbe23be36683c76815defc7efdb94157dee946e7e01a529cdd724e4d6118da",
20
+ "binary": "4d8a0127ca8f0f616612c4617cf1bd3824a556e1c92fb401858cb7460e393b81"
21
+ },
22
+ "git-user_linux_arm64.tar.gz": {
23
+ "archive": "9ddfe15bace025795489e6b545dc4dc9a50a24863aa3eb504e2325d5049c84fd",
24
+ "binary": "6d7a710f699a92baa632263a26f34aeb761e373cd0a84a69c5c2215d487188b2"
25
+ },
26
+ "git-user_linux_x86_64.tar.gz": {
27
+ "archive": "088aac2526c6f3a13ece1892d51696e4be12e03937d6cd0f74b50907ac14d716",
28
+ "binary": "855f46795ed5b18f6463049d29ac868772cf00e48a911d5f15d34ef7b28e4e37"
29
+ },
30
+ "git-user_windows_x86_64.tar.gz": {
31
+ "archive": "b206786fea35395a5911631f03f17f69b6ed9dae401b7c3eea8e3b076e9fee55",
32
+ "binary": "d3034bd2b48e5a95282679ee27b4c3b714f20e0ddacb19b6982ffa75167cf05f"
33
+ }
34
+ };
35
+ // --- END PINNED HASHES ---
36
+
37
+ const platform = os.platform();
38
+ const arch = os.arch();
39
+
40
+ const platformMap = { 'darwin': 'darwin', 'linux': 'linux', 'win32': 'windows' };
41
+ const archMap = { 'x64': 'x86_64', 'arm64': 'arm64' };
42
+
43
+ const osName = platformMap[platform];
44
+ const archName = archMap[arch];
45
+ const ext = platform === 'win32' ? '.exe' : '';
46
+
47
+ if (!osName || !archName) {
48
+ console.error(`❌ Unsupported platform: ${platform} ${arch}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const finalBinaryName = `git-user-${platform}-${arch}${ext}`;
53
+ const finalBinaryPath = path.join(__dirname, '..', 'bin', finalBinaryName);
54
+ const assetName = `git-user_${osName}_${archName}.tar.gz`;
55
+ const pinnedData = PINNED_HASHES[assetName];
56
+
57
+ function computeHash(file) {
58
+ return new Promise((resolve, reject) => {
59
+ const hash = crypto.createHash('sha256');
60
+ const stream = fs.createReadStream(file);
61
+ stream.on('data', d => hash.update(d));
62
+ stream.on('end', () => resolve(hash.digest('hex')));
63
+ stream.on('error', reject);
64
+ });
65
+ }
66
+
67
+ function fetchFile(url, dest) {
68
+ return new Promise((resolve, reject) => {
69
+ https.get(url, { headers: { 'User-Agent': 'node' } }, (res) => {
70
+ if (res.statusCode === 301 || res.statusCode === 302) {
71
+ return fetchFile(res.headers.location, dest).then(resolve).catch(reject);
72
+ }
73
+ if (res.statusCode !== 200) return reject(new Error(`Download Error ${res.statusCode}`));
74
+ const file = fs.createWriteStream(dest);
75
+ res.pipe(file);
76
+ file.on('finish', () => { file.close(); resolve(); });
77
+ }).on('error', reject);
78
+ });
79
+ }
80
+
81
+ async function install() {
82
+ if (fs.existsSync(finalBinaryPath)) {
83
+ return; // Already installed
84
+ }
85
+
86
+ console.log(`[git-user] Downloading cryptographically signed binary for ${platform}-${arch}...`);
87
+
88
+ try {
89
+ const archivePath = path.join(__dirname, '..', 'bin', assetName);
90
+ const scheme = 'https';
91
+ const host = 'github.com';
92
+ const downloadUrl = `${scheme}://${host}/${REPO}/releases/download/${VERSION}/${assetName}`;
93
+
94
+ await fetchFile(downloadUrl, archivePath);
95
+
96
+ if (pinnedData) {
97
+ const archiveHash = await computeHash(archivePath);
98
+ if (archiveHash !== pinnedData.archive) {
99
+ throw new Error("Archive checksum mismatch! Connection may be compromised.");
100
+ }
101
+ }
102
+
103
+ const binaryNameInArchive = platform === 'win32' ? 'git-user.exe' : 'git-user';
104
+ const binDir = path.join(__dirname, '..', 'bin');
105
+ const result = spawnSync('tar', ['-xzf', archivePath, '-C', binDir]);
106
+ if (result.error || result.status !== 0) {
107
+ throw new Error("Failed to extract tar archive");
108
+ }
109
+
110
+ const extractedPath = path.join(__dirname, '..', 'bin', binaryNameInArchive);
111
+ fs.renameSync(extractedPath, finalBinaryPath);
112
+
113
+ if (platform !== 'win32') fs.chmodSync(finalBinaryPath, 0o755);
114
+
115
+ fs.unlinkSync(archivePath);
116
+
117
+ if (pinnedData) {
118
+ const binaryHash = await computeHash(finalBinaryPath);
119
+ if (binaryHash !== pinnedData.binary) {
120
+ fs.unlinkSync(finalBinaryPath);
121
+ throw new Error("Binary checksum mismatch! Payload was modified during extraction.");
122
+ }
123
+ }
124
+
125
+ console.log(`[git-user] Installation and verification complete.\n`);
126
+ } catch (err) {
127
+ console.error(`\n❌ git-user installation failed: ${err.message}`);
128
+ throw err;
129
+ }
130
+ }
131
+
132
+ if (require.main === module) {
133
+ install().catch(() => process.exit(1));
134
+ } else {
135
+ module.exports = install;
136
+ }
Binary file
Binary file