From b7f5ea7b2d737e8bb24f8f929a7472ea21bc1b0c Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 6 Aug 2026 01:27:29 -0700 Subject: [PATCH 1/2] feat(pnpm-policy-graphile): publish the Graphile allowlist as data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second inventory package, so a workspace consuming Graphile can pair it with @constructive-io/pnpm-policy instead of naming an npm account. Why not an account: `maintainers` exempts everything an account ever publishes, and the account behind the Graphile stack also co-maintains `graphql` — the reference implementation — plus graphiql, monaco-graphql and graphql-language-service. Those were swept into our exemption list, so the most depended-upon package in the ecosystem was skipping the quarantine, and `graphql` has six maintainers, meaning a compromise of any one of them shipped a release we installed instantly. An account is a delegation as wide as everything that account will ever touch. Fine for accounts we control; wrong for anyone who also maintains popular upstream software. So this enumerates names. Derivation, reproducible rather than curated by taste: 1. every package the workspaces resolve, read from lockfiles, so transitive dependencies count, not just direct ones 2. keep those whose npm `repository` is Graphile-owned (github.com/graphile/*, github.com/graphile-contrib/*) 3. drop the rest `constructive` and `constructive-db` independently resolve the same 16. graphql, graphiql, monaco-graphql, graphql-language-service, @graphiql/*, graphql-tag, graphql-upload and graphql-ws all fail step 2 and now wait like any other third-party dependency. No scope globs: `@graphile/*` would exempt whatever is published into that scope next, which is the same open-ended delegation as a maintainer entry, only smaller. test.js guards the boundary with plain node asserts — no framework, runs anywhere — failing if a forbidden name appears, if scopes or maintainers stop being empty, or if the list is unsorted or has duplicates. Needs the multi-inventory support in the tool (#116) to be paired with @constructive-io/pnpm-policy; usable on its own before that. --- packages/graphile-pnpm-policy/README.md | 39 ++++++++++++++ packages/graphile-pnpm-policy/inventory.json | 23 +++++++++ packages/graphile-pnpm-policy/package.json | 46 +++++++++++++++++ packages/graphile-pnpm-policy/test.js | 53 ++++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 packages/graphile-pnpm-policy/README.md create mode 100644 packages/graphile-pnpm-policy/inventory.json create mode 100644 packages/graphile-pnpm-policy/package.json create mode 100644 packages/graphile-pnpm-policy/test.js diff --git a/packages/graphile-pnpm-policy/README.md b/packages/graphile-pnpm-policy/README.md new file mode 100644 index 0000000..d37bffd --- /dev/null +++ b/packages/graphile-pnpm-policy/README.md @@ -0,0 +1,39 @@ +# @constructive-io/pnpm-policy-graphile + +Release-age exemptions for the Graphile packages Constructive runs. + +Pair it with `@constructive-io/pnpm-policy` (our own packages) in any workspace that consumes Graphile: + +```yaml +inventory: + - "@constructive-io/pnpm-policy" + - "@constructive-io/pnpm-policy-graphile" +``` + +`pnpm-policy` merges the two, and `intersect: true` narrows the result to whatever that workspace's lockfile actually resolves — so a repo with no Graphile dependency emits none of these names. + +## Why this is a list and not a maintainer query + +The obvious way to exempt an upstream is to add its npm account under `maintainers`. That is what we did first, and it was wrong. + +`maintainers` exempts **everything the account publishes**, and the account that publishes the Graphile stack also co-maintains `graphql` — the reference implementation — along with `graphiql`, `monaco-graphql`, and `graphql-language-service`. Those swept into our exemption list, so the most widely depended-upon package in the JavaScript ecosystem was skipping the quarantine. Worse, `graphql` has six maintainers, so a compromise of *any* of those accounts would have published a release that installed immediately, with no wait. + +An account is a delegation as wide as everything that account will ever touch. That is fine for accounts we control and wrong for anyone who also maintains popular upstream software. + +So this package enumerates package names instead. Nothing is exempt unless it is written down here. + +## How the list is derived + +1. Take every package the Constructive workspaces resolve (read from their lockfiles, so transitive dependencies are included — not just direct ones). +2. Keep the ones whose npm `repository` points at a Graphile-owned repository (`github.com/graphile/*`, `github.com/graphile-contrib/*`). +3. Drop everything else, including packages that merely look Graphile-adjacent. + +Step 2 is the whole test, and it is checkable: `npm view repository.url`. `graphql`, `graphiql`, `monaco-graphql`, `graphql-language-service`, `@graphiql/*`, `graphql-tag`, `graphql-upload` and `graphql-ws` all fail it — they belong to `graphql/graphql-js`, `graphql/graphiql`, Apollo, and individual authors. They wait like any other third-party dependency. + +The current list is the union across `constructive` and `constructive-db`, which independently resolve the same 16 packages. + +Scope globs are deliberately **not** used. `@graphile/*` or `@dataplan/*` would exempt anything published into those scopes in the future, which is the same open-ended delegation as a maintainer entry, just smaller. Names cost a line each and say exactly what was decided. + +## Updating it + +Adding a Graphile dependency that is not listed here means the install waits out the quarantine. Add the name, note why it is needed, and confirm its `repository` is Graphile-owned before merging. That review is the point: this file is a trust boundary, and it should be as boring and explicit as one. diff --git a/packages/graphile-pnpm-policy/inventory.json b/packages/graphile-pnpm-policy/inventory.json new file mode 100644 index 0000000..4052206 --- /dev/null +++ b/packages/graphile-pnpm-policy/inventory.json @@ -0,0 +1,23 @@ +{ + "generatedAt": "2026-08-06T08:25:26.000Z", + "maintainers": [], + "scopes": [], + "packages": [ + "@dataplan/json", + "@dataplan/pg", + "@graphile-contrib/pg-many-to-many", + "@graphile/lru", + "grafast", + "grafserv", + "graphile-build", + "graphile-build-pg", + "graphile-config", + "graphile-utils", + "pg-introspection", + "pg-sql2", + "postgraphile", + "ruru", + "ruru-types", + "tamedevil" + ] +} diff --git a/packages/graphile-pnpm-policy/package.json b/packages/graphile-pnpm-policy/package.json new file mode 100644 index 0000000..a23ec51 --- /dev/null +++ b/packages/graphile-pnpm-policy/package.json @@ -0,0 +1,46 @@ +{ + "name": "@constructive-io/pnpm-policy-graphile", + "version": "0.1.0", + "author": "Constructive ", + "description": "Release-age exemptions for the Graphile packages Constructive runs \u2014 a reviewed allowlist, not a maintainer query", + "homepage": "https://github.com/constructive-io/dev-utils", + "license": "MIT", + "exports": { + "./inventory.json": "./inventory.json", + "./package.json": "./package.json" + }, + "files": [ + "inventory.json", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "repository": { + "type": "git", + "url": "https://github.com/constructive-io/dev-utils" + }, + "bugs": { + "url": "https://github.com/constructive-io/dev-utils/issues" + }, + "scripts": { + "clean": "makage clean", + "copy": "makage assets", + "build": "makage clean && makage copy inventory.json dist --flat && makage assets", + "prepublishOnly": "npm run build", + "test": "node test.js" + }, + "devDependencies": { + "makage": "0.1.10" + }, + "keywords": [ + "pnpm", + "pnpm-policy", + "supply-chain", + "minimumReleaseAge", + "graphile", + "inventory" + ] +} diff --git a/packages/graphile-pnpm-policy/test.js b/packages/graphile-pnpm-policy/test.js new file mode 100644 index 0000000..505c320 --- /dev/null +++ b/packages/graphile-pnpm-policy/test.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node +/** + * Guards the trust boundary this package exists to draw. + * + * Every name here is exempt from the release-age quarantine, so the list must + * stay Graphile-owned. The account that publishes Graphile also co-maintains + * `graphql` and the GraphiQL packages; a maintainer-derived list swept those in + * once already, which is why this package enumerates names instead. + * + * Plain node asserts, no test framework: this is data, and the check should run + * anywhere with no install. + */ +const assert = require('assert'); +const inventory = require('./inventory.json'); + +const FORBIDDEN = [ + 'graphql', + 'graphiql', + 'monaco-graphql', + 'graphql-language-service', + 'graphql-tag', + 'graphql-upload', + 'graphql-ws' +]; + +assert.ok(Array.isArray(inventory.packages), 'inventory.packages must be an array'); +assert.ok(inventory.packages.length > 0, 'inventory.packages must not be empty'); + +// Scope globs would exempt anything published into that scope later — the same +// open-ended delegation as a maintainer entry. Names only. +assert.deepStrictEqual(inventory.scopes, [], 'scopes must stay empty: enumerate names instead'); +assert.deepStrictEqual( + inventory.maintainers, + [], + 'maintainers must stay empty: this is a reviewed allowlist, not a maintainer query' +); + +for (const name of FORBIDDEN) { + assert.ok( + !inventory.packages.includes(name), + `${name} is not Graphile-owned and must not skip the quarantine` + ); +} + +const sorted = [...inventory.packages].sort(); +assert.deepStrictEqual(inventory.packages, sorted, 'packages must stay sorted for reviewable diffs'); +assert.strictEqual( + new Set(inventory.packages).size, + inventory.packages.length, + 'packages must not contain duplicates' +); + +console.log(`ok — ${inventory.packages.length} Graphile packages, no forbidden names`); From 0d8258955263cc114aa594cd8b0339c4d6ec4717 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 6 Aug 2026 02:32:51 -0700 Subject: [PATCH 2/2] fix: register the new package in the lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI installs with --frozen-lockfile, so adding a workspace package without its lockfile entry fails the build before anything runs: ERR_PNPM_OUTDATED_LOCKFILE pnpm-lock.yaml is not up to date with /packages/graphile-pnpm-policy/package.json Adds the importer stanza by hand rather than regenerating. Regenerating rewrites 7897 lines: the committed lockfile was written by a different pnpm major, so any write reformats the whole file, which would bury a data-only PR in unrelated churn and risk dropping entries. Verified with the version CI uses — `pnpm@10.12.2 install --frozen-lockfile --lockfile-only` resolves all 40 workspace projects with no outdated-lockfile error. That command reformats the file as a side effect, so the reformat is discarded and only the 7-line stanza is committed. --- pnpm-lock.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b705ee..5e0cee9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,13 @@ importers: version: 0.1.10 publishDirectory: dist + packages/graphile-pnpm-policy: + devDependencies: + makage: + specifier: 0.1.10 + version: 0.1.10 + publishDirectory: dist + packages/http-errors: devDependencies: makage: