Signed-in readers of the Internet Public Library edit pages and suggest
resources — every change becomes a pull request reviewed by code owners.
Sign in with GitHub or ORCID · runs on Cloudflare Workers · hard fork of
Staticman (MIT)
Status: the Workers service in
src/implements specs 001–003 — see the roadmap. The upstream Staticman code was removed after the port; it remains in the git history and is credited in NOTICE.md.
Reader on an IPL page ──► sign in (GitHub or ORCID) ──► edit page / suggest resource
│
contribute (Cloudflare Worker) ◄─────┘
│ validates, rate-limits, credits the author
▼
IPL GitHub App opens a pull request on the content repository
(en-US, pt-BR or IPLibrary-website _data) ──► CODEOWNERS review ──► merge ──► site rebuild
- Nothing is published without review — the service never commits to a default branch (ADR 0003).
- Authors are credited: GitHub users as commit co-authors; ORCID users by name and ORCID iD in the PR.
- No passwords, no e-mail: sessions hold only the provider, a stable id, the display name and profile URL — see the privacy note.
Defined in spec 003 — the contract consumed by the IPL site:
| Route | Purpose |
|---|---|
GET /v1/auth/:provider/start?return_to= · /callback · GET /v1/session · POST /v1/auth/logout |
Sign-in with GitHub or ORCID (spec 002) — available |
GET /v1/source?url= |
Source file of an IPL page (409 GENERATED_PAGE for topic pages built from the menu) — available |
POST /v1/edits |
Propose a page edit with the sha you read (opens a PR; 409 STALE_SHA with the current content if the page changed) — available |
POST /v1/suggestions |
Suggest a resource for a topic (link must answer; opens a PR on _data) — available |
GET /v1/contributions |
The signed-in author's pull requests — available |
- Title:
Edit: <page title>orResource suggestion: <name>, one line, at most 120 characters. - Labels:
contribution,editorsuggestion,lang:en-US/lang:pt-BR,topic:<category>for suggestions andcontains-htmlwhen an edit adds inline HTML. GitHub creates a label the first time it is used, so content repositories need no setup; maintainers can recolour or describe them afterwards. Labelling needs the App's Pull requests: write permission, already in the manifest. - Body: see spec 003, "Pull request body".
Writes need a signed-in session, an IPL origin and the CSRF token. Before any GitHub call, each identity is checked
against a deny-list and two caps (20 contributions a day, 5 open at once → 429 with Retry-After); bodies are capped
(edits 272 KB, suggestions 16 KB → 413); a per-IP limit protects sign-in and writes, and link checks for suggestions are capped at 50 a day per identity. The
daily slot is reserved before GitHub is called and given back if the contribution fails; the open-PR slot is freed by
the webhook when the pull request closes. KV has no atomic counters, so parallel requests can exceed a cap by a few
(bounded by the per-IP limit). To block an account (maintainers):
npx wrangler kv key put --binding CONTRIBUTE_KV --env production "deny:github:<numeric id>" "reason" # or deny:orcid:<iD>Node.js 22+ and npm.
npm ci
npm run dev # wrangler dev on http://localhost:8787
npm run check # lint + typecheck + tests (Vitest inside workerd)Tests live next to the code (src/**/*.test.ts) and run in the Workers runtime through @cloudflare/vitest-plugin.
CI runs the same lint, typecheck and test scripts on every pull request.
| Name | Kind | Purpose |
|---|---|---|
CONTRIBUTE_KV |
KV binding | GitHub installation ids and tokens (and sessions, spec 002) — one namespace per environment |
IPL_ALLOWED_ORIGINS |
var | Exact https origins of the IPL site (comma-separated); http://localhost only in development |
CONTENT_REPOSITORIES |
var | JSON map role → owner/repo (en-US, pt-BR, website) |
ORCID_ENVIRONMENT |
var | sandbox (default outside production) or production |
MAX_PRS_PER_DAY, MAX_OPEN_PRS |
vars | Per-identity caps (defaults 20 a day, 5 open at once) |
IP_RATE_LIMITER |
rate-limit binding | Coarse per-IP limit (30/min) on sign-in starts and writes; staging and production |
GITHUB_APP_ID |
var | Id of the IPL GitHub App |
GITHUB_APP_PRIVATE_KEY |
secret | The App's private key in PKCS#8 PEM |
GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET |
secrets | GitHub OAuth App for sign-in only (scope read:user); callback https://<service>/v1/auth/github/callback |
ORCID_CLIENT_ID, ORCID_CLIENT_SECRET |
secrets | ORCID API client for the environment's registry (sandbox or production); callback https://<service>/v1/auth/orcid/callback |
GITHUB_WEBHOOK_SECRET |
secret | The App's webhook secret; POST /v1/webhook answers 503 without it and 401 to any delivery whose X-Hub-Signature-256 does not match |
For local development copy .dev.vars.example to .dev.vars (git-ignored). Configure the App webhook with content
type application/json and the events pull_request and installation.
| Environment | Worker | Site origin | Repositories | ORCID |
|---|---|---|---|---|
development (npm run dev) |
local | http://localhost:4000 |
contribute-sandbox |
sandbox |
staging (--env staging) |
ipl-contribute-staging |
https://staging.ipl.better-web.org |
contribute-sandbox |
sandbox |
production (--env production) |
ipl-contribute |
https://ipl.better-web.org |
en-US, pt-BR, IPLibrary-website |
production |
The service must be same-site with the IPL pages: the session cookie is SameSite=Lax, so a *.workers.dev
address would never receive it from ipl.better-web.org. Staging and production therefore run on custom domains of
the IPL zone (contribute.staging.ipl.better-web.org, contribute.ipl.better-web.org) with workers_dev off.
A development or staging Worker refuses to start work if any configured repository is a production one, and only
production may use the ORCID production registry. Pull requests always target the repository's default branch,
read from the GitHub API (cached for an hour) — neither requests nor contribute.yml choose the branch.
Repository rules are read from contribute.yml on that branch (upstream's staticman.yml is still accepted).
GitHub downloads App keys as PKCS#1 (BEGIN RSA PRIVATE KEY), which Web Crypto cannot import. Convert once and
store the result:
openssl pkcs8 -topk8 -nocrypt -in ipl-contribute.private-key.pem -out ipl-contribute.pkcs8.pem
npx wrangler secret put GITHUB_APP_PRIVATE_KEY --env production < ipl-contribute.pkcs8.pemApp permissions, installation, the default-branch ruleset and private key rotation are documented in docs/github/app.md (ADR 0004).
The service authenticates only as the GitHub App; no personal or user token is ever used to write. Opening one pull request costs 4 GitHub API calls with warm caches (base ref, new branch, file, pull request) and 2 more on a cold cache (installation id, installation token, both cached in KV).
| Spec | Scope |
|---|---|
| 001 Workers runtime | Port from Express to a Hono Worker, GitHub App via Web Crypto |
| 002 Sign-in | GitHub and ORCID OAuth (PKCE), KV sessions |
| 003 Edits and suggestions | The API above, PR creation, validation, rate limits |
Decisions: docs/adr/. Contributor notes: AGENTS.md.
IPLibrary-website (consumer, spec 003) · discuss (comments) · Better Web Initiative
MIT — see LICENSE.md and NOTICE.md. Based on Staticman by Eduardo Bouças; the upstream README is kept in docs/UPSTREAM-README.md.
