@bobfrankston/npmglobalize 1.0.113 → 1.0.114
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/lib.js +197 -15
- package/package.json +1 -1
package/lib.js
CHANGED
|
@@ -1406,14 +1406,13 @@ function getSecurityNpmignorePatterns() {
|
|
|
1406
1406
|
function lineHasPattern(lines, pattern) {
|
|
1407
1407
|
return lines.some(line => line === pattern || line === pattern.replace('/', ''));
|
|
1408
1408
|
}
|
|
1409
|
-
/** Detect OAuth credentials
|
|
1410
|
-
* Returns
|
|
1411
|
-
function
|
|
1412
|
-
|
|
1413
|
-
if (!fs.existsSync(credPath))
|
|
1409
|
+
/** Detect OAuth credentials type from a specific JSON file path.
|
|
1410
|
+
* Returns 'installed' (public app ID, safe) or 'web' (real secret) or null. */
|
|
1411
|
+
function detectCredentialsTypeFromFile(filePath) {
|
|
1412
|
+
if (!fs.existsSync(filePath))
|
|
1414
1413
|
return null;
|
|
1415
1414
|
try {
|
|
1416
|
-
const content = fs.readFileSync(
|
|
1415
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1417
1416
|
const parsed = JSON.parse(content);
|
|
1418
1417
|
if (parsed.installed)
|
|
1419
1418
|
return 'installed';
|
|
@@ -1425,6 +1424,156 @@ function detectCredentialsType(cwd) {
|
|
|
1425
1424
|
return null;
|
|
1426
1425
|
}
|
|
1427
1426
|
}
|
|
1427
|
+
/** Detect OAuth credentials.json type: "installed" (public app ID, safe to include) or "web" (has real secret, must ignore).
|
|
1428
|
+
* Returns null if no credentials.json exists or it can't be parsed. */
|
|
1429
|
+
function detectCredentialsType(cwd) {
|
|
1430
|
+
return detectCredentialsTypeFromFile(path.join(cwd, 'credentials.json'));
|
|
1431
|
+
}
|
|
1432
|
+
/** Parse GitHub push protection (GH013) error output and extract secret details + unblock URLs. */
|
|
1433
|
+
function parsePushProtection(errorOutput, cwd) {
|
|
1434
|
+
const result = { detected: false, secrets: [], allInstalledOAuth: false };
|
|
1435
|
+
if (!errorOutput)
|
|
1436
|
+
return result;
|
|
1437
|
+
// Strip 'remote: ' prefixes for easier parsing
|
|
1438
|
+
const cleaned = errorOutput.replace(/^remote:\s*/gm, '');
|
|
1439
|
+
if (!cleaned.includes('GH013') && !cleaned.toLowerCase().includes('push protection')) {
|
|
1440
|
+
return result;
|
|
1441
|
+
}
|
|
1442
|
+
result.detected = true;
|
|
1443
|
+
// Split on secret-type headers: "—— Secret Type ————..."
|
|
1444
|
+
const blocks = cleaned.split(/\u2014\u2014\s+/).slice(1);
|
|
1445
|
+
for (const block of blocks) {
|
|
1446
|
+
const typeMatch = block.match(/^(.+?)[\s\u2014]+/);
|
|
1447
|
+
const type = typeMatch ? typeMatch[1].trim() : '';
|
|
1448
|
+
if (!type)
|
|
1449
|
+
continue;
|
|
1450
|
+
const pathMatch = block.match(/path:\s+(\S+?)(?::(\d+))?\s/);
|
|
1451
|
+
const file = pathMatch ? pathMatch[1].trim() : '';
|
|
1452
|
+
const urlMatch = block.match(/(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/);
|
|
1453
|
+
const unblockUrl = urlMatch ? urlMatch[1].trim() : '';
|
|
1454
|
+
if (type && (file || unblockUrl)) {
|
|
1455
|
+
result.secrets.push({ type, file, unblockUrl });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// Check if all detected secrets are from Google OAuth installed apps (safe to include)
|
|
1459
|
+
if (result.secrets.length > 0) {
|
|
1460
|
+
result.allInstalledOAuth = result.secrets.every(s => {
|
|
1461
|
+
if (!s.type.toLowerCase().includes('oauth'))
|
|
1462
|
+
return false;
|
|
1463
|
+
if (!s.file)
|
|
1464
|
+
return false;
|
|
1465
|
+
const credType = detectCredentialsTypeFromFile(path.join(cwd, s.file));
|
|
1466
|
+
return credType === 'installed';
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
/** Try to auto-bypass push protection for installed OAuth secrets via gh API.
|
|
1472
|
+
* Returns true if all bypasses succeeded and push should be retried. */
|
|
1473
|
+
function tryAutoBypassPushProtection(ppInfo, cwd) {
|
|
1474
|
+
if (!ppInfo.allInstalledOAuth || ppInfo.secrets.length === 0)
|
|
1475
|
+
return false;
|
|
1476
|
+
// Check if gh CLI is available
|
|
1477
|
+
const ghCheck = runCommand('gh', ['auth', 'status'], { cwd, silent: true });
|
|
1478
|
+
if (!ghCheck.success)
|
|
1479
|
+
return false;
|
|
1480
|
+
// Extract repo owner/name from unblock URLs or git remote
|
|
1481
|
+
let repo = '';
|
|
1482
|
+
for (const s of ppInfo.secrets) {
|
|
1483
|
+
if (s.unblockUrl) {
|
|
1484
|
+
const repoMatch = s.unblockUrl.match(/github\.com\/([^/]+\/[^/]+)\//);
|
|
1485
|
+
if (repoMatch) {
|
|
1486
|
+
repo = repoMatch[1];
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (!repo)
|
|
1492
|
+
return false;
|
|
1493
|
+
let allBypassed = true;
|
|
1494
|
+
for (const s of ppInfo.secrets) {
|
|
1495
|
+
if (!s.unblockUrl) {
|
|
1496
|
+
allBypassed = false;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
// Extract placeholder_id from unblock URL (last path segment)
|
|
1500
|
+
const idMatch = s.unblockUrl.match(/\/unblock-secret\/(\S+)/);
|
|
1501
|
+
if (!idMatch) {
|
|
1502
|
+
allBypassed = false;
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const placeholderId = idMatch[1];
|
|
1506
|
+
console.log(colors.cyan(` Bypassing push protection for: ${s.type} (false_positive — installed app ID)...`));
|
|
1507
|
+
const bypassResult = runCommand('gh', [
|
|
1508
|
+
'api', `repos/${repo}/secret-scanning/push-protection-bypasses`,
|
|
1509
|
+
'-X', 'POST',
|
|
1510
|
+
'-f', 'reason=false_positive',
|
|
1511
|
+
'-f', `placeholder_id=${placeholderId}`
|
|
1512
|
+
], { cwd, silent: true });
|
|
1513
|
+
if (!bypassResult.success) {
|
|
1514
|
+
allBypassed = false;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return allBypassed;
|
|
1518
|
+
}
|
|
1519
|
+
/** Display push protection guidance based on the type of secrets detected. */
|
|
1520
|
+
function showPushProtectionGuidance(ppInfo) {
|
|
1521
|
+
console.error('');
|
|
1522
|
+
console.error(colors.yellow('GitHub Push Protection blocked the push — secrets detected in commits.'));
|
|
1523
|
+
if (ppInfo.allInstalledOAuth) {
|
|
1524
|
+
console.error(colors.cyan('These are Google OAuth credentials for a desktop/installed app.'));
|
|
1525
|
+
console.error(colors.cyan('The "client_secret" is just a public app identifier, not a real secret.'));
|
|
1526
|
+
console.error(colors.cyan('(Google docs: "the client_secret is obviously not treated as a secret")'));
|
|
1527
|
+
console.error('');
|
|
1528
|
+
console.error('To unblock, visit each URL below and allow the secret:');
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
console.error('');
|
|
1532
|
+
console.error('To unblock, review each secret and visit the URL to allow or remove it:');
|
|
1533
|
+
}
|
|
1534
|
+
for (const s of ppInfo.secrets) {
|
|
1535
|
+
console.error(` ${s.type}: ${s.file}`);
|
|
1536
|
+
if (s.unblockUrl) {
|
|
1537
|
+
console.error(` ${colors.cyan(s.unblockUrl)}`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
console.error('');
|
|
1541
|
+
console.error('After unblocking, re-run the push or re-run npmglobalize.');
|
|
1542
|
+
}
|
|
1543
|
+
/** Push to git with push-protection detection and auto-bypass for installed OAuth.
|
|
1544
|
+
* Returns true if push succeeded (possibly after auto-bypass). */
|
|
1545
|
+
function pushWithProtection(cwd, verbose) {
|
|
1546
|
+
const pushResult = runCommand('git', ['push'], { cwd, silent: true });
|
|
1547
|
+
if (pushResult.success) {
|
|
1548
|
+
if (verbose)
|
|
1549
|
+
console.log(colors.green(' ✓ Pushed to remote'));
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
const ppInfo = parsePushProtection(pushResult.stderr, cwd);
|
|
1553
|
+
if (!ppInfo.detected) {
|
|
1554
|
+
console.error(colors.red('Git push failed:'));
|
|
1555
|
+
if (pushResult.stderr)
|
|
1556
|
+
console.error(pushResult.stderr);
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
// Try auto-bypass for installed OAuth credentials
|
|
1560
|
+
if (ppInfo.allInstalledOAuth && tryAutoBypassPushProtection(ppInfo, cwd)) {
|
|
1561
|
+
console.log(colors.green(' ✓ Auto-bypassed push protection (installed OAuth — not a real secret)'));
|
|
1562
|
+
const retryPush = runCommand('git', ['push'], { cwd, silent: true });
|
|
1563
|
+
if (retryPush.success) {
|
|
1564
|
+
if (verbose)
|
|
1565
|
+
console.log(colors.green(' ✓ Pushed to remote'));
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
console.error(colors.yellow('Push still failed after bypass:'));
|
|
1569
|
+
if (retryPush.stderr)
|
|
1570
|
+
console.error(retryPush.stderr);
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
// Can't auto-bypass — show manual guidance
|
|
1574
|
+
showPushProtectionGuidance(ppInfo);
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1428
1577
|
/** Ensure credentials.json is handled correctly in ignore files based on OAuth type.
|
|
1429
1578
|
* "installed" apps: client_secret is just a public app registration ID — must be INCLUDED.
|
|
1430
1579
|
* "web" apps: client_secret is a real secret — must be IGNORED. */
|
|
@@ -2824,7 +2973,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2824
2973
|
runCommand('git', ['add', 'package.json'], { cwd });
|
|
2825
2974
|
runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
|
|
2826
2975
|
if (currentGitStatus.hasRemote) {
|
|
2827
|
-
|
|
2976
|
+
pushWithProtection(cwd, verbose);
|
|
2828
2977
|
}
|
|
2829
2978
|
}
|
|
2830
2979
|
}
|
|
@@ -2852,7 +3001,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2852
3001
|
runCommand('git', ['add', 'package.json'], { cwd });
|
|
2853
3002
|
runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
|
|
2854
3003
|
if (currentGitStatus.hasRemote) {
|
|
2855
|
-
|
|
3004
|
+
pushWithProtection(cwd, verbose);
|
|
2856
3005
|
}
|
|
2857
3006
|
}
|
|
2858
3007
|
}
|
|
@@ -3053,13 +3202,14 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3053
3202
|
// Version bump
|
|
3054
3203
|
console.log(`Bumping version (${bump})...`);
|
|
3055
3204
|
if (!dryRun) {
|
|
3056
|
-
// Temporarily disable postversion hook to prevent double-publishing
|
|
3205
|
+
// Temporarily disable postversion hook to prevent double-publishing or push
|
|
3206
|
+
// (push is handled separately with push-protection detection)
|
|
3057
3207
|
const pkg = readPackageJson(cwd);
|
|
3058
3208
|
const originalPostversion = pkg.scripts?.postversion;
|
|
3059
3209
|
let postversDisabled = false;
|
|
3060
|
-
if (originalPostversion && originalPostversion.includes('npm publish')) {
|
|
3210
|
+
if (originalPostversion && (originalPostversion.includes('npm publish') || originalPostversion.includes('git push'))) {
|
|
3061
3211
|
if (verbose) {
|
|
3062
|
-
console.log('Temporarily disabling postversion hook
|
|
3212
|
+
console.log('Temporarily disabling postversion hook (npmglobalize handles publish/push)...');
|
|
3063
3213
|
}
|
|
3064
3214
|
delete pkg.scripts.postversion;
|
|
3065
3215
|
writePackageJson(cwd, pkg);
|
|
@@ -3224,6 +3374,35 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3224
3374
|
console.error(' • Disk space or permissions issues');
|
|
3225
3375
|
console.error(colors.yellow('\nTry running with --verbose or check git status manually'));
|
|
3226
3376
|
}
|
|
3377
|
+
else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
|
|
3378
|
+
// GitHub push protection blocked a push (from postversion script)
|
|
3379
|
+
const ppInfo = parsePushProtection(error.stderr || error.stdout || error.message || '', cwd);
|
|
3380
|
+
if (ppInfo.detected) {
|
|
3381
|
+
showPushProtectionGuidance(ppInfo);
|
|
3382
|
+
}
|
|
3383
|
+
else {
|
|
3384
|
+
console.error(colors.yellow('\nGitHub push protection blocked the push.'));
|
|
3385
|
+
console.error(colors.yellow('Check the output above for unblock URLs.'));
|
|
3386
|
+
}
|
|
3387
|
+
console.log(colors.yellow('Version and tag were created locally. Continuing with publish...'));
|
|
3388
|
+
autoFixed = true;
|
|
3389
|
+
}
|
|
3390
|
+
// If version/tag exist locally despite error, postversion script likely failed
|
|
3391
|
+
// (e.g., push protection with inherited stdio that we couldn't capture)
|
|
3392
|
+
if (!autoFixed) {
|
|
3393
|
+
try {
|
|
3394
|
+
const postPkg = readPackageJson(cwd);
|
|
3395
|
+
const tagCheck = runCommand('git', ['tag', '-l', `v${postPkg.version}`], { cwd, silent: true });
|
|
3396
|
+
if (tagCheck.success && tagCheck.output.trim() === `v${postPkg.version}`) {
|
|
3397
|
+
console.log(colors.yellow('\nVersion and tag created locally — postversion script may have failed.'));
|
|
3398
|
+
console.log(colors.yellow('Continuing — push will be attempted separately.'));
|
|
3399
|
+
autoFixed = true;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
catch {
|
|
3403
|
+
// ignore — fall through to normal error handling
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3227
3406
|
if (!autoFixed) {
|
|
3228
3407
|
if (!force) {
|
|
3229
3408
|
return false;
|
|
@@ -3433,14 +3612,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3433
3612
|
else {
|
|
3434
3613
|
console.log(` [dry-run] Would run: npm publish ${quiet ? '--quiet' : ''}`);
|
|
3435
3614
|
}
|
|
3436
|
-
// Push to git
|
|
3615
|
+
// Push to git (with push-protection detection and auto-bypass)
|
|
3437
3616
|
if (currentGitStatus.hasRemote) {
|
|
3438
3617
|
if (verbose) {
|
|
3439
3618
|
console.log('Pushing to git...');
|
|
3440
3619
|
}
|
|
3441
3620
|
if (!dryRun) {
|
|
3442
|
-
|
|
3443
|
-
|
|
3621
|
+
if (pushWithProtection(cwd, verbose)) {
|
|
3622
|
+
const tagResult = runCommand('git', ['push', '--tags'], { cwd, silent: true });
|
|
3623
|
+
if (verbose && tagResult.success)
|
|
3624
|
+
console.log(colors.green(' ✓ Pushed tags'));
|
|
3625
|
+
}
|
|
3444
3626
|
}
|
|
3445
3627
|
else {
|
|
3446
3628
|
console.log(' [dry-run] Would push to git');
|
|
@@ -3540,7 +3722,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3540
3722
|
runCommand('git', ['add', 'package.json'], { cwd });
|
|
3541
3723
|
runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
|
|
3542
3724
|
if (currentGitStatus.hasRemote) {
|
|
3543
|
-
|
|
3725
|
+
pushWithProtection(cwd, verbose);
|
|
3544
3726
|
}
|
|
3545
3727
|
}
|
|
3546
3728
|
}
|