basecamp / basecamp/basecamp-sdk
TypeScript: enforce the OAuth address policy via an undici lookup seam, settling the global-fetch conflict (SPEC §16 req 5–6)
- Dominant language
- Go
- Stars
- 49
- Forks
- 12
- Avg merge
- 20h 47m
- Merged PRs (30d)
- 89
Description
Extend SPEC §16 requirements 5–6 (dial-time address judgement of the advertised issuer, `token_endpoint`, and `device_authorization_endpoint`) to the TypeScript SDK. Appendix F records today's state: scheme gate, bounded timeout, `redirect: "manual"`, bounded body — a private address is still dialed.
**Step 0 (upstream, blocking):** a TypeScript surfguard classification-core port in the surfguard repo (classification + resolve, IANA generator over `script/iana/*.json`, mirrored 157-case corpus with byte-equality drift tests). Tracked as basecamp/surfguard#25.
**Enforcement design and the open question to settle first:**
- The seam is an undici `Agent` with a `connect.lookup` hook (Node ≥18): resolve, judge every resolved address, and hand the connector only the addresses that passed — connect-time judgement with no second resolution, which is the DNS-rebinding requirement (the address judged MUST be the address connected).
- **The conflict to resolve:** the SDK's OAuth surface is written against global `fetch` (WHATWG), which does not accept an undici dispatcher portably. Options to evaluate in the design: (a) accept a Node-only enforcement tier — the policy applies when running on Node/undici and is documented absent elsewhere; (b) use undici's `fetch` with a per-request `dispatcher` when undici is importable, falling back to global fetch (documented unenforced) otherwise; (c) an injectable "policed fetch" the SDK builds on Node. The issue is not prejudged; the chosen answer must state the non-Node story explicitly in Appendix F rather than leaving it to inference.
- **Loopback is explicit operator configuration:** a policy option on the OAuth entry points (mirroring Go's `AllowLoopback`), never inferred from the endpoint URL or other request-derived values. The existing `isLocalhost` HTTPS exemption is a scheme judgement, not address admission.
- **Overrides (SPEC §16 contract):** replacement policy, replacement fetch/client, disablement (a custom `fetch` counts — and stays "yours, enforcement included").
- **Verdicts:** `invalid_issuer_origin` on the discovery hop, `api_error` elsewhere, non-retryable, device poll terminates on first refusal; unresolvable stays a retryable network fault.
**Definition of done:** Appendix F rows updated; unit tests assert refusal-before-dial for private/loopback/CGNAT/special-purpose targets on discovery, device, and exchange paths, plus override and custom-fetch contracts, on the supported (Node) tier.
Contributor guide
Research direction
Start with the TypeScript surfguard dependency tracked in basecamp/surfguard#25, then evaluate the undici Agent connect.lookup seam against the SDK's global-fetch OAuth surface. Trace the discovery, device, and exchange paths and the OAuth entry-point policy options. Done means the Node-tier refusal-before-dial, override, custom-fetch, and device-poll tests pass and Appendix F documents the non-Node behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, typescript
- Domain
- api, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100