@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.
Files changed (3) hide show
  1. package/README.md +33 -3
  2. package/hipp.js +152 -40
  3. 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.7"
156
+ "tag": "v0.1.10"
155
157
  }
156
158
  ```
157
159
 
158
- ```npx @dk/hipp @dk/hipp@0.1.7```
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
- <!-- HIPP-MANIFEST -->```eyJoYXNoIjoiODYxNDgxZmM3Yzk0ZTM4YTJhZjdjNDc3ZmJkMDcyMTA2YTMyYTI2MjdkYmM1ZTg4MTI1OTk1ODMyYjdlMzhhMiIsInNpZ25hdHVyZSI6IkVnUTBSZUMzU0hKanovdFZoWlY0V3U1SVJOMDJTSlFPWUtxOFBrdHFpcUhvWnRTbE5pQWxrN25nWDcySldxL0ZRa2JTVE10TldlZUNrSG9hczAwR0N3PT0ifQ==```<!-- /HIPP-MANIFEST -->
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 Buffer.from(JSON.stringify({ hash, signature })).toString('base64');
111
+ return JSON.stringify({ hash, signature }, null, 2);
112
112
  }
113
113
 
114
- function parseManifest(manifestBase64) {
114
+ function parseManifest(manifestStr) {
115
115
  try {
116
- return JSON.parse(Buffer.from(manifestBase64, 'base64').toString('utf8'));
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}\`\`\`${manifest}\`\`\`${MANIFEST_END}\n`;
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
- const [pkgName, pkgVersion] = packageSpec.split('@');
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 = 'https://registry.npmjs.org';
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 fetchResult = runCmd('curl', ['-s', '-L', '-w', '%{url_effective}', '-o', '/dev/null', fetchUrl]);
371
- tarballUrl = fetchResult.stdout.trim();
457
+ const json = JSON.parse(registryJson.stdout.trim());
458
+ tarballUrl = json.dist.tarball;
372
459
  } catch {
373
- fail(`❌ Failed to fetch package info for ${pkgName}`);
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
- runCmd('tar', ['-xzf', tarballPath, '-C', os.tmpdir()], { stdio: 'pipe' });
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(os.tmpdir(), 'package');
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 manifestBase64 = extractManifestFromReadme(stagedReadme);
396
- if (!manifestBase64) {
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(manifestBase64);
502
+ const manifest = parseManifest(manifestStr);
401
503
  if (!manifest || !manifest.hash || !manifest.signature) {
402
504
  fail(`❌ Invalid manifest format`);
403
505
  }
404
506
 
405
- log.info(`📦 Extracting tarball to staging...`);
406
- runCmd('tar', ['-xzf', tarballPath, '-C', os.tmpdir()], { stdio: 'pipe' });
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 originUrl = manifest.origin;
413
- const tag = manifest.tag;
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
- git(['clone', '--branch', tag, '--depth', '1', originUrl, tmpDir], { stdio: 'pipe' });
522
+ const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
416
523
 
417
- const clonedReadmePath = path.join(tmpDir, 'README.md');
418
- if (!fs.existsSync(clonedReadmePath)) {
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
- const clonedReadme = fs.readFileSync(clonedReadmePath, 'utf8');
423
- const clonedHash = computeReadmeHash(clonedReadme);
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 (clonedHash !== manifest.hash) {
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
- const packageExtractDir = path.join(os.tmpdir(), 'package');
454
- if (fs.existsSync(packageExtractDir)) {
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
- stagedReadme += '\n```json\n{\n "origin": "' + provenance.remoteUrl + '",\n "tag": "' + rawTag + '"\n}\n```\n\n```npx @dk/hipp ' + pkg.name + '@' + version + '```\n\n';
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dk/hipp",
3
- "version": "0.1.7",
3
+ "version": "0.1.12",
4
4
  "description": "High Integrity Package Publisher",
5
5
  "main": "hipp.js",
6
6
  "bin": {