@dk/hipp 0.1.5 → 0.1.7
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 +97 -24
- package/hipp.js +261 -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
|
|
43
|
+
|
|
44
|
+
### Setup
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
This is the **HIPP Doctrine**.
|
|
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
|
|
93
|
+
|
|
94
|
+
HIPP provides out-of-band verification to guarantee package integrity:
|
|
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
|
|
67
110
|
|
|
68
|
-
|
|
111
|
+
### Integrity Rules
|
|
69
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
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
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.
|
|
70
134
|
|
|
71
135
|
---
|
|
72
136
|
|
|
73
|
-
##
|
|
137
|
+
## License
|
|
74
138
|
|
|
75
139
|
**0BSD** (BSD Zero Clause License) By Dmytri Kleiner <dev@dmytri.to>
|
|
76
140
|
|
|
@@ -83,5 +147,14 @@ 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.7"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```npx @dk/hipp @dk/hipp@0.1.7```
|
|
87
159
|
|
|
160
|
+
<!-- HIPP-MANIFEST -->```eyJoYXNoIjoiODYxNDgxZmM3Yzk0ZTM4YTJhZjdjNDc3ZmJkMDcyMTA2YTMyYTI2MjdkYmM1ZTg4MTI1OTk1ODMyYjdlMzhhMiIsInNpZ25hdHVyZSI6IkVnUTBSZUMzU0hKanovdFZoWlY0V3U1SVJOMDJTSlFPWUtxOFBrdHFpcUhvWnRTbE5pQWxrN25nWDcySldxL0ZRa2JTVE10TldlZUNrSG9hczAwR0N3PT0ifQ==```<!-- /HIPP-MANIFEST -->
|
package/hipp.js
CHANGED
|
@@ -42,6 +42,126 @@ 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 Buffer.from(JSON.stringify({ hash, signature })).toString('base64');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseManifest(manifestBase64) {
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(Buffer.from(manifestBase64, 'base64').toString('utf8'));
|
|
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}\`\`\`${manifest}\`\`\`${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(/^```(.+)```$/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
|
+
|
|
45
165
|
function safeStageName(name) {
|
|
46
166
|
return name.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
47
167
|
}
|
|
@@ -237,6 +357,106 @@ function copyTrackedFiles(stageDir, files) {
|
|
|
237
357
|
}
|
|
238
358
|
}
|
|
239
359
|
|
|
360
|
+
async function runVerify(packageSpec) {
|
|
361
|
+
const [pkgName, pkgVersion] = packageSpec.split('@');
|
|
362
|
+
log.info(`🔍 HIPP Verify: ${pkgName}${pkgVersion ? '@' + pkgVersion : ''}`);
|
|
363
|
+
|
|
364
|
+
const registryUrl = 'https://registry.npmjs.org';
|
|
365
|
+
const fetchUrl = `${registryUrl}/${encodeURIComponent(pkgName)}/${pkgVersion ? pkgVersion : 'latest'}`;
|
|
366
|
+
|
|
367
|
+
log.info(`📦 Fetching from npm...`);
|
|
368
|
+
let tarballUrl;
|
|
369
|
+
try {
|
|
370
|
+
const fetchResult = runCmd('curl', ['-s', '-L', '-w', '%{url_effective}', '-o', '/dev/null', fetchUrl]);
|
|
371
|
+
tarballUrl = fetchResult.stdout.trim();
|
|
372
|
+
} catch {
|
|
373
|
+
fail(`❌ Failed to fetch package info for ${pkgName}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const tarballPath = path.join(os.tmpdir(), `hipp-verify-${safeStageName(pkgName)}-tgz`);
|
|
377
|
+
try {
|
|
378
|
+
log.info(`📦 Downloading tarball...`);
|
|
379
|
+
const curlResult = runCmd('curl', ['-s', '-L', '-o', tarballPath, tarballUrl]);
|
|
380
|
+
if (curlResult.status !== 0) {
|
|
381
|
+
fail(`❌ Failed to download tarball`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
log.info(`📦 Extracting tarball...`);
|
|
385
|
+
runCmd('tar', ['-xzf', tarballPath, '-C', os.tmpdir()], { stdio: 'pipe' });
|
|
386
|
+
|
|
387
|
+
const packageDir = path.join(os.tmpdir(), 'package');
|
|
388
|
+
const stagedReadmePath = path.join(packageDir, 'README.md');
|
|
389
|
+
|
|
390
|
+
if (!fs.existsSync(stagedReadmePath)) {
|
|
391
|
+
fail(`❌ README.md not found in package`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
395
|
+
const manifestBase64 = extractManifestFromReadme(stagedReadme);
|
|
396
|
+
if (!manifestBase64) {
|
|
397
|
+
fail(`❌ Manifest not found in README`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const manifest = parseManifest(manifestBase64);
|
|
401
|
+
if (!manifest || !manifest.hash || !manifest.signature) {
|
|
402
|
+
fail(`❌ Invalid manifest format`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
log.info(`📦 Extracting tarball to staging...`);
|
|
406
|
+
runCmd('tar', ['-xzf', tarballPath, '-C', os.tmpdir()], { stdio: 'pipe' });
|
|
407
|
+
|
|
408
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `hipp-verify-git-`));
|
|
409
|
+
try {
|
|
410
|
+
log.info(`🌿 Fetching from git origin...`);
|
|
411
|
+
|
|
412
|
+
const originUrl = manifest.origin;
|
|
413
|
+
const tag = manifest.tag;
|
|
414
|
+
|
|
415
|
+
git(['clone', '--branch', tag, '--depth', '1', originUrl, tmpDir], { stdio: 'pipe' });
|
|
416
|
+
|
|
417
|
+
const clonedReadmePath = path.join(tmpDir, 'README.md');
|
|
418
|
+
if (!fs.existsSync(clonedReadmePath)) {
|
|
419
|
+
fail(`❌ README.md not found in git at tag ${tag}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const clonedReadme = fs.readFileSync(clonedReadmePath, 'utf8');
|
|
423
|
+
const clonedHash = computeReadmeHash(clonedReadme);
|
|
424
|
+
|
|
425
|
+
if (clonedHash !== manifest.hash) {
|
|
426
|
+
fail(`❌ Hash mismatch: git content does not match npm manifest`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
log.success(`🔒 Content hash verified: ${manifest.hash.slice(0, 12)}...`);
|
|
430
|
+
|
|
431
|
+
const publicKeyPath = path.join(tmpDir, 'hipp.pub');
|
|
432
|
+
if (!fs.existsSync(publicKeyPath)) {
|
|
433
|
+
fail(`❌ hipp.pub not found in git at tag ${tag}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
437
|
+
const signData = buildSignData(manifest.hash, originUrl, tag);
|
|
438
|
+
const signatureValid = verifySignature(signData, manifest.signature, publicKey);
|
|
439
|
+
|
|
440
|
+
if (!signatureValid) {
|
|
441
|
+
fail(`❌ Signature verification failed`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
log.success(`🔏 Signature verified`);
|
|
445
|
+
log.success(`✅ Package ${pkgName} verified successfully!`);
|
|
446
|
+
log.info(`📍 Origin: ${originUrl}`);
|
|
447
|
+
log.info(`📍 Tag: ${tag}`);
|
|
448
|
+
} finally {
|
|
449
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
450
|
+
}
|
|
451
|
+
} finally {
|
|
452
|
+
fs.rmSync(tarballPath, { recursive: true, force: true });
|
|
453
|
+
const packageExtractDir = path.join(os.tmpdir(), 'package');
|
|
454
|
+
if (fs.existsSync(packageExtractDir)) {
|
|
455
|
+
fs.rmSync(packageExtractDir, { recursive: true, force: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
240
460
|
async function confirmPrompt(name, version) {
|
|
241
461
|
const rl = readline.createInterface({
|
|
242
462
|
input: process.stdin,
|
|
@@ -266,6 +486,19 @@ async function run() {
|
|
|
266
486
|
}
|
|
267
487
|
|
|
268
488
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
489
|
+
|
|
490
|
+
loadOrGenerateKeys();
|
|
491
|
+
|
|
492
|
+
const pubPath = getPublicKeyPath();
|
|
493
|
+
try {
|
|
494
|
+
git(['ls-files', '--error-unmatch', 'hipp.pub']);
|
|
495
|
+
} catch {
|
|
496
|
+
log.info('📝 Committing hipp.pub to repo...');
|
|
497
|
+
git(['add', 'hipp.pub']);
|
|
498
|
+
git(['commit', '-m', 'Add hipp public key for package signing']);
|
|
499
|
+
log.success('📝 hipp.pub committed.');
|
|
500
|
+
}
|
|
501
|
+
|
|
269
502
|
ensureCleanRepo(pkg);
|
|
270
503
|
|
|
271
504
|
const { rawTag, version } = getVersionFromExactTagOnHead();
|
|
@@ -296,11 +529,31 @@ async function run() {
|
|
|
296
529
|
log.info(`🏗️ Staging tracked files to ${stageDir}...`);
|
|
297
530
|
copyTrackedFiles(stageDir, trackedFiles);
|
|
298
531
|
|
|
532
|
+
const { privateKey } = loadOrGenerateKeys();
|
|
533
|
+
|
|
299
534
|
const stagedPkgPath = path.join(stageDir, 'package.json');
|
|
300
535
|
const stagedPkg = JSON.parse(fs.readFileSync(stagedPkgPath, 'utf8'));
|
|
301
536
|
stagedPkg.version = version;
|
|
302
537
|
fs.writeFileSync(stagedPkgPath, JSON.stringify(stagedPkg, null, 2) + '\n');
|
|
303
538
|
|
|
539
|
+
const stagedReadmePath = path.join(stageDir, 'README.md');
|
|
540
|
+
let stagedReadme = '';
|
|
541
|
+
if (fs.existsSync(stagedReadmePath)) {
|
|
542
|
+
stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
stagedReadme += '\n```json\n{\n "origin": "' + provenance.remoteUrl + '",\n "tag": "' + rawTag + '"\n}\n```\n\n```npx @dk/hipp ' + pkg.name + '@' + version + '```\n\n';
|
|
546
|
+
|
|
547
|
+
const readmeHash = computeReadmeHash(stagedReadme);
|
|
548
|
+
const dataToSign = buildSignData(readmeHash, provenance.remoteUrl, rawTag);
|
|
549
|
+
const signature = signContent(dataToSign, privateKey);
|
|
550
|
+
const manifest = createManifest(readmeHash, signature);
|
|
551
|
+
stagedReadme = appendManifestToReadme(stagedReadme, manifest);
|
|
552
|
+
|
|
553
|
+
fs.writeFileSync(stagedReadmePath, stagedReadme);
|
|
554
|
+
|
|
555
|
+
log.success('🔏 Manifest signed.');
|
|
556
|
+
|
|
304
557
|
log.info('🔥 Ignition...');
|
|
305
558
|
|
|
306
559
|
const result = spawnSync('npm', ['publish', ...npmArgs], {
|
|
@@ -330,11 +583,18 @@ async function run() {
|
|
|
330
583
|
}
|
|
331
584
|
}
|
|
332
585
|
|
|
333
|
-
|
|
586
|
+
const isVerify = process.argv.includes('verify');
|
|
587
|
+
const verifyIndex = process.argv.indexOf('verify');
|
|
588
|
+
const packageSpec = verifyIndex !== -1 ? process.argv[verifyIndex + 1] : null;
|
|
589
|
+
|
|
590
|
+
if (isVerify && packageSpec) {
|
|
591
|
+
runVerify(packageSpec);
|
|
592
|
+
} else if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
334
593
|
console.log(`\x1b[36mHIPP - High Integrity Package Publisher\x1b[0m
|
|
335
594
|
|
|
336
595
|
Usage:
|
|
337
596
|
npx hipp [options] [-- npm-options]
|
|
597
|
+
npx hipp verify <package>[@version]
|
|
338
598
|
|
|
339
599
|
Options:
|
|
340
600
|
-y, --yes Skip confirmation prompt
|
package/hipp.pub
ADDED