@dk/hipp 0.1.7 → 0.1.12
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 +33 -3
- package/hipp.js +152 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,13 +148,43 @@ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
|
148
148
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
149
149
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
150
150
|
PERFORMANCE OF THIS SOFTWARE.
|
|
151
|
+
|
|
152
|
+
|
|
151
153
|
```json
|
|
152
154
|
{
|
|
153
155
|
"origin": "git@github.com:dmytri/hipp.git",
|
|
154
|
-
"tag": "v0.1.
|
|
156
|
+
"tag": "v0.1.10"
|
|
155
157
|
}
|
|
156
158
|
```
|
|
157
159
|
|
|
158
|
-
```npx @dk/hipp @dk/hipp@0.1.
|
|
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 -->
|
|
159
174
|
|
|
160
|
-
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"origin": "git@github.com:dmytri/hipp.git",
|
|
178
|
+
"tag": "v0.1.12"
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```npx @dk/hipp @dk/hipp@0.1.12```
|
|
183
|
+
<!-- HIPP-MANIFEST -->
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"hash": "9213a0bc269441f5dc3dc07fe07eba4d127815ae78f0d7e07474547bb342ce1b",
|
|
187
|
+
"signature": "Bg5/in81S3ia4x4W2C1WzvXeZPxspCdXEcSHCyLT3aUaEF5JrSANcIerZXrgbAvDyrxsf9O2wDJ/0cZNI4KOAA=="
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
<!-- /HIPP-MANIFEST -->
|
package/hipp.js
CHANGED
|
@@ -108,12 +108,12 @@ function verifySignature(data, signature, publicKey) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
function createManifest(hash, signature) {
|
|
111
|
-
return
|
|
111
|
+
return JSON.stringify({ hash, signature }, null, 2);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
function parseManifest(
|
|
114
|
+
function parseManifest(manifestStr) {
|
|
115
115
|
try {
|
|
116
|
-
return JSON.parse(
|
|
116
|
+
return JSON.parse(manifestStr);
|
|
117
117
|
} catch {
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
@@ -127,7 +127,7 @@ const MANIFEST_START = '<!-- HIPP-MANIFEST -->';
|
|
|
127
127
|
const MANIFEST_END = '<!-- /HIPP-MANIFEST -->';
|
|
128
128
|
|
|
129
129
|
function appendManifestToReadme(readmeContent, manifest) {
|
|
130
|
-
return `${readmeContent}${MANIFEST_START}
|
|
130
|
+
return `${readmeContent}${MANIFEST_START}\n\`\`\`json\n${manifest}\n\`\`\`\n${MANIFEST_END}\n`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function extractManifestFromReadme(readmeContent) {
|
|
@@ -136,7 +136,7 @@ function extractManifestFromReadme(readmeContent) {
|
|
|
136
136
|
const endIdx = readmeContent.indexOf(MANIFEST_END, startIdx);
|
|
137
137
|
if (endIdx === -1) return null;
|
|
138
138
|
const content = readmeContent.slice(startIdx + MANIFEST_START.length, endIdx).trim();
|
|
139
|
-
const match = content.match(/^```(.+)```$/s);
|
|
139
|
+
const match = content.match(/^```json\n(.+)\n```$/s);
|
|
140
140
|
if (!match) return null;
|
|
141
141
|
return match[1];
|
|
142
142
|
}
|
|
@@ -162,6 +162,42 @@ function computeReadmeHash(readmeContent) {
|
|
|
162
162
|
return sha256(stripped);
|
|
163
163
|
}
|
|
164
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
|
+
|
|
165
201
|
function safeStageName(name) {
|
|
166
202
|
return name.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
167
203
|
}
|
|
@@ -336,6 +372,19 @@ function getTrackedFiles() {
|
|
|
336
372
|
.filter(Boolean);
|
|
337
373
|
}
|
|
338
374
|
|
|
375
|
+
function getTrackedFilesFromDir(repoDir) {
|
|
376
|
+
const out = execFileSync('git', ['ls-files', '-z'], {
|
|
377
|
+
encoding: 'buffer',
|
|
378
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
379
|
+
cwd: repoDir,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return out
|
|
383
|
+
.toString('utf8')
|
|
384
|
+
.split('\0')
|
|
385
|
+
.filter(Boolean);
|
|
386
|
+
}
|
|
387
|
+
|
|
339
388
|
function copyTrackedFiles(stageDir, files) {
|
|
340
389
|
const repoRoot = process.cwd();
|
|
341
390
|
|
|
@@ -357,83 +406,143 @@ function copyTrackedFiles(stageDir, files) {
|
|
|
357
406
|
}
|
|
358
407
|
}
|
|
359
408
|
|
|
409
|
+
function copyTrackedFilesFromDir(stageDir, repoDir, files) {
|
|
410
|
+
for (const rel of files) {
|
|
411
|
+
const src = path.join(repoDir, rel);
|
|
412
|
+
const dest = path.join(stageDir, rel);
|
|
413
|
+
const stat = fs.lstatSync(src);
|
|
414
|
+
|
|
415
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
416
|
+
|
|
417
|
+
if (stat.isSymbolicLink()) {
|
|
418
|
+
const target = fs.readlinkSync(src);
|
|
419
|
+
fs.symlinkSync(target, dest);
|
|
420
|
+
} else if (stat.isDirectory()) {
|
|
421
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
422
|
+
} else if (stat.isFile()) {
|
|
423
|
+
fs.copyFileSync(src, dest);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
360
428
|
async function runVerify(packageSpec) {
|
|
361
|
-
|
|
429
|
+
let pkgName, pkgVersion;
|
|
430
|
+
if (packageSpec.startsWith('@')) {
|
|
431
|
+
const atIndex = packageSpec.indexOf('@', 1);
|
|
432
|
+
if (atIndex === -1) {
|
|
433
|
+
pkgName = packageSpec;
|
|
434
|
+
pkgVersion = undefined;
|
|
435
|
+
} else {
|
|
436
|
+
pkgName = packageSpec.slice(0, atIndex);
|
|
437
|
+
pkgVersion = packageSpec.slice(atIndex + 1);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
const atIndex = packageSpec.indexOf('@');
|
|
441
|
+
if (atIndex === -1) {
|
|
442
|
+
pkgName = packageSpec;
|
|
443
|
+
pkgVersion = undefined;
|
|
444
|
+
} else {
|
|
445
|
+
pkgName = packageSpec.slice(0, atIndex);
|
|
446
|
+
pkgVersion = packageSpec.slice(atIndex + 1);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
362
449
|
log.info(`🔍 HIPP Verify: ${pkgName}${pkgVersion ? '@' + pkgVersion : ''}`);
|
|
363
450
|
|
|
364
|
-
const registryUrl =
|
|
365
|
-
const fetchUrl = `${registryUrl}/${encodeURIComponent(pkgName)}/${pkgVersion ? pkgVersion : 'latest'}`;
|
|
451
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${pkgVersion || 'latest'}`;
|
|
366
452
|
|
|
367
453
|
log.info(`📦 Fetching from npm...`);
|
|
454
|
+
const registryJson = runCmd('curl', ['-s', '-L', registryUrl]);
|
|
368
455
|
let tarballUrl;
|
|
369
456
|
try {
|
|
370
|
-
const
|
|
371
|
-
tarballUrl =
|
|
457
|
+
const json = JSON.parse(registryJson.stdout.trim());
|
|
458
|
+
tarballUrl = json.dist.tarball;
|
|
372
459
|
} catch {
|
|
373
|
-
fail(`❌ Failed to
|
|
460
|
+
fail(`❌ Failed to parse npm registry response for ${pkgName}`);
|
|
374
461
|
}
|
|
375
462
|
|
|
376
463
|
const tarballPath = path.join(os.tmpdir(), `hipp-verify-${safeStageName(pkgName)}-tgz`);
|
|
464
|
+
const extractDir = path.join(os.tmpdir(), `hipp-verify-extract-${safeStageName(pkgName)}`);
|
|
465
|
+
|
|
377
466
|
try {
|
|
378
|
-
log.info(`📦 Downloading tarball...`);
|
|
467
|
+
log.info(`📦 Downloading tarball from ${tarballUrl}...`);
|
|
379
468
|
const curlResult = runCmd('curl', ['-s', '-L', '-o', tarballPath, tarballUrl]);
|
|
380
469
|
if (curlResult.status !== 0) {
|
|
381
470
|
fail(`❌ Failed to download tarball`);
|
|
382
471
|
}
|
|
383
472
|
|
|
473
|
+
if (fs.existsSync(extractDir)) {
|
|
474
|
+
fs.rmSync(extractDir, { recursive: true });
|
|
475
|
+
}
|
|
476
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
477
|
+
|
|
384
478
|
log.info(`📦 Extracting tarball...`);
|
|
385
|
-
|
|
479
|
+
const tarResult = spawnSync('tar', ['-xzf', tarballPath, '-C', extractDir], { encoding: 'utf8', stdio: 'pipe' });
|
|
480
|
+
if (tarResult.status !== 0) {
|
|
481
|
+
fail(`❌ Failed to extract tarball: ${tarResult.stderr}`);
|
|
482
|
+
}
|
|
386
483
|
|
|
387
|
-
const packageDir = path.join(
|
|
484
|
+
const packageDir = path.join(extractDir, 'package');
|
|
388
485
|
const stagedReadmePath = path.join(packageDir, 'README.md');
|
|
389
486
|
|
|
390
487
|
if (!fs.existsSync(stagedReadmePath)) {
|
|
391
|
-
fail(`❌ README.md not found in package`);
|
|
488
|
+
fail(`❌ README.md not found in package at ${stagedReadmePath}`);
|
|
392
489
|
}
|
|
393
490
|
|
|
394
491
|
const stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
395
|
-
const
|
|
396
|
-
if (!
|
|
492
|
+
const jsonMeta = extractJsonMetaFromReadme(stagedReadme);
|
|
493
|
+
if (!jsonMeta || !jsonMeta.origin || !jsonMeta.tag) {
|
|
494
|
+
fail(`❌ JSON meta (origin/tag) not found in README`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const manifestStr = extractManifestFromReadme(stagedReadme);
|
|
498
|
+
if (!manifestStr) {
|
|
397
499
|
fail(`❌ Manifest not found in README`);
|
|
398
500
|
}
|
|
399
501
|
|
|
400
|
-
const manifest = parseManifest(
|
|
502
|
+
const manifest = parseManifest(manifestStr);
|
|
401
503
|
if (!manifest || !manifest.hash || !manifest.signature) {
|
|
402
504
|
fail(`❌ Invalid manifest format`);
|
|
403
505
|
}
|
|
404
506
|
|
|
405
|
-
|
|
406
|
-
|
|
507
|
+
const originUrl = jsonMeta.origin;
|
|
508
|
+
const tag = jsonMeta.tag;
|
|
407
509
|
|
|
408
510
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `hipp-verify-git-`));
|
|
511
|
+
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), `hipp-verify-stage-`));
|
|
512
|
+
|
|
409
513
|
try {
|
|
410
|
-
log.info(`🌿 Fetching from git origin...`);
|
|
514
|
+
log.info(`🌿 Fetching from git origin at tag ${tag}...`);
|
|
515
|
+
git(['clone', '--branch', tag, '--depth', '1', originUrl, tmpDir], { stdio: 'pipe' });
|
|
411
516
|
|
|
412
|
-
const
|
|
413
|
-
|
|
517
|
+
const publicKeyPath = path.join(tmpDir, 'hipp.pub');
|
|
518
|
+
if (!fs.existsSync(publicKeyPath)) {
|
|
519
|
+
fail(`❌ hipp.pub not found in git at tag ${tag}`);
|
|
520
|
+
}
|
|
414
521
|
|
|
415
|
-
|
|
522
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
416
523
|
|
|
417
|
-
|
|
418
|
-
|
|
524
|
+
log.info(`🏗️ Staging git files...`);
|
|
525
|
+
const trackedFiles = getTrackedFilesFromDir(tmpDir);
|
|
526
|
+
copyTrackedFilesFromDir(stageDir, tmpDir, trackedFiles);
|
|
527
|
+
|
|
528
|
+
const stagedReadmePath = path.join(stageDir, 'README.md');
|
|
529
|
+
if (!fs.existsSync(stagedReadmePath)) {
|
|
419
530
|
fail(`❌ README.md not found in git at tag ${tag}`);
|
|
420
531
|
}
|
|
421
532
|
|
|
422
|
-
|
|
423
|
-
const
|
|
533
|
+
let stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
534
|
+
const verifyBlock = '```npx @dk/hipp ' + pkgName + '@' + tag + '```';
|
|
535
|
+
const jsonMetaStr = '```json\n{\n "origin": "' + originUrl + '",\n "tag": "' + tag + '"\n}\n```';
|
|
536
|
+
stagedReadme = stagedReadme.trimEnd() + '\n\n' + jsonMetaStr + '\n\n' + verifyBlock + '\n';
|
|
537
|
+
|
|
538
|
+
const stagedHash = computeReadmeHash(stagedReadme);
|
|
424
539
|
|
|
425
|
-
if (
|
|
540
|
+
if (stagedHash !== manifest.hash) {
|
|
426
541
|
fail(`❌ Hash mismatch: git content does not match npm manifest`);
|
|
427
542
|
}
|
|
428
543
|
|
|
429
544
|
log.success(`🔒 Content hash verified: ${manifest.hash.slice(0, 12)}...`);
|
|
430
545
|
|
|
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
546
|
const signData = buildSignData(manifest.hash, originUrl, tag);
|
|
438
547
|
const signatureValid = verifySignature(signData, manifest.signature, publicKey);
|
|
439
548
|
|
|
@@ -447,12 +556,12 @@ async function runVerify(packageSpec) {
|
|
|
447
556
|
log.info(`📍 Tag: ${tag}`);
|
|
448
557
|
} finally {
|
|
449
558
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
559
|
+
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
450
560
|
}
|
|
451
561
|
} finally {
|
|
452
562
|
fs.rmSync(tarballPath, { recursive: true, force: true });
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
fs.rmSync(packageExtractDir, { recursive: true, force: true });
|
|
563
|
+
if (fs.existsSync(extractDir)) {
|
|
564
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
456
565
|
}
|
|
457
566
|
}
|
|
458
567
|
}
|
|
@@ -499,9 +608,10 @@ async function run() {
|
|
|
499
608
|
log.success('📝 hipp.pub committed.');
|
|
500
609
|
}
|
|
501
610
|
|
|
611
|
+
const { rawTag, version } = getVersionFromExactTagOnHead();
|
|
612
|
+
|
|
502
613
|
ensureCleanRepo(pkg);
|
|
503
614
|
|
|
504
|
-
const { rawTag, version } = getVersionFromExactTagOnHead();
|
|
505
615
|
const refInfo = ensureMutableRefPolicy();
|
|
506
616
|
const provenance = ensureRemoteProvenance(rawTag, refInfo.head);
|
|
507
617
|
const lockInfo = ensureLockIntegrity(pkg);
|
|
@@ -542,7 +652,9 @@ async function run() {
|
|
|
542
652
|
stagedReadme = fs.readFileSync(stagedReadmePath, 'utf8');
|
|
543
653
|
}
|
|
544
654
|
|
|
545
|
-
|
|
655
|
+
const verifyBlock = '```npx @dk/hipp ' + pkg.name + '@' + version + '```';
|
|
656
|
+
const jsonMetaStr = '```json\n{\n "origin": "' + provenance.remoteUrl + '",\n "tag": "' + rawTag + '"\n}\n```';
|
|
657
|
+
stagedReadme = stagedReadme.trimEnd() + '\n\n' + jsonMetaStr + '\n\n' + verifyBlock + '\n';
|
|
546
658
|
|
|
547
659
|
const readmeHash = computeReadmeHash(stagedReadme);
|
|
548
660
|
const dataToSign = buildSignData(readmeHash, provenance.remoteUrl, rawTag);
|