mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:47:20 +01:00
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
This commit is contained in:
parent
795a8eae62
commit
14a50b44e2
@ -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<String>(
|
||||
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
|
||||
```
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<void> 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<void> 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<DeviceCodeResponse> 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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user