# Contributing to @udibo/oauth2 Thank you for your interest in contributing! This document covers development setup, conventions, and the release process. ## Code of Conduct Please be respectful and inclusive in all interactions. We are committed to providing a welcoming and harassment-free experience for everyone. Be kind, constructive, and professional in your communications. ## Security issues **Never open a public issue for a security vulnerability.** Follow [SECURITY.md](https://www.udibo.com/docs/oauth2/security) instead. ## Getting started ### Prerequisites - [Node.js](https://nodejs.org/) 22.18 or later (`.nvmrc` pins 26). The scripts are TypeScript that Node runs directly, which needs 22.18+. - [pnpm](https://pnpm.io/) 12 (`packageManager` in `package.json` names the exact version; `corepack enable` installs it) - [Git](https://git-scm.com/) - A code editor with TypeScript and [oxc](https://oxc.rs/) support ### Development setup ```bash pnpm install # install dependencies pnpm test # package and script tests (Vitest) pnpm test src/utils/crypto.test.ts # one file pnpm test:coverage # with V8 coverage pnpm exec playwright install chromium # once, for the browser test pnpm test:browser # React form behavior in headless Chromium pnpm check # lint, format check, types, and docs gates pnpm fmt # format pnpm build && pnpm smoke # pack the package and prove it from a clean consumer pnpm jsr:dry-run # check the JSR publish ``` Run all commands from the repository root. The example apps and the template are pnpm workspace members; run one with `pnpm --filter start` or `test` after `pnpm build` (see each README for its name and port). ### Repository structure ``` oauth2/ ├── src/ # Package source (what publishes to JSR) │ ├── server/ # Authorization + resource servers, grants, signing keys │ ├── client/ # Typed OAuth2 client │ ├── identity/ # Own-auth flows (sign-in, reset, verification, lockout) │ ├── adapters/hono/ # Hono adapters (servers, BFF, identity routes) │ ├── cli/ # The `udibo-oauth2` command (bin): keygen, local identity provider │ ├── react/ # React bindings for the BFF │ ├── testing/ # Test helpers + contract test suites │ ├── models/ # Shared protocol types │ └── utils/ # Crypto, URL, PKCE helpers ├── examples/hono/ # Runnable example apps ├── templates/ # Auth-preconfigured starter (React Router) ├── docs/ # Quickstart, guides, stability policy, known limitations, triage ├── scripts/ # Documentation gates, npm smoke test, release guards, PSL updater ├── PUBLISHING.md # Maintainer runbook: release configuration and verification (not shipped) ├── package.json # npm manifest; `exports` map to dist/ ├── jsr.json # JSR manifest; `exports` map to src/ ├── CHANGELOG.md # Generated by semantic-release ├── SECURITY.md # Vulnerability disclosure process └── .releaserc.json # Release pipeline configuration ``` `src/` is erasable TypeScript with explicit `.ts` import extensions: `tsc` (`pnpm build`) emits `dist/` for npm, and the same files publish to JSR as written. Keep the two export maps (`package.json` and `jsr.json`) in step; `pnpm smoke` fails when an entrypoint is missing from the tarball or from `scripts/npm-smoke/consumer.tsx`. ## Making changes 1. **Branch from `main`** — short-lived feature branches, squash-merged. 2. **Write tests.** Every behavioral change needs corresponding coverage; the security-sensitive paths (enumeration safety, timing equalization, throttle and lockout behavior) are guarded by tests and must stay that way. 3. **Keep repository links relative in documentation.** `pnpm doc:links` checks that every local link and heading resolves. `llms-full.txt` resolves each page's links to the public repository before concatenation so they keep their original context; regenerate it with `pnpm llms:generate` after editing a guide, and list any new page in `llms.txt`. 4. **No new runtime dependencies without discussion.** The package is deliberately close to zero-dependency; crypto uses Web Crypto only. 5. **Respect the design invariants below.** They are the shape of the package, not preferences, and each one has been re-litigated at least once. 6. **Run the gate before pushing:** `pnpm check` and `pnpm test`. `pnpm check` runs the type-aware lint, the format check, `tsc`, and the documentation gates: `doc:lint` requires JSDoc on every exported symbol and public member, `doc:check` type-checks the fenced snippets in the markdown **and** every JSDoc `@example` in the API reference (mark a fence `` ```ts ignore `` when a snippet is deliberately not compilable; the number of opt-outs is capped), `doc:links` checks local links, and `llms:check` checks the AI-ready index and corpus. Run `pnpm build && pnpm smoke` after changing an export map, a dependency, or anything that affects the packed files. 7. **Run `pnpm test:browser` after changing the React form components.** It drives the drop-in forms in headless Chromium through Playwright for behavior jsdom cannot show, such as where focus goes while a submit is pending. Install the browser once with `pnpm exec playwright install chromium`. CI runs it as its own job. ### Design invariants The package sits deliberately below the turnkey-identity line: the application owns identity policy, and the library owns the protocol. Four invariants carry that, and a change that erodes one is a change to the positioning. - **`ClientInterface` stays minimal** — `id`, `grants`, `redirectUris`, the fields the framework actually reads. Application policy (`firstParty`, `userId`) does not belong in the library's client model; the owner user is resolved through `clientService.getUser(client)`, so the service decides how. Because an application's own entity already has those three fields, it is returned as the client directly — no adapter type, no mapper. Trust decisions go in `handleConsent`, which is **optional**: with no handler configured the request is treated as consented, so a self-hosted server whose clients are all first-party is never forced to write one. - **Errors are `HttpError`.** `OAuth2Error` and friends are built with `createHttpErrorClass` from `@udibo/http-error`, so `instanceof HttpError` holds and each carries `extensions.error` — the OAuth2 code — plus a status. That is what lets a consumer render them as RFC 9457 problem details with the codes as extensions (`errorFormat`). Never re-flatten one to a generic `HttpError(403, "…")`: it discards the code and the `WWW-Authenticate` header. Let them flow through. - **Rate limiting is a bring-your-own seam.** `IdentityService` keeps the small `RateLimiter` contract (`check` / `reset`) and the package takes no dependency on any limiter library. The built-in fixed-window `RateLimiter` / `MemoryRateLimitStore` is the zero-dependency implementation and is **feature frozen** — no bursts, penalties, or distributed stores. Its `store` is required, so the caller chooses where counters live. Anything beyond fixed-window arrives through the seam, wired app-side or shown in a docs example. - **Examples contain no test-only seams.** They are reference implementations of production code, so they construct `IntrospectionTokenReader`, `HonoAuthorizationServer` and the rest as module-level constants — no factory functions, no injected `fetch` option, no dependency-injection parameter whose only justification is testability. A test that needs to fake a downstream call stubs the method that makes it (`vi.spyOn(tokenReader, "getToken")`); one that genuinely needs the wire uses a real listener on an ephemeral port. The framework's own tests already cover the wire format — an example test only proves the wiring. ### Commit messages [Conventional Commits](https://www.conventionalcommits.org/) — they drive releases and the changelog: - `feat:` new feature (minor), `fix:`/`perf:` (patch) - A breaking change is marked with `!` after the type or scope (`feat!:`, `fix(testing)!:`) or a `BREAKING CHANGE:` footer. Either cuts a minor while 0.x (see [docs/stability.md](https://www.udibo.com/docs/oauth2/stability) for what qualifies pre-1.0) and lists the change under **⚠ BREAKING CHANGES** in the release notes. The notes quote the subject unless a footer is present; PRs are squash-merged with the PR body as the commit body, so to put a migration note there, start a line of the PR body with `BREAKING CHANGE:` followed by the note. Write it as one paragraph in the body's `## Closes` section, directly before the `Closes #` lines, or last in the body when it closes nothing. The note runs until the first of those lines or the end of the message, so a note placed earlier carries every heading after it into the release notes. - `chore(deps):` / `build(deps):` dependency bumps (patch — the `deps` scope is required for a release to be cut) - Present tense, lowercase, subject under 72 characters. ## Releases Releases are automatic. On every push to `main`, the `release` job in `.github/workflows/ci-cd.yml` runs semantic-release, which analyzes the commits, determines the next version, generates the changelog, publishes to JSR and npm over GitHub OIDC (no stored registry token), and tags a GitHub release (`.releaserc.json`). The job runs only while the repository variable `OAUTH2_RELEASE_ENABLED` is `true`. Nobody hand-edits versions or CHANGELOG entries. The release configuration, what a release does, and how to verify a published version are in [PUBLISHING.md](https://github.com/udibo/oauth2/blob/main/PUBLISHING.md). The stability and breaking-change policy is in [docs/stability.md](https://www.udibo.com/docs/oauth2/stability); note that while the package is `0.x`, `.releaserc.json` deliberately maps a breaking change to a **minor**. ## Issue reports See [docs/triage.md](https://www.udibo.com/docs/oauth2/triage) for how issues are triaged, labeled, and prioritized.