From 14a50b44e29151e8548c995c07d6594a882e8728 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 15 Aug 2026 16:01:21 +0200 Subject: [PATCH] github_device_auth: configurable endpoints for web builds (v0.2.0) diet-guard's copy was not cosmetic drift from todo's: it takes the device-code and token URLs as parameters, because GitHub's device-flow endpoints send no CORS headers and its desktop **web** build has to route both through a local proxy (`github_client_factory_web.dart` passes them). Shipping only todo's hardcoded version would have broken that build the moment diet-guard adopted the package. So the package takes the more capable variant: `deviceCodeUrl`/`tokenUrl` default to GitHub's own URLs, leaving every other consumer unaffected, and `githubDeviceCodeUrl`/`githubTokenUrl` are exported so callers can reference the defaults rather than retyping the literals. 23 tests (two new ones pin both the defaults and an override actually being used for the request), 100% line coverage, clean analyze. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno --- github_device_auth/README.md | 13 ++++- .../lib/github_device_auth.dart | 7 ++- github_device_auth/lib/src/device_auth.dart | 26 +++++++-- github_device_auth/pubspec.yaml | 2 +- github_device_auth/test/device_auth_test.dart | 58 +++++++++++++++++++ 5 files changed, 98 insertions(+), 8 deletions(-) diff --git a/github_device_auth/README.md b/github_device_auth/README.md index c38b956..c788bcb 100644 --- a/github_device_auth/README.md +++ b/github_device_auth/README.md @@ -34,7 +34,7 @@ dependencies: github_device_auth: git: url: https://github.com/kuhyx/utils - ref: github_device_auth-v0.1.0 + ref: github_device_auth-v0.2.0 path: github_device_auth ``` @@ -48,6 +48,15 @@ final token = await showDialog( auth.close(); ``` +## Configurable endpoints (web builds) + +`deviceCodeUrl` / `tokenUrl` default to GitHub's own URLs, which is right for +mobile and desktop. A **web** build must point both at a local proxy instead: +GitHub's device-flow endpoints send no CORS headers, so a page cannot call +them at all. diet-guard's desktop web build does exactly that, which is why +this knob exists — it was in diet-guard's local copy and would have been lost +had the package shipped only todo's simpler version. + ## One behaviour change from the extracted copies The dialog now catches `on Object`, not `on Exception`. @@ -63,7 +72,7 @@ pins it with a mock that throws an `Error` rather than an `Exception`. ```bash flutter pub get -flutter test # 20 tests +flutter test # 23 tests flutter test --coverage # 100% of lines flutter analyze # very_good_analysis, clean ``` diff --git a/github_device_auth/lib/github_device_auth.dart b/github_device_auth/lib/github_device_auth.dart index 102bf83..ab21520 100644 --- a/github_device_auth/lib/github_device_auth.dart +++ b/github_device_auth/lib/github_device_auth.dart @@ -18,5 +18,10 @@ library; export 'src/device_auth.dart' - show DeviceAuthException, DeviceCodeResponse, GitHubDeviceAuth; + show + DeviceAuthException, + DeviceCodeResponse, + GitHubDeviceAuth, + githubDeviceCodeUrl, + githubTokenUrl; export 'src/device_code_dialog.dart' show DeviceCodeDialog; diff --git a/github_device_auth/lib/src/device_auth.dart b/github_device_auth/lib/src/device_auth.dart index f22ca5c..5dc6ecb 100644 --- a/github_device_auth/lib/src/device_auth.dart +++ b/github_device_auth/lib/src/device_auth.dart @@ -3,6 +3,12 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +/// GitHub's device-code endpoint. The default for [GitHubDeviceAuth]. +const githubDeviceCodeUrl = 'https://github.com/login/device/code'; + +/// GitHub's device-flow token endpoint. The default for [GitHubDeviceAuth]. +const githubTokenUrl = 'https://github.com/login/oauth/access_token'; + /// First-stage response of the GitHub OAuth Device Flow: the code the user /// types on github.com and the URL to type it into. class DeviceCodeResponse { @@ -72,6 +78,8 @@ class GitHubDeviceAuth { GitHubDeviceAuth({ required this.clientId, this.scope = 'repo', + this.deviceCodeUrl = githubDeviceCodeUrl, + this.tokenUrl = githubTokenUrl, http.Client? httpClient, Future Function(Duration)? delay, }) : _http = httpClient ?? http.Client(), @@ -84,17 +92,27 @@ class GitHubDeviceAuth { /// OAuth scope requested. `repo` is required for private-repo contents. final String scope; + /// Endpoint asked for a device + user code. + /// + /// Defaults to GitHub's own URL, which is right for mobile and desktop. + /// A **web** build must point this (and [tokenUrl]) at a local proxy + /// instead: GitHub's device-flow endpoints send no CORS headers, so a page + /// cannot call them at all. diet-guard's desktop web build does exactly + /// that — see its `desktop/github_proxy.dart`. + final String deviceCodeUrl; + + /// Endpoint polled for the access token. See [deviceCodeUrl]. + final String tokenUrl; + final http.Client _http; final Future Function(Duration) _delay; - static const _deviceCodeUrl = 'https://github.com/login/device/code'; - static const _tokenUrl = 'https://github.com/login/oauth/access_token'; static const _grantType = 'urn:ietf:params:oauth:grant-type:device_code'; /// Step 1: ask GitHub for a device + user code. Future requestDeviceCode() async { final res = await _http.post( - Uri.parse(_deviceCodeUrl), + Uri.parse(deviceCodeUrl), headers: const {'Accept': 'application/json'}, body: {'client_id': clientId, 'scope': scope}, ); @@ -118,7 +136,7 @@ class GitHubDeviceAuth { while (DateTime.now().isBefore(deadline)) { await _delay(Duration(seconds: intervalSeconds)); final res = await _http.post( - Uri.parse(_tokenUrl), + Uri.parse(tokenUrl), headers: const {'Accept': 'application/json'}, body: { 'client_id': clientId, diff --git a/github_device_auth/pubspec.yaml b/github_device_auth/pubspec.yaml index abba60c..564bf50 100644 --- a/github_device_auth/pubspec.yaml +++ b/github_device_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: >- modal that drives it. Extracted from four apps that each carried a copy, two of them byte-identical. publish_to: 'none' -version: 0.1.0 +version: 0.2.0 environment: sdk: ^3.12.2 diff --git a/github_device_auth/test/device_auth_test.dart b/github_device_auth/test/device_auth_test.dart index a5ed488..dafd353 100644 --- a/github_device_auth/test/device_auth_test.dart +++ b/github_device_auth/test/device_auth_test.dart @@ -80,6 +80,39 @@ void main() { expect(device.userCode, 'UC'); }); + test('defaults to GitHub s own endpoints', () { + final auth = GitHubDeviceAuth(clientId: 'cid')..close(); + expect(auth.deviceCodeUrl, githubDeviceCodeUrl); + expect(auth.tokenUrl, githubTokenUrl); + }); + + test('honours an overridden endpoint', () async { + // diet-guard's desktop web build routes both endpoints through a local + // proxy, because GitHub's device-flow endpoints send no CORS headers + // and a page cannot call them at all. + late Uri seen; + final auth = GitHubDeviceAuth( + clientId: 'cid', + deviceCodeUrl: 'http://127.0.0.1:9/gh/auth/device/start', + httpClient: MockClient((req) async { + seen = req.url; + return http.Response( + jsonEncode({ + 'device_code': 'dc', + 'user_code': 'UC', + 'verification_uri': 'u', + }), + 200, + ); + }), + delay: noDelay, + ); + + await auth.requestDeviceCode(); + + expect(seen.toString(), 'http://127.0.0.1:9/gh/auth/device/start'); + }); + test('throws with the status code on a non-200', () async { final auth = authWith(MockClient((_) async => http.Response('bad', 503))); await expectLater( @@ -228,4 +261,29 @@ void main() { final auth = GitHubDeviceAuth(clientId: 'cid')..close(); expect(auth.scope, 'repo'); }); + + test('polls the overridden token endpoint', () async { + late Uri seen; + final auth = GitHubDeviceAuth( + clientId: 'cid', + tokenUrl: 'http://127.0.0.1:9/gh/auth/device/poll', + httpClient: MockClient((req) async { + seen = req.url; + return http.Response(jsonEncode({'access_token': 'tok'}), 200); + }), + delay: noDelay, + ); + + await auth.pollForToken( + const DeviceCodeResponse( + deviceCode: 'dc', + userCode: 'UC', + verificationUri: 'u', + interval: 1, + expiresIn: 60, + ), + ); + + expect(seen.toString(), 'http://127.0.0.1:9/gh/auth/device/poll'); + }); }