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.
Extend SPEC §16 requirements 5–6 (dial-time address judgement of the advertised issuer,
token_endpoint, anddevice_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:
Agentwith aconnect.lookuphook (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).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'sfetchwith a per-requestdispatcherwhen 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.AllowLoopback), never inferred from the endpoint URL or other request-derived values. The existingisLocalhostHTTPS exemption is a scheme judgement, not address admission.fetchcounts — and stays "yours, enforcement included").invalid_issuer_originon the discovery hop,api_errorelsewhere, 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.