From 2700eda2cc23aff712e4d5dab1c68082b9310002 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Mon, 3 Aug 2026 19:07:40 +0100 Subject: [PATCH] chore: pre-publication hardening pass - pin GitHub Actions to commit SHAs and npm to 12.0.2 so the publish job never executes an unreviewed upstream release - clean dist/ before every build; tsc leaves orphaned outputs, so locally built tarballs could ship files absent from git - create .env with mode 0600 (owner-only) since it holds the cloud's API secret; pre-existing files keep their permissions --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/publish.yml | 12 ++++++++---- package.json | 3 ++- src/lib/env-file.ts | 7 ++++++- test/env-file.test.mjs | 8 +++++++- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550f82d..6d25cbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ jobs: # Lower bound must match "engines.node" in package.json. node: ['20', '22', '24'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node }} cache: npm @@ -40,9 +40,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc cache: npm diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9c56eba..8e77367 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,7 @@ name: Publish to npm # Publishes @cloudinary/cloud to the PUBLIC npm registry -# (https://registry.npmjs.org) — never to an internal Nexus mirror. +# (https://registry.npmjs.org) — never to an internal registry mirror. # `registry-url` below plus `publishConfig.registry` in package.json pin the # target so a misconfigured ~/.npmrc cannot redirect a release. @@ -38,17 +38,21 @@ jobs: # Configure under Settings → Environments → npm-publish → Required reviewers. environment: npm-publish steps: - - uses: actions/checkout@v4 + # Actions are pinned to commit SHAs so a hijacked upstream tag cannot + # run code in the job that holds the publish credential. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc cache: npm registry-url: 'https://registry.npmjs.org' # Trusted publishing (OIDC, no long-lived token) needs npm >= 11.5.1. + # Pinned (not @latest) so the publish job never runs a freshly-minted, + # unreviewed npm release. - name: Use an npm that supports trusted publishing - run: npm install -g npm@latest + run: npm install -g npm@12.0.2 - run: npm ci diff --git a/package.json b/package.json index cd48301..0558e93 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ }, "homepage": "https://github.com/cloudinary/cloudinary-cloud#readme", "scripts": { - "build": "tsc", + "build": "npm run clean && tsc", + "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", "dev": "tsc --watch", "test": "npm run build && node --test", "prepack": "npm run build" diff --git a/src/lib/env-file.ts b/src/lib/env-file.ts index eddc95f..296c345 100644 --- a/src/lib/env-file.ts +++ b/src/lib/env-file.ts @@ -37,7 +37,12 @@ export function writeCloudEnv( if (entries.expiresAt) pairs.push([EXPIRES_AT_KEY, entries.expiresAt]); if (!existsSync(envPath)) { - writeFileSync(envPath, pairs.map(([k, v]) => `${k}=${v}`).join('\n') + '\n', 'utf-8'); + // Owner-only: the file holds a live API secret. Applies on creation only — + // a pre-existing .env keeps whatever permissions the user gave it. + writeFileSync(envPath, pairs.map(([k, v]) => `${k}=${v}`).join('\n') + '\n', { + encoding: 'utf-8', + mode: 0o600, + }); return { action: 'created' }; } diff --git a/test/env-file.test.mjs b/test/env-file.test.mjs index 316aa1d..73bd414 100644 --- a/test/env-file.test.mjs +++ b/test/env-file.test.mjs @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { writeCloudEnv, readCloudEnv, hasCloudinaryUrl, isEnvExposedToGit } from '../dist/lib/env-file.js'; @@ -19,6 +19,12 @@ test('creates .env when missing', () => { assert.equal(readFileSync(envPath, 'utf-8'), `CLOUDINARY_URL=${URL_A}\n`); }); +test('creates .env owner-readable only', { skip: process.platform === 'win32' }, () => { + const envPath = tempEnvPath(); + writeCloudEnv(envPath, { cloudinaryUrl: URL_A }); + assert.equal(statSync(envPath).mode & 0o777, 0o600); +}); + test('appends to existing .env without CLOUDINARY_URL', () => { const envPath = tempEnvPath(); writeFileSync(envPath, 'OTHER_VAR=hello\n');