Skip to content

chore: use pnpm and harden security#221

Merged
mandarini merged 2 commits into
mainfrom
chore/pnpm-and-security
May 12, 2026
Merged

chore: use pnpm and harden security#221
mandarini merged 2 commits into
mainfrom
chore/pnpm-and-security

Conversation

@mandarini

@mandarini mandarini commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates the repo to pnpm and applies the low-risk subset of supply-chain hardening from the playbook informed by the TanStack/router compromise (May 2026) and Adnan Khan's GitHub Actions cache poisoning research. End users of @supabase/ssr are unaffected: same tarball, same npm publish --provenance flow, same trusted-publisher binding.

Package manager

  • Pin packageManager to pnpm@11.1.0 with a sha512 hash in package.json. A tampered pnpm binary served by a compromised mirror fails the hash check instead of running.
  • Convert package-lock.json to pnpm-lock.yaml (3589 lines removed, 3213 added). Lockfile contains only registry resolutions (no github:, git+, file: refs).

Supply-chain hardening (pnpm-workspace.yaml)

New file with three guards aimed at the TanStack class of attacks:

  • minimumReleaseAge: 10080 blocks installs of any version published in the last 7 days. The TanStack malicious versions were pulled within hours, so this single setting would have blocked the install path.
  • minimumReleaseAgeExclude: ['@supabase/*'] lets the update-supabase-js.yml workflow bypass the quarantine for first-party releases.
  • blockExoticSubdeps: true refuses non-registry refs in transitive deps. This kills the optionalDependencies: { foo: "github:owner/repo#sha" } vector TanStack used.
  • allowBuilds: { esbuild: true } is the explicit install-script allowlist (default deny). Only esbuild is permitted to run lifecycle scripts, since vitest depends on it.

Workflow hardening

Applied across all four workflows (ci.yml, release.yml, update-supabase-js.yml, conventional-commits.yml):

  • persist-credentials: false on every actions/checkout. Default behavior writes a write-capable token to .git/config on disk, where any later step (including a compromised dependency) can read it.
  • Convert npm commands to pnpm: pnpm install --frozen-lockfile, pnpm build, pnpm test, pnpm exec prettier, pnpm exec eslint.
  • Add pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 (v6.0.7), pinned by commit SHA. v6 reads the packageManager field automatically so no version: arg is needed.
  • Bump update-supabase-js.yml checkout SHA from v4.3.0 to v6.0.1 so dependabot only tracks one version.
  • Drop the corepack enable npm && corepack prepare npm@11 line from release.yml (no longer needed).
  • Add a # v4.4.0 comment next to the googleapis/release-please-action SHA pin for readability.

Cache poisoning fix

Removed cache: pnpm from release.yml and update-supabase-js.yml. Per Adnan Khan's research, caching in release workflows is exploitable: a compromised dep that runs on a main-context job can dump the Actions runtime token from runner memory and poison cache entries that a later release run will restore, replacing package.json with one carrying a preinstall payload. With id-token: write in scope, that payload can mint a publish token.

ci.yml retains cache: pnpm because cache poisoning there only breaks CI, not releases.

Other changes

  • .github/dependabot.yml (new): weekly github-actions ecosystem updates so SHA-pinned actions stay current.
  • .github/CODEOWNERS (new): blanket ownership across the repo so workflow and release-config edits require explicit review.
  • README.md: install snippet now shows npm, pnpm, yarn, and bun side by side instead of pushing consumers toward one manager.
  • src/createBrowserClient.ts and src/createServerClient.ts: prettier reformatted these (whitespace only, no semantic change) after the prettier version resolved by pnpm bumped from the previous lockfile pin to 3.8.3. No runtime difference.

@mandarini mandarini force-pushed the chore/pnpm-and-security branch from 246b97c to 41292ce Compare May 12, 2026 10:00
@mandarini mandarini merged commit 442966b into main May 12, 2026
5 checks passed
@mandarini mandarini deleted the chore/pnpm-and-security branch May 12, 2026 10:31
mandarini added a commit that referenced this pull request Jun 9, 2026
## Summary

- Re-adds an explicit npm install before the publish step so the
workflow runs on **npm 11.5.2** (the version that supports OIDC
trusted-publisher exchange) instead of Node 22's bundled npm 10.9.x.
- Adds a multi-line comment in the workflow explaining the dependency,
so the line is not deleted again.

## Background

The release pipeline has been silently broken since #221 (May 12). That
PR removed `corepack enable npm && corepack prepare npm@11 --activate`
with the claim "no longer needed", but `actions/setup-node@v6.4.0` does
**not** upgrade npm — Node 22 ships with npm 10.9.x.

npm only learned how to do OIDC trusted-publisher exchange in
**11.5.1**. On npm 10, `npm publish --provenance` signs the provenance
via sigstore (which works because it uses the GitHub OIDC token
directly), then sends the bogus `.npmrc` placeholder
`XXXXX-XXXXX-XXXXX-XXXXX` as the bearer token to the registry. The
registry returns `404 Not Found - PUT
https://registry.npmjs.org/@supabase%2fssr` (npm registry returns 404
for unauthenticated PUTs to avoid leaking which packages exist).

The repo has **no `NPM_TOKEN` secret** configured anywhere — by design,
per #221's hardening posture. Auth is OIDC-only via the
trusted-publisher binding on npmjs.com. Re-adding a token would weaken
that posture, so the fix is to ensure the npm CLI is new enough to use
OIDC.

## Why this went undetected

Between #221's merge (May 12) and #240's merge (June 4), every release
run was for a `chore: update @supabase/supabase-js` commit. Those have
no pending release-please PR, so the workflow's version-determination
step took the `skip=true` branch and never attempted to publish. The
first publish attempt on npm 10 — #240's merge on June 4 — failed with
E404, as did the two release runs after it (#245, #244).

Failed runs:
-
[27136548767](https://github.com/supabase/ssr/actions/runs/27136548767)
— PR #244 ("release 0.11.0") merge, tried to publish `0.12.0-rc.118`
-
[27008002122](https://github.com/supabase/ssr/actions/runs/27008002122)
— PR #245 merge
-
[26949675017](https://github.com/supabase/ssr/actions/runs/26949675017)
— PR #240 merge, tried to publish `0.11.0-rc.117`

Last successful publish: `v0.10.3` on May 7 (run
[25509681243](https://github.com/supabase/ssr/actions/runs/25509681243)),
which ran on npm 11 via the corepack line that #221 removed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants