@dk/hipp 0.1.6 → 0.1.8
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 +104 -24
- package/hipp.js +327 -1
- package/hipp.pub +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HIPP: High Integrity Package Publisher
|
|
2
2
|
|
|
3
3
|
By Dmytri Kleiner <dev@dmytri.to>
|
|
4
4
|
|
|
@@ -7,43 +7,51 @@ friction of version-bump commits. It treats your **Git Tags** as the single
|
|
|
7
7
|
source of truth, enforcing a "Ground State" where your `package.json` version
|
|
8
8
|
remains permanently at `0.0.0`.
|
|
9
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.
|
|
12
|
+
|
|
10
13
|
---
|
|
11
14
|
|
|
12
|
-
##
|
|
15
|
+
## Why HIPP?
|
|
16
|
+
|
|
17
|
+
### Integrity
|
|
13
18
|
|
|
14
19
|
Traditional NPM versioning requires you to store your "Version of Truth" inside
|
|
15
20
|
your source code files (`package.json`). This creates a **State Conflict** that
|
|
16
21
|
leads to several systemic problems:
|
|
17
22
|
|
|
18
|
-
### 1. Integrity Failure in standard workflow
|
|
19
23
|
`npm version` and `git tag` are two distinct, non-atomic actions. If you tag a
|
|
20
24
|
commit but forget to update the JSON (or vice-versa), your registry package and
|
|
21
25
|
your Git history diverge. This scenario makes it impossible to guarantee that
|
|
22
|
-
the code in the registry matches the code at that tag.
|
|
26
|
+
the code in the registry matches the code at that tag.
|
|
23
27
|
|
|
24
28
|
**HIPP ensures they are fundamentally linked by extracting the version directly
|
|
25
|
-
from the Git Tag.**
|
|
29
|
+
from the Git Tag, and cryptographically signing the package contents.**
|
|
30
|
+
|
|
31
|
+
### No "Chore" Noise
|
|
26
32
|
|
|
27
|
-
### 2. Chore" Noise & Merge Conflicts
|
|
28
33
|
Every release usually requires a "chore: bump version" commit. When multiple
|
|
29
34
|
branches are developed simultaneously, these version changes cause constant,
|
|
30
|
-
trivial merge conflicts.
|
|
35
|
+
trivial merge conflicts.
|
|
31
36
|
|
|
32
37
|
**HIPP makes your `package.json` version immutable (0.0.0), so it never
|
|
33
38
|
conflicts and your git history stays clean.**
|
|
34
39
|
|
|
35
40
|
---
|
|
36
41
|
|
|
37
|
-
##
|
|
42
|
+
## Usage
|
|
38
43
|
|
|
39
|
-
###
|
|
40
|
-
|
|
44
|
+
### Setup
|
|
45
|
+
|
|
46
|
+
1. Set your project's `package.json` version to `0.0.0`. This is the **HIPP Doctrine**.
|
|
41
47
|
|
|
42
48
|
```json
|
|
43
49
|
{ "name": "your-package", "version": "0.0.0" }
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
2. Ensure `package-lock.json` exists and is tracked by git.
|
|
53
|
+
|
|
54
|
+
### Tag and Publish
|
|
47
55
|
|
|
48
56
|
```bash
|
|
49
57
|
git tag v1.0.0
|
|
@@ -51,26 +59,82 @@ npx @dk/hipp
|
|
|
51
59
|
```
|
|
52
60
|
|
|
53
61
|
HIPP will:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
3.
|
|
58
|
-
4.
|
|
59
|
-
5.
|
|
60
|
-
|
|
62
|
+
|
|
63
|
+
1. **Key Generation**: Generate Ed25519 signing keys if needed (`hipp.priv`, `hipp.pub`)
|
|
64
|
+
2. **Verify**: Ensure the `0.0.0` doctrine is being followed
|
|
65
|
+
3. **Clean Check**: Ensure your git status is clean
|
|
66
|
+
4. **Validate**: Extract and verify the latest tag against Semver rules
|
|
67
|
+
5. **Sign**: Create a cryptographic manifest of your package content
|
|
68
|
+
6. **Publish**: Publish to npm from a staging directory (never mutating your source)
|
|
69
|
+
7. **Confirm**: Ask for confirmation before ignition (skip with `-y`)
|
|
70
|
+
|
|
71
|
+
### Signing Keys
|
|
72
|
+
|
|
73
|
+
On first run, HIPP generates an Ed25519 keypair:
|
|
74
|
+
|
|
75
|
+
- **`hipp.priv`** - Your private signing key. **Never committed to git.** Added to `.gitignore` automatically.
|
|
76
|
+
- **`hipp.pub`** - Your public verification key. **Committed to git** automatically.
|
|
77
|
+
|
|
78
|
+
The private key holder can sign packages. The public key verifies signatures.
|
|
61
79
|
|
|
62
80
|
### Options
|
|
63
|
-
* `-y, --yes`: Skip the confirmation (ideal for CI/CD pipelines).
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
* `-y, --yes`: Skip the confirmation prompt (ideal for CI/CD pipelines).
|
|
83
|
+
|
|
84
|
+
To pass additional flags to npm publish (like access or a custom registry), use `--`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @dk/hipp -- --access public --tag beta
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Verification
|
|
67
93
|
|
|
68
|
-
|
|
94
|
+
HIPP provides out-of-band verification to guarantee package integrity:
|
|
69
95
|
|
|
96
|
+
```bash
|
|
97
|
+
npx @dk/hipp verify @dk/your-package[@version]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### How Verification Works
|
|
101
|
+
|
|
102
|
+
1. **Fetch from npm**: Downloads the package tarball and extracts the manifest
|
|
103
|
+
2. **Fetch from git**: Clones the repository at the tagged commit and extracts the public key
|
|
104
|
+
3. **Hash Verification**: Computes the hash of the git content and compares with the manifest
|
|
105
|
+
4. **Signature Verification**: Verifies the manifest signature using the public key
|
|
106
|
+
|
|
107
|
+
If both checks pass, you can be certain that:
|
|
108
|
+
- The package contents in npm exactly match the git tag
|
|
109
|
+
- The manifest was signed by the holder of the private key
|
|
110
|
+
|
|
111
|
+
### Integrity Rules
|
|
112
|
+
|
|
113
|
+
HIPP enforces strict integrity rules:
|
|
114
|
+
|
|
115
|
+
- `package.json` version must be `0.0.0`
|
|
116
|
+
- `package-lock.json` must exist and be tracked by git
|
|
117
|
+
- `npm ci --ignore-scripts --dry-run` must succeed
|
|
118
|
+
- Repository must be clean
|
|
119
|
+
- HEAD must be on a branch with an upstream
|
|
120
|
+
- HEAD must exactly match upstream
|
|
121
|
+
- HEAD must have an exact v-prefixed semver tag
|
|
122
|
+
- The exact tag must exist on origin and match locally
|
|
123
|
+
- HEAD commit must be contained in an origin remote branch
|
|
124
|
+
- Only git-tracked files are staged
|
|
125
|
+
- Only staged `package.json` is rewritten
|
|
70
126
|
|
|
71
127
|
---
|
|
72
128
|
|
|
73
|
-
##
|
|
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
|
+
## License
|
|
74
138
|
|
|
75
139
|
**0BSD** (BSD Zero Clause License) By Dmytri Kleiner <dev@dmytri.to>
|
|
76
140
|
|
|
@@ -83,5 +147,21 @@ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
|
83
147
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
84
148
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
85
149
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
86
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
150
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"origin": "git@github.com:dmytri/hipp.git",
|
|
154
|
+
"tag": "v0.1.8"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```npx @dk/hipp @dk/hipp@0.1.8```
|
|
87
159
|
|
|
160
|
+
<!-- HIPP-MANIFEST -->
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"hash": "57d56d76a120bbc755700c3aa1f91f756e2cda09076dc0df522beb6a22018dca",
|
|
164
|
+
"signature": "eclO/AvbMfQMGS7g/vwX1DbB3dbm4XxWvlMFjo06KpE+d3w7KagtIv9klyjTazls74yGy3qcHhoy3i6/zWmZAg=="
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
<!-- /HIPP-MANIFEST -->
|
package/hipp.js
CHANGED
|
@@ -42,6 +42,162 @@ function sha256(input) {
|
|
|
42
42
|
return crypto.createHash('sha256').update(input).digest('hex');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function getPrivateKeyPath() {
|
|
46
|
+
return path.join(process.cwd(), 'hipp.priv');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getPublicKeyPath() {
|
|
50
|
+
return path.join(process.cwd(), 'hipp.pub');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function generateKeyPair() {
|
|
54
|
+
const { generateKeyPairSync } = crypto;
|
|
55
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
|
|
56
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
57
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
58
|
+
});
|
|
59
|
+
return { privateKey, publicKey };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadOrGenerateKeys() {
|
|
63
|
+
const privPath = getPrivateKeyPath();
|
|
64
|
+
const pubPath = getPublicKeyPath();
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(privPath) && fs.existsSync(pubPath)) {
|
|
67
|
+
return {
|
|
68
|
+
privateKey: fs.readFileSync(privPath, 'utf8'),
|
|
69
|
+
publicKey: fs.readFileSync(pubPath, 'utf8'),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
log.info('🔑 Generating Ed25519 keypair...');
|
|
74
|
+
const { privateKey, publicKey } = generateKeyPair();
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(privPath, privateKey, { mode: 0o600 });
|
|
77
|
+
fs.writeFileSync(pubPath, publicKey);
|
|
78
|
+
|
|
79
|
+
log.success('🔑 Keypair generated.');
|
|
80
|
+
|
|
81
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
82
|
+
let gitignore = '';
|
|
83
|
+
if (fs.existsSync(gitignorePath)) {
|
|
84
|
+
gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
if (!gitignore.includes('hipp.priv')) {
|
|
87
|
+
fs.writeFileSync(gitignorePath, gitignore.trimEnd() + '\nhipp.priv\n');
|
|
88
|
+
log.info('📝 Added hipp.priv to .gitignore');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { privateKey, publicKey };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function signContent(data, privateKey) {
|
|
95
|
+
const signature = crypto.sign(null, Buffer.from(data), {
|
|
96
|
+
key: privateKey,
|
|
97
|
+
dsaEncoding: 'ieee-p1363',
|
|
98
|
+
});
|
|
99
|
+
return signature.toString('base64');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function verifySignature(data, signature, publicKey) {
|
|
103
|
+
const verify = crypto.verify;
|
|
104
|
+
return verify(null, Buffer.from(data), {
|
|
105
|
+
key: publicKey,
|
|
106
|
+
dsaEncoding: 'ieee-p1363',
|
|
107
|
+
}, Buffer.from(signature, 'base64'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createManifest(hash, signature) {
|
|
111
|
+
return JSON.stringify({ hash, signature }, null, 2);
|
|
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
|
+
}
|
|
125
|
+
|
|
126
|
+
const MANIFEST_START = '<!-- HIPP-MANIFEST -->';
|
|
127
|
+
const MANIFEST_END = '<!-- /HIPP-MANIFEST -->';
|
|
128
|
+
|
|
129
|
+
function appendManifestToReadme(readmeContent, manifest) {
|
|
130
|
+
return `${readmeContent}${MANIFEST_START}\n\`\`\`json\n${manifest}\n\`\`\`\n${MANIFEST_END}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractManifestFromReadme(readmeContent) {
|
|
134
|
+
const startIdx = readmeContent.indexOf(MANIFEST_START);
|
|
135
|
+
if (startIdx === -1) return null;
|
|
136
|
+
const endIdx = readmeContent.indexOf(MANIFEST_END, startIdx);
|
|
137
|
+
if (endIdx === -1) return null;
|
|
138
|
+
const content = readmeContent.slice(startIdx + MANIFEST_START.length, endIdx).trim();
|
|
139
|
+
const match = content.match(/^```json\n(.+)\n```$/s);
|
|
140
|
+
if (!match) return null;
|
|
141
|
+
return match[1];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stripManifestFromReadme(readmeContent) {
|
|
145
|
+
const startIdx = readmeContent.indexOf(MANIFEST_START);
|
|
146
|
+
const endIdx = readmeContent.indexOf(MANIFEST_END, startIdx);
|
|
147
|
+
if (startIdx === -1 || endIdx === -1) return readmeContent;
|
|
148
|
+
|
|
149
|
+
const endLineIdx = readmeContent.indexOf('\n', endIdx);
|
|
150
|
+
const endOfManifest = endLineIdx !== -1 ? endLineIdx + 1 : readmeContent.length;
|
|
151
|
+
|
|
152
|
+
const beforeWithNewline = readmeContent.slice(0, startIdx);
|
|
153
|
+
const lastNewlineBefore = beforeWithNewline.lastIndexOf('\n');
|
|
154
|
+
const before = lastNewlineBefore !== -1 ? beforeWithNewline.slice(0, lastNewlineBefore) : beforeWithNewline;
|
|
155
|
+
|
|
156
|
+
const after = readmeContent.slice(endOfManifest);
|
|
157
|
+
return before + '\n' + after;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function computeReadmeHash(readmeContent) {
|
|
161
|
+
const stripped = stripManifestFromReadme(readmeContent);
|
|
162
|
+
return sha256(stripped);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractJsonMetaFromReadme(readmeContent) {
|
|
166
|
+
const lines = readmeContent.split('\n');
|
|
167
|
+
let jsonStart = -1;
|
|
168
|
+
let braceCount = 0;
|
|
169
|
+
let inJson = false;
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
172
|
+
const line = lines[i];
|
|
173
|
+
if (line === '```json') {
|
|
174
|
+
jsonStart = i + 1;
|
|
175
|
+
inJson = true;
|
|
176
|
+
braceCount = 0;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (inJson) {
|
|
180
|
+
for (const char of line) {
|
|
181
|
+
if (char === '{') braceCount++;
|
|
182
|
+
if (char === '}') braceCount--;
|
|
183
|
+
}
|
|
184
|
+
if (braceCount === 0 && line.includes('}')) {
|
|
185
|
+
const jsonStr = lines.slice(jsonStart, i + 1).join('\n');
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(jsonStr);
|
|
188
|
+
if (parsed.origin && parsed.tag) {
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// continue searching
|
|
193
|
+
}
|
|
194
|
+
inJson = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
45
201
|
function safeStageName(name) {
|
|
46
202
|
return name.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
47
203
|
}
|
|
@@ -237,6 +393,136 @@ function copyTrackedFiles(stageDir, files) {
|
|
|
237
393
|
}
|
|
238
394
|
}
|
|
239
395
|
|
|
396
|
+
async function runVerify(packageSpec) {
|
|
397
|
+
let pkgName, pkgVersion;
|
|
398
|
+
if (packageSpec.startsWith('@')) {
|
|
399
|
+
const atIndex = packageSpec.indexOf('@', 1);
|
|
400
|
+
if (atIndex === -1) {
|
|
401
|
+
pkgName = packageSpec;
|
|
402
|
+
pkgVersion = undefined;
|
|
403
|
+
} else {
|
|
404
|
+
pkgName = packageSpec.slice(0, atIndex);
|
|
405
|
+
pkgVersion = packageSpec.slice(atIndex + 1);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
const atIndex = packageSpec.indexOf('@');
|
|
409
|
+
if (atIndex === -1) {
|
|
410
|
+
pkgName = packageSpec;
|
|
411
|
+
pkgVersion = undefined;
|
|
412
|
+
} else {
|
|
413
|
+
pkgName = packageSpec.slice(0, atIndex);
|
|
414
|
+
pkgVersion = packageSpec.slice(atIndex + 1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
log.info(`🔍 HIPP Verify: ${pkgName}${pkgVersion ? '@' + pkgVersion : ''}`);
|
|
418
|
+
|
|
419
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${pkgVersion || 'latest'}`;
|
|
420
|
+
|
|
421
|
+
log.info(`📦 Fetching from npm...`);
|
|
422
|
+
const registryJson = runCmd('curl', ['-s', '-L', registryUrl]);
|
|
423
|
+
let tarballUrl;
|
|
424
|
+
try {
|
|
425
|
+
const json = JSON.parse(registryJson.stdout.trim());
|
|
426
|
+
tarballUrl = json.dist.tarball;
|
|
427
|
+
} catch {
|
|
428
|
+
fail(`❌ Failed to parse npm registry response for ${pkgName}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const tarballPath = path.join(os.tmpdir(), `hipp-verify-${safeStageName(pkgName)}-tgz`);
|
|
432
|
+
const extractDir = path.join(os.tmpdir(), `hipp-verify-extract-${safeStageName(pkgName)}`);
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
log.info(`📦 Downloading tarball from ${tarballUrl}...`);
|
|
436
|
+
const curlResult = runCmd('curl', ['-s', '-L', '-o', tarballPath, tarballUrl]);
|
|
437
|
+
if (curlResult.status !== 0) {
|
|
438
|
+
fail(`❌ Failed to download tarball`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (fs.existsSync(extractDir)) {
|
|
442
|
+
fs.rmSync(extractDir, { recursive: true });
|
|
443
|
+
}
|
|
444
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
445
|
+
|
|
446
|
+
log.info(`📦 Extracting tarball...`);
|
|
447
|
+
const tarResult = spawnSync('tar', ['-xzf', tarballPath, '-C', extractDir], { encoding: 'utf8', stdio: 'pipe' });
|
|
448
|
+
if (tarResult.status !== 0) {
|
|
449
|
+
fail(`❌ Failed to extract tarball: ${tarResult.stderr}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const packageDir = path.join(extractDir, 'package');
|
|
453
|
+
const stagedReadmePath = path.join(packageDir, 'README.md');
|
|
454
|
+
|
|
455
|
+
if (!fs.existsSync(stagedReadmePath)) {
|
|
456
|
+
fail(`❌ README.md not found in package at ${stagedReadmePath}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
460
|
+
const jsonMeta = extractJsonMetaFromReadme(stagedReadme);
|
|
461
|
+
if (!jsonMeta || !jsonMeta.origin || !jsonMeta.tag) {
|
|
462
|
+
fail(`❌ JSON meta (origin/tag) not found in README`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const manifestBase64 = extractManifestFromReadme(stagedReadme);
|
|
466
|
+
if (!manifestBase64) {
|
|
467
|
+
fail(`❌ Manifest not found in README`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const manifest = parseManifest(manifestBase64);
|
|
471
|
+
if (!manifest || !manifest.hash || !manifest.signature) {
|
|
472
|
+
fail(`❌ Invalid manifest format`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `hipp-verify-git-`));
|
|
476
|
+
try {
|
|
477
|
+
log.info(`🌿 Fetching from git origin...`);
|
|
478
|
+
|
|
479
|
+
const originUrl = jsonMeta.origin;
|
|
480
|
+
const tag = jsonMeta.tag;
|
|
481
|
+
|
|
482
|
+
git(['clone', '--branch', tag, '--depth', '1', originUrl, tmpDir], { stdio: 'pipe' });
|
|
483
|
+
|
|
484
|
+
const clonedReadmePath = path.join(tmpDir, 'README.md');
|
|
485
|
+
if (!fs.existsSync(clonedReadmePath)) {
|
|
486
|
+
fail(`❌ README.md not found in git at tag ${tag}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const clonedReadme = fs.readFileSync(clonedReadmePath, 'utf8');
|
|
490
|
+
const clonedHash = computeReadmeHash(clonedReadme);
|
|
491
|
+
|
|
492
|
+
if (clonedHash !== manifest.hash) {
|
|
493
|
+
fail(`❌ Hash mismatch: git content does not match npm manifest`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
log.success(`🔒 Content hash verified: ${manifest.hash.slice(0, 12)}...`);
|
|
497
|
+
|
|
498
|
+
const publicKeyPath = path.join(tmpDir, 'hipp.pub');
|
|
499
|
+
if (!fs.existsSync(publicKeyPath)) {
|
|
500
|
+
fail(`❌ hipp.pub not found in git at tag ${tag}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
504
|
+
const signData = buildSignData(manifest.hash, originUrl, tag);
|
|
505
|
+
const signatureValid = verifySignature(signData, manifest.signature, publicKey);
|
|
506
|
+
|
|
507
|
+
if (!signatureValid) {
|
|
508
|
+
fail(`❌ Signature verification failed`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
log.success(`🔏 Signature verified`);
|
|
512
|
+
log.success(`✅ Package ${pkgName} verified successfully!`);
|
|
513
|
+
log.info(`📍 Origin: ${originUrl}`);
|
|
514
|
+
log.info(`📍 Tag: ${tag}`);
|
|
515
|
+
} finally {
|
|
516
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
fs.rmSync(tarballPath, { recursive: true, force: true });
|
|
520
|
+
if (fs.existsSync(extractDir)) {
|
|
521
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
240
526
|
async function confirmPrompt(name, version) {
|
|
241
527
|
const rl = readline.createInterface({
|
|
242
528
|
input: process.stdin,
|
|
@@ -266,6 +552,19 @@ async function run() {
|
|
|
266
552
|
}
|
|
267
553
|
|
|
268
554
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
555
|
+
|
|
556
|
+
loadOrGenerateKeys();
|
|
557
|
+
|
|
558
|
+
const pubPath = getPublicKeyPath();
|
|
559
|
+
try {
|
|
560
|
+
git(['ls-files', '--error-unmatch', 'hipp.pub']);
|
|
561
|
+
} catch {
|
|
562
|
+
log.info('📝 Committing hipp.pub to repo...');
|
|
563
|
+
git(['add', 'hipp.pub']);
|
|
564
|
+
git(['commit', '-m', 'Add hipp public key for package signing']);
|
|
565
|
+
log.success('📝 hipp.pub committed.');
|
|
566
|
+
}
|
|
567
|
+
|
|
269
568
|
ensureCleanRepo(pkg);
|
|
270
569
|
|
|
271
570
|
const { rawTag, version } = getVersionFromExactTagOnHead();
|
|
@@ -296,11 +595,31 @@ async function run() {
|
|
|
296
595
|
log.info(`🏗️ Staging tracked files to ${stageDir}...`);
|
|
297
596
|
copyTrackedFiles(stageDir, trackedFiles);
|
|
298
597
|
|
|
598
|
+
const { privateKey } = loadOrGenerateKeys();
|
|
599
|
+
|
|
299
600
|
const stagedPkgPath = path.join(stageDir, 'package.json');
|
|
300
601
|
const stagedPkg = JSON.parse(fs.readFileSync(stagedPkgPath, 'utf8'));
|
|
301
602
|
stagedPkg.version = version;
|
|
302
603
|
fs.writeFileSync(stagedPkgPath, JSON.stringify(stagedPkg, null, 2) + '\n');
|
|
303
604
|
|
|
605
|
+
const stagedReadmePath = path.join(stageDir, 'README.md');
|
|
606
|
+
let stagedReadme = '';
|
|
607
|
+
if (fs.existsSync(stagedReadmePath)) {
|
|
608
|
+
stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
stagedReadme += '\n```json\n{\n "origin": "' + provenance.remoteUrl + '",\n "tag": "' + rawTag + '"\n}\n```\n\n```npx @dk/hipp ' + pkg.name + '@' + version + '```\n\n';
|
|
612
|
+
|
|
613
|
+
const readmeHash = computeReadmeHash(stagedReadme);
|
|
614
|
+
const dataToSign = buildSignData(readmeHash, provenance.remoteUrl, rawTag);
|
|
615
|
+
const signature = signContent(dataToSign, privateKey);
|
|
616
|
+
const manifest = createManifest(readmeHash, signature);
|
|
617
|
+
stagedReadme = appendManifestToReadme(stagedReadme, manifest);
|
|
618
|
+
|
|
619
|
+
fs.writeFileSync(stagedReadmePath, stagedReadme);
|
|
620
|
+
|
|
621
|
+
log.success('🔏 Manifest signed.');
|
|
622
|
+
|
|
304
623
|
log.info('🔥 Ignition...');
|
|
305
624
|
|
|
306
625
|
const result = spawnSync('npm', ['publish', ...npmArgs], {
|
|
@@ -330,11 +649,18 @@ async function run() {
|
|
|
330
649
|
}
|
|
331
650
|
}
|
|
332
651
|
|
|
333
|
-
|
|
652
|
+
const isVerify = process.argv.includes('verify');
|
|
653
|
+
const verifyIndex = process.argv.indexOf('verify');
|
|
654
|
+
const packageSpec = verifyIndex !== -1 ? process.argv[verifyIndex + 1] : null;
|
|
655
|
+
|
|
656
|
+
if (isVerify && packageSpec) {
|
|
657
|
+
runVerify(packageSpec);
|
|
658
|
+
} else if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
334
659
|
console.log(`\x1b[36mHIPP - High Integrity Package Publisher\x1b[0m
|
|
335
660
|
|
|
336
661
|
Usage:
|
|
337
662
|
npx hipp [options] [-- npm-options]
|
|
663
|
+
npx hipp verify <package>[@version]
|
|
338
664
|
|
|
339
665
|
Options:
|
|
340
666
|
-y, --yes Skip confirmation prompt
|
package/hipp.pub
ADDED