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 instead.
Getting started
Prerequisites
Development setup
deno install # install dependencies
deno task test # run the package tests
deno task test:all # package + scripts + examples + templates
deno task check # type check, lint, and format checkRun all commands from the package root (packages/oauth2 inside the monorepo).
The example apps have their own tasks — see deno.json for the serve:* /
dev:* / test:* variants.
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)
│ ├── react/ # React bindings for the BFF
│ ├── testing/ # Test helpers + contract test suites
│ ├── models/ # Shared protocol types
│ └── utils/ # Crypto, URL, PKCE helpers
├── examples/ # Runnable example apps (Hono + Juniper)
├── templates/ # Auth-preconfigured starters (Juniper, React Router)
├── docs/ # Quickstart, guides, stability policy, known limitations, triage
├── PUBLISHING.md # Founder runbook: extract to a standalone repo and publish (not shipped)
├── smoke-consumer/ # Out-of-workspace canary: resolves the published export map under a foreign config
├── CHANGELOG.md # Generated by semantic-release once publishing starts
├── SECURITY.md # Vulnerability disclosure process
└── .releaserc.json # Release pipeline configurationsrc/ intentionally contains no README/LICENSE copies — the release pipeline
copies them in at publish time.
Making changes
Branch from
main— short-lived feature branches, squash-merged.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.
Keep repository links relative in source documentation.
deno task publish:preparestages the consumer docs and rewrites those links to the public repository, including references to examples and templates that are not part of the JSR payload.llms-full.txtresolves each page's links before concatenation so its links retain their original context.No new runtime dependencies without discussion. The package is deliberately close to zero-dependency; crypto uses Web Crypto only.
Respect the design invariants below. They are the shape of the package, not preferences, and each one has been re-litigated at least once.
Run the gate before pushing:
deno task checkanddeno task test:all.deno task testcovers the package suite only;test:allfans out to the script, example, and template suites CI also runs.deno task doc-check(part ofcheck) type-checks the fenced snippets in the markdown and every JSDoc@examplein the API reference — mark a fence```ts ignorewhen a snippet is deliberately not compilable.
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.
ClientInterfacestays 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 throughclientService.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 inhandleConsent, 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.OAuth2Errorand friends are built withcreateHttpErrorClassfrom@udibo/http-error, soinstanceof HttpErrorholds and each carriesextensions.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 genericHttpError(403, "…"): it discards the code and theWWW-Authenticateheader. Let them flow through.Rate limiting is a bring-your-own seam.
IdentityServicekeeps the smallRateLimitercontract (check/reset) and the package takes no dependency on any limiter library. The built-in fixed-windowRateLimiter/MemoryRateLimitStoreis the zero-dependency default and is feature frozen — no bursts, penalties, or distributed stores. 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,HonoAuthorizationServerand the rest as module-level constants — no factory functions, no injectedfetchoption, no dependency-injection parameter whose only justification is testability. A test that needs to fake a downstream call stubs the method that makes it (stub(tokenReader, "getToken", …)); one that genuinely needs the wire uses a realDeno.servelistener. The framework's own tests already cover the wire format — an example test only proves the wiring.
Commit messages
Conventional Commits — they drive releases and the changelog:
feat:new feature (minor),fix:/perf:(patch)feat!:or aBREAKING CHANGE:footer (minor while 0.x; see docs/stability.md for what qualifies pre-1.0)chore(deps):/build(deps):dependency bumps (patch — thedepsscope is required for a release to be cut)Present tense, lowercase, subject under 72 characters.
Releases
Nothing has been published yet. The release pipeline is written and wired,
but it is switched off: the release-oauth2-package job in this package's
.github/workflows/ci-cd.yml only runs when the repository variable
OAUTH2_RELEASE_ENABLED is true. Publishing is opt-in. The package also still
lives inside the monorepo; the pipeline is shaped for the standalone
github.com/udibo/oauth2 repository it will be extracted to.
Once the package is extracted and the gate is flipped, releases are automatic:
semantic-release analyzes commits on main, determines the next version,
generates the changelog, publishes to JSR over GitHub OIDC (no stored token),
and tags a GitHub release (.releaserc.json). Nobody hand-edits versions or
CHANGELOG entries.
The ordered steps to get there — extraction, the 0.0.0 baseline tag that makes
the first release 0.1.0 rather than 1.0.0, the JSR scope and repository
link, flipping the gate, and what to watch on the first run — are in
PUBLISHING.md. The stability and breaking-change policy is in
docs/stability.md; note that while the package is 0.x,
.releaserc.json deliberately maps a breaking change to a minor.
Issue reports
See docs/triage.md for how issues are triaged, labeled, and prioritized.

