@dk/hipp 0.1.15 → 0.1.17
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 +112 -71
- package/hipp.js +98 -62
- 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
|
|
11
|
+
## The Problem
|
|
18
12
|
|
|
19
|
-
Traditional NPM versioning requires
|
|
20
|
-
|
|
21
|
-
leads to several systemic problems:
|
|
13
|
+
Traditional NPM versioning requires storing the "Version of Truth" in `package.json`.
|
|
14
|
+
This creates a **State Conflict**:
|
|
22
15
|
|
|
23
|
-
`npm version` and `git tag` are two distinct, non-atomic actions
|
|
24
|
-
commit but forget to update the JSON (or vice-versa), your
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
from the Git Tag, and cryptographically signing the package contents.**
|
|
23
|
+
## The Solution
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
**HIPP makes `package.json` version immutable (0.0.0)** - the **HIPP Doctrine**.
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
trivial merge conflicts.
|
|
36
|
-
|
|
37
|
-
**HIPP makes your `package.json` version immutable (0.0.0), so it never
|
|
38
|
-
conflicts and your git history stays clean.**
|
|
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,26 +90,97 @@ 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
|
|
101
101
|
|
|
102
|
-
1
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
**Step 1: Get manifest from npm**
|
|
103
|
+
|
|
104
|
+
1. Fetch the package tarball from npm registry
|
|
105
|
+
2. Extract the README and parse the JSON manifest appended to it
|
|
106
|
+
|
|
107
|
+
The manifest contains:
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"origin": "git@github.com:dk/your-package.git",
|
|
111
|
+
"tag": "v1.0.0",
|
|
112
|
+
"hash": "<sha256-of-tarball>",
|
|
113
|
+
"signature": "<base64-ed25519-signature>",
|
|
114
|
+
"name": "Jane Developer",
|
|
115
|
+
"email": "jane@example.com"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Step 2: Clone git and verify**
|
|
120
|
+
|
|
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
|
|
126
|
+
|
|
127
|
+
**Step 3: Verify signature**
|
|
128
|
+
|
|
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`
|
|
132
|
+
|
|
133
|
+
**Step 4: Rebuild verification**
|
|
134
|
+
|
|
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
|
|
138
|
+
|
|
139
|
+
### Three Verification Checks
|
|
140
|
+
|
|
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 |
|
|
146
|
+
|
|
147
|
+
### What Verification Guarantees
|
|
148
|
+
|
|
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.
|
|
160
|
+
|
|
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
|
|
106
176
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
110
180
|
|
|
111
181
|
### Integrity Rules
|
|
112
182
|
|
|
113
|
-
HIPP enforces strict integrity rules:
|
|
183
|
+
HIPP enforces strict integrity rules when publishing:
|
|
114
184
|
|
|
115
185
|
- `package.json` version must be `0.0.0`
|
|
116
186
|
- `package-lock.json` must exist and be tracked by git
|
|
@@ -126,14 +196,6 @@ HIPP enforces strict integrity rules:
|
|
|
126
196
|
|
|
127
197
|
---
|
|
128
198
|
|
|
129
|
-
## Security
|
|
130
|
-
|
|
131
|
-
HIPP uses **Ed25519** public-key signatures. The private key never leaves your
|
|
132
|
-
machine. The public key is distributed via git. Anyone can verify a signed
|
|
133
|
-
package, but only private key holders can publish.
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
199
|
## License
|
|
138
200
|
|
|
139
201
|
**0BSD** (BSD Zero Clause License) By Dmytri Kleiner <dev@dmytri.to>
|
|
@@ -149,34 +211,13 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
|
149
211
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
150
212
|
PERFORMANCE OF THIS SOFTWARE.
|
|
151
213
|
|
|
152
|
-
|
|
153
|
-
```json
|
|
154
|
-
{
|
|
155
|
-
"origin": "git@github.com:dmytri/hipp.git",
|
|
156
|
-
"tag": "v0.1.10"
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
```npx @dk/hipp @dk/hipp@0.1.10
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
<!-- HIPP-META -->
|
|
164
|
-
```json
|
|
165
|
-
{
|
|
166
|
-
"origin": "git@github.com:dmytri/hipp.git",
|
|
167
|
-
"tag": "v0.1.11"
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
```npx @dk/hipp @dk/hipp@0.1.11
|
|
172
|
-
```
|
|
173
|
-
<!-- /HIPP-META -->
|
|
174
|
-
|
|
175
214
|
```json
|
|
176
215
|
{
|
|
177
216
|
"origin": "git@github.com:dmytri/hipp.git",
|
|
178
|
-
"tag": "v0.1.
|
|
179
|
-
"hash": "
|
|
180
|
-
"signature": "
|
|
217
|
+
"tag": "v0.1.17",
|
|
218
|
+
"hash": "7a9817b5d57a5c5c43cac3d608d4a0e907dcbdf8031252a62b29195391b38b26",
|
|
219
|
+
"signature": "/o75TYcgRUS0pVQEmJeCamZ5z6wTDQtawuaKYd4lbZCBWuqEeuaPECRefTgzC7tDr6bCyFd6p4biPGWIroBlCA==",
|
|
220
|
+
"name": "Dmytri Kleiner",
|
|
221
|
+
"email": "dev@dmytri.to"
|
|
181
222
|
}
|
|
182
223
|
```
|
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,52 @@ 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
|
+
stagedReadme = stagedReadme.trimEnd() + '\n\n```json\n' + JSON.stringify(manifest, null, 2) + '\n```\n';
|
|
512
|
+
fs.writeFileSync(stagedReadmePath, stagedReadme);
|
|
513
|
+
|
|
514
|
+
const stagedPkgPath = path.join(stageDir, 'package.json');
|
|
515
|
+
const stagedPkg = JSON.parse(fs.readFileSync(stagedPkgPath, 'utf8'));
|
|
516
|
+
const tagVersion = semver.clean(tag);
|
|
517
|
+
if (!tagVersion) {
|
|
518
|
+
fail(`❌ Tag ${tag} is not valid semver`);
|
|
519
|
+
}
|
|
520
|
+
stagedPkg.version = tagVersion;
|
|
521
|
+
fs.writeFileSync(stagedPkgPath, JSON.stringify(stagedPkg, null, 2) + '\n');
|
|
522
|
+
|
|
523
|
+
const { tarballHash: rebuildHash } = packAndHash(stageDir);
|
|
524
|
+
log.success(`📦 Rebuild hash: ${rebuildHash.slice(0, 12)}...`);
|
|
525
|
+
|
|
526
|
+
if (rebuildHash !== npmHash) {
|
|
527
|
+
log.error(`❌ Rebuild mismatch!`);
|
|
528
|
+
log.error(` NPM tarball: ${npmHash}`);
|
|
529
|
+
log.error(` Git rebuild: ${rebuildHash}`);
|
|
530
|
+
fail(`❌ Package integrity compromised`);
|
|
531
|
+
}
|
|
532
|
+
log.success(`🔄 Rebuild verified`);
|
|
533
|
+
|
|
534
|
+
log.success(`✅ Verified: all checks passed`);
|
|
535
|
+
log.info(`📍 Publisher: ${name} <${email}>`);
|
|
517
536
|
log.info(`📍 Origin: ${originUrl}`);
|
|
518
537
|
log.info(`📍 Tag: ${tag}`);
|
|
519
538
|
} finally {
|
|
@@ -613,7 +632,8 @@ async function run() {
|
|
|
613
632
|
stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
614
633
|
}
|
|
615
634
|
|
|
616
|
-
const
|
|
635
|
+
const { name, email } = getGitUserInfo();
|
|
636
|
+
const dataToSign = buildSignData(tarballHash, provenance.remoteUrl, rawTag, name, email);
|
|
617
637
|
const signature = signContent(dataToSign, privateKey);
|
|
618
638
|
|
|
619
639
|
const manifestJson = {
|
|
@@ -621,6 +641,8 @@ async function run() {
|
|
|
621
641
|
tag: rawTag,
|
|
622
642
|
hash: tarballHash,
|
|
623
643
|
signature: signature,
|
|
644
|
+
name: name,
|
|
645
|
+
email: email,
|
|
624
646
|
};
|
|
625
647
|
|
|
626
648
|
stagedReadme = stagedReadme.trimEnd() + '\n\n```json\n' + JSON.stringify(manifestJson, null, 2) + '\n```\n';
|
|
@@ -666,19 +688,33 @@ const isVerify = process.argv.includes('verify');
|
|
|
666
688
|
const verifyIndex = process.argv.indexOf('verify');
|
|
667
689
|
const packageSpec = verifyIndex !== -1 ? process.argv[verifyIndex + 1] : null;
|
|
668
690
|
|
|
669
|
-
if (isVerify
|
|
670
|
-
|
|
691
|
+
if (isVerify) {
|
|
692
|
+
const specToVerify = packageSpec;
|
|
693
|
+
if (specToVerify) {
|
|
694
|
+
runVerify(specToVerify);
|
|
695
|
+
} else {
|
|
696
|
+
const hippPkgPath = path.join(path.dirname(process.argv[1]), 'package.json');
|
|
697
|
+
const hippPkg = JSON.parse(fs.readFileSync(hippPkgPath, 'utf8'));
|
|
698
|
+
runVerify(`${hippPkg.name}@${hippPkg.version}`);
|
|
699
|
+
}
|
|
671
700
|
} else if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
672
701
|
console.log(`\x1b[36mHIPP - High Integrity Package Publisher\x1b[0m
|
|
673
702
|
|
|
674
703
|
Usage:
|
|
675
704
|
npx hipp [options] [-- npm-options]
|
|
676
|
-
npx hipp verify
|
|
705
|
+
npx hipp verify [@package[@version]]
|
|
706
|
+
|
|
707
|
+
Without arguments, verifies the installed hipp version.
|
|
677
708
|
|
|
678
709
|
Options:
|
|
679
710
|
-y, --yes Skip confirmation prompt
|
|
680
711
|
-h, --help Show this help
|
|
681
712
|
|
|
713
|
+
Verify: Downloads npm tarball, clones git at tag, runs all three verification checks:
|
|
714
|
+
1. Signature verification (manifest signed by private key)
|
|
715
|
+
2. Manifest hash (clean git tarball matches manifest hash)
|
|
716
|
+
3. Rebuild verification (npm tarball equals git rebuild with manifest+version)
|
|
717
|
+
|
|
682
718
|
Integrity rules:
|
|
683
719
|
- package.json version must be 0.0.0
|
|
684
720
|
- 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.17",
|
|
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": {
|