@dk/hipp 0.1.16 → 0.1.19
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 +96 -64
- package/hipp.js +109 -63
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,40 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
By Dmytri Kleiner <dev@dmytri.to>
|
|
4
4
|
|
|
5
|
-
**HIPP** is a minimalist, stateless publishing tool
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
remains permanently at `0.0.0`.
|
|
9
|
-
|
|
10
|
-
HIPP provides **cryptographic signing** and **out-of-band verification** to
|
|
11
|
-
guarantee that the package in the npm registry exactly matches your git tag.
|
|
5
|
+
**HIPP** is a minimalist, stateless publishing tool that eliminates version-bump
|
|
6
|
+
commits and merge conflicts by treating Git Tags as the single source of truth.
|
|
7
|
+
Your `package.json` version stays permanently at `0.0.0`.
|
|
12
8
|
|
|
13
9
|
---
|
|
14
10
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
### Integrity
|
|
18
|
-
|
|
19
|
-
Traditional NPM versioning requires you to store your "Version of Truth" inside
|
|
20
|
-
your source code files (`package.json`). This creates a **State Conflict** that
|
|
21
|
-
leads to several systemic problems:
|
|
11
|
+
## The Problem
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
your Git history diverge. This scenario makes it impossible to guarantee that
|
|
26
|
-
the code in the registry matches the code at that tag.
|
|
13
|
+
Traditional NPM versioning requires storing the "Version of Truth" in `package.json`.
|
|
14
|
+
This creates a **State Conflict**:
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
- `npm version` and `git tag` are two distinct, non-atomic actions
|
|
17
|
+
- If you tag a commit but forget to update the JSON (or vice-versa), your
|
|
18
|
+
registry package and Git history diverge
|
|
19
|
+
- Every release requires a "chore: bump version" commit
|
|
20
|
+
- When multiple branches are developed simultaneously, these trivial changes
|
|
21
|
+
cause constant merge conflicts
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
## The Solution
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
branches are developed simultaneously, these version changes cause constant,
|
|
35
|
-
trivial merge conflicts.
|
|
25
|
+
**HIPP makes `package.json` version immutable (0.0.0)** - the **HIPP Doctrine**.
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
Version is extracted directly from the Git Tag during publish. Your Git history
|
|
28
|
+
stays clean, and your registry package is guaranteed to match your Git tag.
|
|
39
29
|
|
|
40
30
|
---
|
|
41
31
|
|
|
@@ -43,7 +33,7 @@ conflicts and your git history stays clean.**
|
|
|
43
33
|
|
|
44
34
|
### Setup
|
|
45
35
|
|
|
46
|
-
1. Set your project's `package.json` version to `0.0.0
|
|
36
|
+
1. Set your project's `package.json` version to `0.0.0`:
|
|
47
37
|
|
|
48
38
|
```json
|
|
49
39
|
{ "name": "your-package", "version": "0.0.0" }
|
|
@@ -72,11 +62,20 @@ HIPP will:
|
|
|
72
62
|
|
|
73
63
|
On first run, HIPP generates an Ed25519 keypair:
|
|
74
64
|
|
|
75
|
-
- **`hipp.priv`** - Your private signing key. **Never committed to git.**
|
|
65
|
+
- **`hipp.priv`** - Your private signing key. **Never committed to git.**
|
|
66
|
+
Added to `.gitignore` automatically.
|
|
76
67
|
- **`hipp.pub`** - Your public verification key. **Committed to git** automatically.
|
|
77
68
|
|
|
78
69
|
The private key holder can sign packages. The public key verifies signatures.
|
|
79
70
|
|
|
71
|
+
**Key rotation**: Delete `hipp.pub` and run HIPP again. A new keypair will be
|
|
72
|
+
generated and committed automatically. Verification uses the public key from
|
|
73
|
+
the specific git revision at the tag, so previous packages remain verifiable
|
|
74
|
+
with their original keys.
|
|
75
|
+
|
|
76
|
+
**Multiple publishers**: Each developer can use their own private key. Delete
|
|
77
|
+
`hipp.pub`, run HIPP, and a new keypair will be generated for that revision.
|
|
78
|
+
|
|
80
79
|
### Options
|
|
81
80
|
|
|
82
81
|
* `-y, --yes`: Skip the confirmation prompt (ideal for CI/CD pipelines).
|
|
@@ -91,10 +90,11 @@ npx @dk/hipp -- --access public --tag beta
|
|
|
91
90
|
|
|
92
91
|
## Verification
|
|
93
92
|
|
|
94
|
-
HIPP provides out-of-band verification to
|
|
93
|
+
HIPP provides out-of-band verification to prove package integrity:
|
|
95
94
|
|
|
96
95
|
```bash
|
|
97
96
|
npx @dk/hipp verify @dk/your-package[@version]
|
|
97
|
+
npx @dk/hipp verify # verifies the installed hipp version
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
### How Verification Works
|
|
@@ -102,8 +102,7 @@ npx @dk/hipp verify @dk/your-package[@version]
|
|
|
102
102
|
**Step 1: Get manifest from npm**
|
|
103
103
|
|
|
104
104
|
1. Fetch the package tarball from npm registry
|
|
105
|
-
2. Extract the README
|
|
106
|
-
3. Parse the JSON manifest appended to the README
|
|
105
|
+
2. Extract the README and parse the JSON manifest appended to it
|
|
107
106
|
|
|
108
107
|
The manifest contains:
|
|
109
108
|
```json
|
|
@@ -111,42 +110,73 @@ The manifest contains:
|
|
|
111
110
|
"origin": "git@github.com:dk/your-package.git",
|
|
112
111
|
"tag": "v1.0.0",
|
|
113
112
|
"hash": "<sha256-of-tarball>",
|
|
114
|
-
"signature": "<base64-ed25519-signature>"
|
|
113
|
+
"signature": "<base64-ed25519-signature>",
|
|
114
|
+
"name": "Jane Developer",
|
|
115
|
+
"email": "jane@example.com"
|
|
115
116
|
}
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
**Step 2: Clone git and
|
|
119
|
+
**Step 2: Clone git and verify**
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
3. Clone the repository at the tagged commit (using origin/tag from manifest)
|
|
122
|
+
4. Stage all tracked files
|
|
123
|
+
5. Run `npm pack` to create a tarball
|
|
124
|
+
6. Compute SHA256 hash of the clean tarball
|
|
125
|
+
7. Compare with the `hash` field from the npm manifest
|
|
122
126
|
|
|
123
|
-
**Step 3: Verify
|
|
127
|
+
**Step 3: Verify signature**
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
8. Read `hipp.pub` from the cloned repository
|
|
130
|
+
9. Verify the signature was created by signing:
|
|
131
|
+
`hash + "\n" + origin + "\n" + tag + "\n" + name + "\n" + email`
|
|
128
132
|
|
|
129
|
-
**
|
|
133
|
+
**Step 4: Rebuild verification**
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
10. Append the manifest to the staged README
|
|
136
|
+
11. Update the staged `package.json` version to match the tag
|
|
137
|
+
12. Run `npm pack` again and verify the hash matches the npm tarball
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
10. Verify the signature using the public key
|
|
139
|
+
### Three Verification Checks
|
|
135
140
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
**
|
|
141
|
+
| Check | What it proves |
|
|
142
|
+
|-------|----------------|
|
|
143
|
+
| **1. Signature** | The manifest was signed by the holder of the private key matching `hipp.pub` |
|
|
144
|
+
| **2. Manifest hash** | The claimed hash is accurate for this exact git revision |
|
|
145
|
+
| **3. Rebuild** | The npm tarball exactly matches what you'd produce from clean git source |
|
|
139
146
|
|
|
140
147
|
### What Verification Guarantees
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
- **Integrity**: The code in npm exactly matches git at the tagged commit
|
|
150
|
+
- **Authenticity**: The package was published by the holder of the private key
|
|
151
|
+
- **Reproducibility**: The npm tarball is byte-for-byte identical to a git rebuild
|
|
152
|
+
|
|
153
|
+
### What Verification Does NOT Guarantee
|
|
154
|
+
|
|
155
|
+
- **Code is safe or bug-free**: Malicious or buggy code can be signed
|
|
156
|
+
- **Publisher is trustworthy**: The key holder could sign bad code intentionally
|
|
157
|
+
|
|
158
|
+
Verification proves that npm matches git - it says nothing about whether that
|
|
159
|
+
code is correct or safe.
|
|
146
160
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
-
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Dual-Channel Trust
|
|
164
|
+
|
|
165
|
+
npm and git serve as independent verification channels:
|
|
166
|
+
|
|
167
|
+
- **npm** records *when* and *what* was published by *whom*
|
|
168
|
+
- **git** records the source code and the public key
|
|
169
|
+
|
|
170
|
+
An attacker would need to compromise both registries to forge a valid package.
|
|
171
|
+
This doesn't prove code is safe, but it proves the code in npm matches git.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Security
|
|
176
|
+
|
|
177
|
+
HIPP uses **Ed25519** public-key signatures. The private key never leaves your
|
|
178
|
+
machine. The public key is distributed via git. Anyone can verify a signed
|
|
179
|
+
package, but only private key holders can publish.
|
|
150
180
|
|
|
151
181
|
### Integrity Rules
|
|
152
182
|
|
|
@@ -166,14 +196,6 @@ HIPP enforces strict integrity rules when publishing:
|
|
|
166
196
|
|
|
167
197
|
---
|
|
168
198
|
|
|
169
|
-
## Security
|
|
170
|
-
|
|
171
|
-
HIPP uses **Ed25519** public-key signatures. The private key never leaves your
|
|
172
|
-
machine. The public key is distributed via git. Anyone can verify a signed
|
|
173
|
-
package, but only private key holders can publish.
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
199
|
## License
|
|
178
200
|
|
|
179
201
|
**0BSD** (BSD Zero Clause License) By Dmytri Kleiner <dev@dmytri.to>
|
|
@@ -189,11 +211,21 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
|
189
211
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
190
212
|
PERFORMANCE OF THIS SOFTWARE.
|
|
191
213
|
|
|
214
|
+
## Verify
|
|
215
|
+
|
|
216
|
+
Verify this package with [@dk/hipp](https://www.npmjs.com/package/@dk/hipp):
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npx @dk/hipp verify @dk/hipp@0.1.19
|
|
220
|
+
```
|
|
221
|
+
|
|
192
222
|
```json
|
|
193
223
|
{
|
|
194
224
|
"origin": "git@github.com:dmytri/hipp.git",
|
|
195
|
-
"tag": "v0.1.
|
|
196
|
-
"hash": "
|
|
197
|
-
"signature": "
|
|
225
|
+
"tag": "v0.1.19",
|
|
226
|
+
"hash": "adcdd07b563c5667d405b36ee1e391710e1baf4ee9434b1fd2f5a11c2acdecf0",
|
|
227
|
+
"signature": "MdkA4AbDX4ofShqXKJxwWX+x+RWQJU16WpSkFvHU8LihgdIBJbsyNvzIfrjm+3cll2FdR5u3+/dwv+a0rHbUDw==",
|
|
228
|
+
"name": "Dmytri Kleiner",
|
|
229
|
+
"email": "dev@dmytri.to"
|
|
198
230
|
}
|
|
199
231
|
```
|
package/hipp.js
CHANGED
|
@@ -27,6 +27,12 @@ function git(args, options = {}) {
|
|
|
27
27
|
}).trim();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function getGitUserInfo() {
|
|
31
|
+
const name = git(['config', 'user.name']);
|
|
32
|
+
const email = git(['config', 'user.email']);
|
|
33
|
+
return { name, email };
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
function runCmd(cmd, args, options = {}) {
|
|
31
37
|
const result = spawnSync(cmd, args, {
|
|
32
38
|
encoding: 'utf8',
|
|
@@ -64,10 +70,18 @@ function loadOrGenerateKeys() {
|
|
|
64
70
|
const pubPath = getPublicKeyPath();
|
|
65
71
|
|
|
66
72
|
if (fs.existsSync(privPath) && fs.existsSync(pubPath)) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
const privateKey = fs.readFileSync(privPath, 'utf8');
|
|
74
|
+
const publicKey = fs.readFileSync(pubPath, 'utf8');
|
|
75
|
+
|
|
76
|
+
const testData = 'hipp-key-validation';
|
|
77
|
+
const testSignature = signContent(testData, privateKey);
|
|
78
|
+
const valid = verifySignature(testData, testSignature, publicKey);
|
|
79
|
+
|
|
80
|
+
if (valid) {
|
|
81
|
+
return { privateKey, publicKey };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log.warn('⚠️ Key mismatch detected. Generating new keypair...');
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
log.info('🔑 Generating Ed25519 keypair...');
|
|
@@ -107,20 +121,8 @@ function verifySignature(data, signature, publicKey) {
|
|
|
107
121
|
}, Buffer.from(signature, 'base64'));
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
function
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function parseManifest(manifestStr) {
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(manifestStr);
|
|
117
|
-
} catch {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function buildSignData(hash, origin, tag) {
|
|
123
|
-
return `${hash}\n${origin}\n${tag}\n`;
|
|
124
|
+
function buildSignData(hash, origin, tag, name, email) {
|
|
125
|
+
return `${hash}\n${origin}\n${tag}\n${name}\n${email}\n`;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
function findLastJsonBlock(readmeContent) {
|
|
@@ -408,33 +410,18 @@ function copyTrackedFilesFromDir(stageDir, repoDir, files) {
|
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
async function runVerify(packageSpec) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
pkgName = packageSpec;
|
|
416
|
-
pkgVersion = undefined;
|
|
417
|
-
} else {
|
|
418
|
-
pkgName = packageSpec.slice(0, atIndex);
|
|
419
|
-
pkgVersion = packageSpec.slice(atIndex + 1);
|
|
420
|
-
}
|
|
421
|
-
} else {
|
|
422
|
-
const atIndex = packageSpec.indexOf('@');
|
|
423
|
-
if (atIndex === -1) {
|
|
424
|
-
pkgName = packageSpec;
|
|
425
|
-
pkgVersion = undefined;
|
|
426
|
-
} else {
|
|
427
|
-
pkgName = packageSpec.slice(0, atIndex);
|
|
428
|
-
pkgVersion = packageSpec.slice(atIndex + 1);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
413
|
+
const npa = require('npm-package-arg');
|
|
414
|
+
const parsed = npa(packageSpec);
|
|
415
|
+
const pkgName = parsed.name;
|
|
416
|
+
const pkgVersion = parsed.fetchSpec;
|
|
431
417
|
log.info(`🔍 HIPP Verify: ${pkgName}${pkgVersion ? '@' + pkgVersion : ''}`);
|
|
432
418
|
|
|
433
|
-
const registryUrl = `https://registry.npmjs.org/${
|
|
419
|
+
const registryUrl = `https://registry.npmjs.org/${parsed.escapedName}/${pkgVersion || 'latest'}`;
|
|
434
420
|
|
|
435
|
-
log.info(`📦 Fetching from npm...`);
|
|
421
|
+
log.info(`📦 Fetching manifest from npm...`);
|
|
436
422
|
const registryJson = runCmd('curl', ['-s', '-L', registryUrl]);
|
|
437
423
|
let tarballUrl;
|
|
424
|
+
let manifest;
|
|
438
425
|
try {
|
|
439
426
|
const json = JSON.parse(registryJson.stdout.trim());
|
|
440
427
|
tarballUrl = json.dist.tarball;
|
|
@@ -452,6 +439,10 @@ async function runVerify(packageSpec) {
|
|
|
452
439
|
fail(`❌ Failed to download tarball`);
|
|
453
440
|
}
|
|
454
441
|
|
|
442
|
+
const npmTarballContent = fs.readFileSync(tarballPath);
|
|
443
|
+
const npmHash = sha256(npmTarballContent);
|
|
444
|
+
log.success(`📦 NPM tarball hash: ${npmHash.slice(0, 12)}...`);
|
|
445
|
+
|
|
455
446
|
if (fs.existsSync(extractDir)) {
|
|
456
447
|
fs.rmSync(extractDir, { recursive: true });
|
|
457
448
|
}
|
|
@@ -464,25 +455,25 @@ async function runVerify(packageSpec) {
|
|
|
464
455
|
}
|
|
465
456
|
|
|
466
457
|
const packageDir = path.join(extractDir, 'package');
|
|
467
|
-
const
|
|
458
|
+
const npmReadmePath = path.join(packageDir, 'README.md');
|
|
468
459
|
|
|
469
|
-
if (!fs.existsSync(
|
|
470
|
-
fail(`❌ README.md not found in package
|
|
460
|
+
if (!fs.existsSync(npmReadmePath)) {
|
|
461
|
+
fail(`❌ README.md not found in npm package`);
|
|
471
462
|
}
|
|
472
463
|
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
if (!manifest || !manifest.origin || !manifest.tag || !manifest.hash || !manifest.signature) {
|
|
464
|
+
const npmReadme = fs.readFileSync(npmReadmePath, 'utf8');
|
|
465
|
+
manifest = findLastJsonBlock(npmReadme);
|
|
466
|
+
if (!manifest || !manifest.origin || !manifest.tag || !manifest.hash || !manifest.signature || !manifest.name || !manifest.email) {
|
|
476
467
|
fail(`❌ Manifest not found or invalid in README`);
|
|
477
468
|
}
|
|
478
469
|
|
|
479
|
-
const { origin: originUrl, tag,
|
|
470
|
+
const { origin: originUrl, tag, signature, name, email } = manifest;
|
|
480
471
|
|
|
472
|
+
log.info(`🌿 Cloning git origin at tag ${tag}...`);
|
|
481
473
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `hipp-verify-git-`));
|
|
482
474
|
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), `hipp-verify-stage-`));
|
|
483
475
|
|
|
484
476
|
try {
|
|
485
|
-
log.info(`🌿 Fetching from git origin at tag ${tag}...`);
|
|
486
477
|
git(['clone', '--branch', tag, '--depth', '1', originUrl, tmpDir], { stdio: 'pipe' });
|
|
487
478
|
|
|
488
479
|
const publicKeyPath = path.join(tmpDir, 'hipp.pub');
|
|
@@ -496,24 +487,57 @@ async function runVerify(packageSpec) {
|
|
|
496
487
|
const trackedFiles = getTrackedFilesFromDir(tmpDir);
|
|
497
488
|
copyTrackedFilesFromDir(stageDir, tmpDir, trackedFiles);
|
|
498
489
|
|
|
499
|
-
log.info(`📦 Packing
|
|
500
|
-
const { tarballHash } = packAndHash(stageDir);
|
|
490
|
+
log.info(`📦 Packing clean git files...`);
|
|
491
|
+
const { tarballHash: cleanHash } = packAndHash(stageDir);
|
|
492
|
+
log.success(`📦 Clean hash: ${cleanHash.slice(0, 12)}...`);
|
|
501
493
|
|
|
502
|
-
|
|
503
|
-
|
|
494
|
+
log.info(`🔍 Check 2: Verifying manifest hash...`);
|
|
495
|
+
if (cleanHash !== manifest.hash) {
|
|
496
|
+
fail(`❌ Manifest hash mismatch: clean git tarball does not match manifest`);
|
|
504
497
|
}
|
|
498
|
+
log.success(`🔒 Manifest hash verified`);
|
|
505
499
|
|
|
506
|
-
log.
|
|
507
|
-
|
|
508
|
-
const signData = buildSignData(npmHash, originUrl, tag);
|
|
500
|
+
log.info(`🔍 Check 1: Verifying signature...`);
|
|
501
|
+
const signData = buildSignData(manifest.hash, originUrl, tag, name, email);
|
|
509
502
|
const signatureValid = verifySignature(signData, signature, publicKey);
|
|
510
|
-
|
|
511
503
|
if (!signatureValid) {
|
|
512
504
|
fail(`❌ Signature verification failed`);
|
|
513
505
|
}
|
|
514
|
-
|
|
515
506
|
log.success(`🔏 Signature verified`);
|
|
516
|
-
|
|
507
|
+
|
|
508
|
+
log.info(`🔍 Check 3: Rebuilding from source...`);
|
|
509
|
+
const stagedReadmePath = path.join(stageDir, 'README.md');
|
|
510
|
+
let stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
511
|
+
const tagVersion = semver.clean(tag);
|
|
512
|
+
if (!tagVersion) {
|
|
513
|
+
fail(`❌ Tag ${tag} is not valid semver`);
|
|
514
|
+
}
|
|
515
|
+
stagedReadme = stagedReadme.trimEnd() + '\n\n## Verify\n\n' +
|
|
516
|
+
'Verify this package with [@dk/hipp](https://www.npmjs.com/package/@dk/hipp):\n\n' +
|
|
517
|
+
'```bash\n' +
|
|
518
|
+
`npx @dk/hipp verify ${pkgName}@${tagVersion}\n` +
|
|
519
|
+
'```\n\n' +
|
|
520
|
+
'```json\n' + JSON.stringify(manifest, null, 2) + '\n```\n';
|
|
521
|
+
fs.writeFileSync(stagedReadmePath, stagedReadme);
|
|
522
|
+
|
|
523
|
+
const stagedPkgPath = path.join(stageDir, 'package.json');
|
|
524
|
+
const stagedPkg = JSON.parse(fs.readFileSync(stagedPkgPath, 'utf8'));
|
|
525
|
+
stagedPkg.version = tagVersion;
|
|
526
|
+
fs.writeFileSync(stagedPkgPath, JSON.stringify(stagedPkg, null, 2) + '\n');
|
|
527
|
+
|
|
528
|
+
const { tarballHash: rebuildHash } = packAndHash(stageDir);
|
|
529
|
+
log.success(`📦 Rebuild hash: ${rebuildHash.slice(0, 12)}...`);
|
|
530
|
+
|
|
531
|
+
if (rebuildHash !== npmHash) {
|
|
532
|
+
log.error(`❌ Rebuild mismatch!`);
|
|
533
|
+
log.error(` NPM tarball: ${npmHash}`);
|
|
534
|
+
log.error(` Git rebuild: ${rebuildHash}`);
|
|
535
|
+
fail(`❌ Package integrity compromised`);
|
|
536
|
+
}
|
|
537
|
+
log.success(`🔄 Rebuild verified`);
|
|
538
|
+
|
|
539
|
+
log.success(`✅ Verified: all checks passed`);
|
|
540
|
+
log.info(`📍 Publisher: ${name} <${email}>`);
|
|
517
541
|
log.info(`📍 Origin: ${originUrl}`);
|
|
518
542
|
log.info(`📍 Tag: ${tag}`);
|
|
519
543
|
} finally {
|
|
@@ -613,7 +637,8 @@ async function run() {
|
|
|
613
637
|
stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
614
638
|
}
|
|
615
639
|
|
|
616
|
-
const
|
|
640
|
+
const { name, email } = getGitUserInfo();
|
|
641
|
+
const dataToSign = buildSignData(tarballHash, provenance.remoteUrl, rawTag, name, email);
|
|
617
642
|
const signature = signContent(dataToSign, privateKey);
|
|
618
643
|
|
|
619
644
|
const manifestJson = {
|
|
@@ -621,9 +646,16 @@ async function run() {
|
|
|
621
646
|
tag: rawTag,
|
|
622
647
|
hash: tarballHash,
|
|
623
648
|
signature: signature,
|
|
649
|
+
name: name,
|
|
650
|
+
email: email,
|
|
624
651
|
};
|
|
625
652
|
|
|
626
|
-
stagedReadme = stagedReadme.trimEnd() + '\n\n
|
|
653
|
+
stagedReadme = stagedReadme.trimEnd() + '\n\n## Verify\n\n' +
|
|
654
|
+
'Verify this package with [@dk/hipp](https://www.npmjs.com/package/@dk/hipp):\n\n' +
|
|
655
|
+
'```bash\n' +
|
|
656
|
+
`npx @dk/hipp verify ${pkg.name}@${version}\n` +
|
|
657
|
+
'```\n\n' +
|
|
658
|
+
'```json\n' + JSON.stringify(manifestJson, null, 2) + '\n```\n';
|
|
627
659
|
fs.writeFileSync(stagedReadmePath, stagedReadme);
|
|
628
660
|
|
|
629
661
|
const stagedPkgPath = path.join(stageDir, 'package.json');
|
|
@@ -666,19 +698,33 @@ const isVerify = process.argv.includes('verify');
|
|
|
666
698
|
const verifyIndex = process.argv.indexOf('verify');
|
|
667
699
|
const packageSpec = verifyIndex !== -1 ? process.argv[verifyIndex + 1] : null;
|
|
668
700
|
|
|
669
|
-
if (isVerify
|
|
670
|
-
|
|
701
|
+
if (isVerify) {
|
|
702
|
+
const specToVerify = packageSpec;
|
|
703
|
+
if (specToVerify) {
|
|
704
|
+
runVerify(specToVerify);
|
|
705
|
+
} else {
|
|
706
|
+
const hippPkgPath = path.join(path.dirname(process.argv[1]), 'package.json');
|
|
707
|
+
const hippPkg = JSON.parse(fs.readFileSync(hippPkgPath, 'utf8'));
|
|
708
|
+
runVerify(`${hippPkg.name}@${hippPkg.version}`);
|
|
709
|
+
}
|
|
671
710
|
} else if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
672
711
|
console.log(`\x1b[36mHIPP - High Integrity Package Publisher\x1b[0m
|
|
673
712
|
|
|
674
713
|
Usage:
|
|
675
714
|
npx hipp [options] [-- npm-options]
|
|
676
|
-
npx hipp verify
|
|
715
|
+
npx hipp verify [@package[@version]]
|
|
716
|
+
|
|
717
|
+
Without arguments, verifies the installed hipp version.
|
|
677
718
|
|
|
678
719
|
Options:
|
|
679
720
|
-y, --yes Skip confirmation prompt
|
|
680
721
|
-h, --help Show this help
|
|
681
722
|
|
|
723
|
+
Verify: Downloads npm tarball, clones git at tag, runs all three verification checks:
|
|
724
|
+
1. Signature verification (manifest signed by private key)
|
|
725
|
+
2. Manifest hash (clean git tarball matches manifest hash)
|
|
726
|
+
3. Rebuild verification (npm tarball equals git rebuild with manifest+version)
|
|
727
|
+
|
|
682
728
|
Integrity rules:
|
|
683
729
|
- package.json version must be 0.0.0
|
|
684
730
|
- package-lock.json must exist and be tracked
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dk/hipp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "High Integrity Package Publisher",
|
|
5
5
|
"main": "hipp.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"author": "Dmytri Kleiner <dev@dmytri.to>",
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"npm-package-arg": "^13.0.2",
|
|
14
15
|
"semver": ">=7.6.0"
|
|
15
16
|
},
|
|
16
17
|
"engines": {
|