diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7a7f660b..75f51eb3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,13 @@ * @darkroomengineering/devs + +# Release-critical paths. Changes here can compromise the supply chain +# (workflow swap, manifest tampering, registry config). Reviewers should +# verify that any modification is intentional and does not weaken the +# publish pipeline. Replace with a smaller `release-admins` team once +# one exists. +/.github/workflows/ @darkroomengineering/devs +/.github/CODEOWNERS @darkroomengineering/devs +/.github/dependabot.yml @darkroomengineering/devs +/package.json @darkroomengineering/devs +/.npmrc @darkroomengineering/devs +/SECURITY.md @darkroomengineering/devs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dbdf455c..185044ce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,30 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - - version: 2 updates: - - package-ecosystem: "github-actions" # See documentation for possible values - directory: "/" # Location of package manifests + # Track new versions of GitHub Actions. Dependabot rewrites SHA-pinned actions + # back to the new SHA, preserving the pinning policy in release.yml. + - package-ecosystem: "github-actions" + directory: "/" schedule: interval: "daily" + groups: + actions: + patterns: + - "*" + + # Track advisories on npm dev dependencies. Grouped to reduce PR noise. + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + dev-dependencies: + dependency-type: "development" + update-types: + - "minor" + - "patch" + ignore: + # Peer deps — bumping these in this repo doesn't change what consumers get. + - dependency-name: "react" + - dependency-name: "vue" + - dependency-name: "@nuxt/kit" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..9efa015a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +# Publishes `lenis` to npm with build provenance and OIDC-based Trusted Publishing. +# No long-lived NPM_TOKEN exists in this repository or in GitHub Actions secrets: +# the npm CLI exchanges a short-lived GitHub OIDC token for a publish credential at +# the moment of publish, scoped to this workflow file in this repository. +# +# Prerequisites (one-time setup on npmjs.com, see SECURITY.md): +# - Configure `lenis` with a Trusted Publisher pointing at: +# repository: darkroomengineering/lenis +# workflow file: .github/workflows/release.yml +# - Revoke every classic / automation npm token after the first successful run. + +on: + push: + tags: + - 'v*' + +# Minimum permissions for the whole workflow. The publish job widens to id-token:write. +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write # Required for npm Trusted Publishing + provenance attestations. + + steps: + # Restrict outbound network from the runner so a compromised dep can't exfiltrate. + # Start in `audit` mode; tighten to `block` after observing allowed endpoints. + - name: Harden runner + uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1 + with: + egress-policy: audit + disable-sudo: true + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.13 + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + + # Trusted Publishing requires npm >= 11.5.1; Node 22 ships with npm 10.x. + - name: Upgrade npm to a Trusted-Publishing-capable version + run: npm install -g npm@latest + + # Refuse to publish if the git tag and package.json version disagree. + # Prevents an attacker who can push a tag from publishing an arbitrary version. + - name: Verify tag matches package.json version + run: | + PKG_VERSION="v$(node -p "require('./package.json').version")" + if [ "${GITHUB_REF_NAME}" != "${PKG_VERSION}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match package.json version ${PKG_VERSION}" + exit 1 + fi + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build + run: bun run build + + # Refuse to publish if dist/ is missing or empty after the build. + - name: Verify build output + run: | + if [ ! -d dist ] || [ -z "$(ls -A dist)" ]; then + echo "::error::dist/ is missing or empty after build" + exit 1 + fi + + - name: Publish to npm + run: npm publish --provenance --access public diff --git a/.gitignore b/.gitignore index 8f2291dd..0863f97f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,10 @@ yarn-error.log* .vercel .eslintcache +# Keep root /.npmrc tracked (provenance=true setting, no secrets). +# Ignore nested .npmrc files where auth tokens may end up. .npmrc +!/.npmrc packages/core/dist/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..d308232f --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +; Redundant with publishConfig.provenance in package.json, kept for defense in depth. +provenance=true diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a6d276e3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,120 @@ +# Security Policy + +## Reporting a vulnerability + +Please **do not** open public GitHub issues for security problems. Email +`hello@darkroom.engineering` or use GitHub's +[private vulnerability reporting](https://github.com/darkroomengineering/lenis/security/advisories/new) +on this repository. + +We aim to acknowledge reports within three business days. + +## Supply-chain hardening + +`lenis` is published to npm via a hardened pipeline designed to make +credential theft and tarball tampering ineffective. + +The protections in this repository: + +- **No long-lived npm token exists.** Publishing runs only from + `.github/workflows/release.yml`, which uses npm Trusted Publishing + (OIDC) to mint a short-lived publish credential at the moment of + publish. There is no `NPM_TOKEN` in GitHub Actions secrets, in CI, on + any maintainer's machine, or in a password manager. +- **Build provenance.** Every published tarball ships with an [npm + provenance attestation](https://docs.npmjs.com/generating-provenance-statements) + cryptographically tied to this repository, the workflow file, the + commit, and the runner. Consumers can verify provenance with + `npm audit signatures` (see below). +- **Tag-protected releases.** The workflow only runs on tag pushes + matching `v*` and refuses to publish unless the tag matches the + version in `package.json`. +- **Workflow integrity.** All GitHub Actions are pinned to commit SHAs + (not floating tags). Dependabot updates SHAs in PRs that go through + CODEOWNERS review. +- **Local `npm publish` is removed.** `package.json` no longer carries + `publish:main` / `publish:dev` scripts. There is no sanctioned path + to publish from a developer machine. +- **CODEOWNERS** requires review on `.github/`, `package.json`, `.npmrc`, + and this file — anything that could weaken the pipeline. +- **`files` allowlist** in `package.json` limits the published tarball + to `dist/` only; nothing else from the working tree is shipped. + +## One-time setup (must be done outside the repository) + +These steps cannot be encoded in repository files. A maintainer with admin +rights on the npm package and the GitHub repository must do them once. +After completion, audit them annually. + +### On npmjs.com + +1. **Confirm two-factor authentication is enforced for writes** on the + account or org that owns `lenis` on npm, and on every account with + publish rights to it. +2. **Configure Trusted Publisher** for `lenis`: + - Settings → Publishing access → "Enable Trusted Publisher" + - Publisher: GitHub Actions + - Repository: `darkroomengineering/lenis` + - Workflow filename: `.github/workflows/release.yml` + - Environment name: *(leave blank unless using GitHub Environments)* +3. **After the first OIDC-based publish succeeds** and the latest + version shows a provenance attestation, set the Publishing access + policy on `lenis` to **"Require two-factor authentication and + disallow tokens"** (the recommended option on + Settings → Publishing access). This forbids any token-based + publish path. Do not enable this before a successful OIDC publish, + or there will be no fallback if Trusted Publisher is misconfigured. +4. **Confirm no long-lived publish tokens remain** for the `lenis` + package. + +### On github.com + +1. **Branch protection on `main`**: + - Require a pull request before merging + - Require review from CODEOWNERS + - Require signed commits + - Restrict who can push (no direct push to `main`) + - Block force pushes and deletions +2. **Tag protection** for `v*`: restrict who can create release tags to + the same group that can approve releases. +3. **Restrict workflow permissions** (Settings → Actions → General): + - Workflow permissions: "Read repository contents and packages permissions" + - Allow GitHub Actions to create and approve pull requests: off +4. **Secret scanning + push protection**: on. + +## Release flow (for maintainers) + +```bash +bun run version:patch # or version:minor / version:major +git add package.json +git commit -m "v$(node -p "require('./package.json').version")" +git tag "v$(node -p "require('./package.json').version")" +git push --follow-tags +``` + +The tag push triggers `.github/workflows/release.yml`, which builds and +publishes. Nothing else publishes; if you ever feel the urge to run +`npm publish` locally, stop and investigate why the workflow isn't +working instead. + +## Verifying a published version (for consumers) + +After installing `lenis`, verify provenance and signatures: + +```bash +npm audit signatures +``` + +You should see `lenis` listed under "verified registry signatures" with +a "provenance" attestation pointing at `github.com/darkroomengineering/lenis` +and a specific commit SHA. If provenance is missing, the version was not +published through the trusted pipeline — treat as suspect, do not run +`postinstall`, and report it through the channel above. + +To inspect provenance for a specific version without installing: + +```bash +npm view lenis@ --json | jq '.dist' +``` + +The `attestations` block lists the provenance bundle URL. diff --git a/package.json b/package.json index 82b7f781..7b957751 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,12 @@ "version:minor": "npm version minor --force --no-git-tag-version", "version:major": "npm version major --force --no-git-tag-version", "postversion": "bun run build && bun run readme", - "publish:dev": "npm publish --tag dev", - "publish:main": "npm publish" + "prepublishOnly": "bun run build" + }, + "packageManager": "bun@1.3.13", + "publishConfig": { + "access": "public", + "provenance": true }, "files": [ "dist"