Use package:vendor to vendor package:tar and package:oauth2 (#3638)

diff --git a/lib/src/io.dart b/lib/src/io.dart
index 2b5d0cd..6f99b5b 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -15,13 +15,14 @@
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
 import 'package:pool/pool.dart';
+// ignore: prefer_relative_imports
+import 'package:pub/src/third_party/tar/lib/tar.dart';
 import 'package:stack_trace/stack_trace.dart';
 
 import 'error_group.dart';
 import 'exceptions.dart';
 import 'exit_codes.dart' as exit_codes;
 import 'log.dart' as log;
-import 'third_party/tar/tar.dart';
 import 'utils.dart';
 
 export 'package:http/http.dart' show ByteStream;
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index 41eb5f5..4715014 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -8,8 +8,9 @@
 import 'package:collection/collection.dart' show IterableExtension;
 import 'package:http/http.dart' as http;
 import 'package:http/retry.dart';
-import 'package:oauth2/oauth2.dart';
 import 'package:path/path.dart' as path;
+// ignore: prefer_relative_imports
+import 'package:pub/src/third_party/oauth2/lib/oauth2.dart';
 import 'package:shelf/shelf.dart' as shelf;
 import 'package:shelf/shelf_io.dart' as shelf_io;
 
diff --git a/lib/src/third_party/oauth2/CHANGELOG.md b/lib/src/third_party/oauth2/CHANGELOG.md
new file mode 100644
index 0000000..0c0deb2
--- /dev/null
+++ b/lib/src/third_party/oauth2/CHANGELOG.md
@@ -0,0 +1,123 @@
+# 2.0.1
+
+* Handle `expires_in` when encoded as string.
+* Populate the pubspec `repository` field.
+* Increase the minimum Dart SDK to `2.17.0`.
+
+# 2.0.0
+
+* Migrate to null safety.
+
+# 1.6.3
+
+* Added optional `codeVerifier` parameter to `AuthorizationCodeGrant` constructor.
+
+# 1.6.1
+
+* Added fix to make sure that credentials are only refreshed once when multiple calls are made.
+
+# 1.6.0
+
+* Added PKCE support to `AuthorizationCodeGrant`.
+
+# 1.5.0
+
+* Added support for `clientCredentialsGrant`.
+
+# 1.4.0
+
+* OpenID's id_token treated.
+
+# 1.3.0
+
+* Added `onCredentialsRefreshed` option when creating `Client` objects.
+
+# 1.2.3
+
+* Support the latest `package:http` release.
+
+# 1.2.2
+
+* Allow the stable 2.0 SDK.
+
+# 1.2.1
+
+* Updated SDK version to 2.0.0-dev.17.0
+
+# 1.2.0
+
+* Add a `getParameter()` parameter to `new AuthorizationCodeGrant()`, `new
+  Credentials()`, and `resourceOwnerPasswordGrant()`. This controls how the
+  authorization server's response is parsed for servers that don't provide the
+  standard JSON response.
+
+# 1.1.1
+
+* `resourceOwnerPasswordGrant()` now properly uses its HTTP client for requests
+  made by the OAuth2 client it returns.
+
+# 1.1.0
+
+* Add a `delimiter` parameter to `new AuthorizationCodeGrant()`, `new
+  Credentials()`, and `resourceOwnerPasswordGrant()`. This controls the
+  delimiter between scopes, which some authorization servers require to be
+  different values than the specified `' '`.
+
+# 1.0.2
+
+* Fix all strong-mode warnings.
+
+* Support `crypto` 1.0.0.
+
+* Support `http_parser` 3.0.0.
+
+# 1.0.1
+
+* Support `http_parser` 2.0.0.
+
+# 1.0.0
+
+## Breaking changes
+
+* Requests that use client authentication, such as the
+  `AuthorizationCodeGrant`'s access token request and `Credentials`' refresh
+  request, now use HTTP Basic authentication by default. This form of
+  authentication is strongly recommended by the OAuth 2.0 spec. The new
+  `basicAuth` parameter may be set to `false` to force form-based authentication
+  for servers that require it.
+
+* `new AuthorizationCodeGrant()` now takes `secret` as an optional named
+  argument rather than a required argument. This matches the OAuth 2.0 spec,
+  which says that a client secret is only required for confidential clients.
+
+* `new Client()` and `Credentials.refresh()` now take both `identifier` and
+  `secret` as optional named arguments rather than required arguments. This
+  matches the OAuth 2.0 spec, which says that the server may choose not to
+  require client authentication for some flows.
+
+* `new Credentials()` now takes named arguments rather than optional positional
+  arguments.
+
+## Non-breaking changes
+
+* Added a `resourceOwnerPasswordGrant` method.
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` and
+  `new Credentials()` and the `newScopes` argument to `Credentials.refresh` now
+  take an `Iterable` rather than just a `List`.
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` now
+  defaults to `null` rather than `const []`.
+
+# 0.9.3
+
+* Update the `http` dependency.
+
+* Since `http` 0.11.0 now works in non-`dart:io` contexts, `oauth2` does as
+  well.
+
+# 0.9.2
+
+* Expand the dependency on the HTTP package to include 0.10.x.
+
+* Add a README file.
diff --git a/lib/src/third_party/oauth2/LICENSE b/lib/src/third_party/oauth2/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/lib/src/third_party/oauth2/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/src/third_party/oauth2/README.md b/lib/src/third_party/oauth2/README.md
new file mode 100644
index 0000000..196b9f7
--- /dev/null
+++ b/lib/src/third_party/oauth2/README.md
@@ -0,0 +1,260 @@
+[![Dart CI](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml)
+[![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2)
+[![package publisher](https://img.shields.io/pub/publisher/oauth2.svg)](https://pub.dev/packages/oauth2/publisher)
+
+A client library for authenticating with a remote service via OAuth2 on behalf
+of a user, and making authorized HTTP requests with the user's OAuth2
+credentials.
+
+## About OAuth2
+
+OAuth2 allows a client (the program using this library) to access and manipulate
+a resource that's owned by a resource owner (the end user) and lives on a remote
+server. The client directs the resource owner to an authorization server
+(usually but not always the same as the server that hosts the resource), where
+the resource owner tells the authorization server to give the client an access
+token. This token serves as proof that the client has permission to access
+resources on behalf of the resource owner.
+
+OAuth2 provides several different methods for the client to obtain
+authorization. At the time of writing, this library only supports the
+[Authorization Code Grant][authorizationCodeGrantSection],
+[Client Credentials Grant][clientCredentialsGrantSection] and
+[Resource Owner Password Grant][resourceOwnerPasswordGrantSection] flows, but
+more may be added in the future.
+
+## Authorization Code Grant
+
+**Resources:** [Class summary][authorizationCodeGrantMethod],
+[OAuth documentation][authorizationCodeGrantDocs]
+
+```dart
+import 'dart:io';
+
+import 'package:oauth2/oauth2.dart' as oauth2;
+
+// These URLs are endpoints that are provided by the authorization
+// server. They're usually included in the server's documentation of its
+// OAuth2 API.
+final authorizationEndpoint =
+    Uri.parse('http://example.com/oauth2/authorization');
+final tokenEndpoint = Uri.parse('http://example.com/oauth2/token');
+
+// The authorization server will issue each client a separate client
+// identifier and secret, which allows the server to tell which client
+// is accessing it. Some servers may also have an anonymous
+// identifier/secret pair that any client may use.
+//
+// Note that clients whose source code or binary executable is readily
+// available may not be able to make sure the client secret is kept a
+// secret. This is fine; OAuth2 servers generally won't rely on knowing
+// with certainty that a client is who it claims to be.
+final identifier = 'my client identifier';
+final secret = 'my client secret';
+
+// This is a URL on your application's server. The authorization server
+// will redirect the resource owner here once they've authorized the
+// client. The redirection will include the authorization code in the
+// query parameters.
+final redirectUrl = Uri.parse('http://my-site.com/oauth2-redirect');
+
+/// A file in which the users credentials are stored persistently. If the server
+/// issues a refresh token allowing the client to refresh outdated credentials,
+/// these may be valid indefinitely, meaning the user never has to
+/// re-authenticate.
+final credentialsFile = File('~/.myapp/credentials.json');
+
+/// Either load an OAuth2 client from saved credentials or authenticate a new
+/// one.
+Future<oauth2.Client> createClient() async {
+  var exists = await credentialsFile.exists();
+
+  // If the OAuth2 credentials have already been saved from a previous run, we
+  // just want to reload them.
+  if (exists) {
+    var credentials =
+        oauth2.Credentials.fromJson(await credentialsFile.readAsString());
+    return oauth2.Client(credentials, identifier: identifier, secret: secret);
+  }
+
+  // If we don't have OAuth2 credentials yet, we need to get the resource owner
+  // to authorize us. We're assuming here that we're a command-line application.
+  var grant = oauth2.AuthorizationCodeGrant(
+      identifier, authorizationEndpoint, tokenEndpoint,
+      secret: secret);
+
+  // A URL on the authorization server (authorizationEndpoint with some additional
+  // query parameters). Scopes and state can optionally be passed into this method.
+  var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
+
+  // Redirect the resource owner to the authorization URL. Once the resource
+  // owner has authorized, they'll be redirected to `redirectUrl` with an
+  // authorization code. The `redirect` should cause the browser to redirect to
+  // another URL which should also have a listener.
+  //
+  // `redirect` and `listen` are not shown implemented here. See below for the
+  // details.
+  await redirect(authorizationUrl);
+  var responseUrl = await listen(redirectUrl);
+
+  // Once the user is redirected to `redirectUrl`, pass the query parameters to
+  // the AuthorizationCodeGrant. It will validate them and extract the
+  // authorization code to create a new Client.
+  return await grant.handleAuthorizationResponse(responseUrl.queryParameters);
+}
+
+void main() async {
+  var client = await createClient();
+
+  // Once you have a Client, you can use it just like any other HTTP client.
+  print(await client.read('http://example.com/protected-resources.txt'));
+
+  // Once we're done with the client, save the credentials file. This ensures
+  // that if the credentials were automatically refreshed while using the
+  // client, the new credentials are available for the next run of the
+  // program.
+  await credentialsFile.writeAsString(client.credentials.toJson());
+}
+```
+
+<details>
+  <summary>Click here to learn how to implement `redirect` and `listen`.</summary>
+
+--------------------------------------------------------------------------------
+
+There is not a universal example for implementing `redirect` and `listen`,
+because different options exist for each platform.
+
+For Flutter apps, there's two popular approaches:
+
+1.  Launch a browser using [url_launcher][] and listen for a redirect using
+    [uni_links][].
+
+    ```dart
+      if (await canLaunch(authorizationUrl.toString())) {
+        await launch(authorizationUrl.toString()); }
+
+      // ------- 8< -------
+
+      final linksStream = getLinksStream().listen((Uri uri) async {
+       if (uri.toString().startsWith(redirectUrl)) {
+         responseUrl = uri;
+       }
+     });
+    ```
+
+1.  Launch a WebView inside the app and listen for a redirect using
+    [webview_flutter][].
+
+    ```dart
+      WebView(
+        javascriptMode: JavascriptMode.unrestricted,
+        initialUrl: authorizationUrl.toString(),
+        navigationDelegate: (navReq) {
+          if (navReq.url.startsWith(redirectUrl)) {
+            responseUrl = Uri.parse(navReq.url);
+            return NavigationDecision.prevent;
+          }
+          return NavigationDecision.navigate;
+        },
+        // ------- 8< -------
+      );
+    ```
+
+For Dart apps, the best approach depends on the available options for accessing
+a browser. In general, you'll need to launch the authorization URL through the
+client's browser and listen for the redirect URL.
+</details>
+
+## Client Credentials Grant
+
+**Resources:** [Method summary][clientCredentialsGrantMethod],
+[OAuth documentation][clientCredentialsGrantDocs]
+
+```dart
+// This URL is an endpoint that's provided by the authorization server. It's
+// usually included in the server's documentation of its OAuth2 API.
+final authorizationEndpoint =
+    Uri.parse('http://example.com/oauth2/authorization');
+
+// The OAuth2 specification expects a client's identifier and secret
+// to be sent when using the client credentials grant.
+//
+// Because the client credentials grant is not inherently associated with a user,
+// it is up to the server in question whether the returned token allows limited
+// API access.
+//
+// Either way, you must provide both a client identifier and a client secret:
+final identifier = 'my client identifier';
+final secret = 'my client secret';
+
+// Calling the top-level `clientCredentialsGrant` function will return a
+// [Client] instead.
+var client = await oauth2.clientCredentialsGrant(
+    authorizationEndpoint, identifier, secret);
+
+// With an authenticated client, you can make requests, and the `Bearer` token
+// returned by the server during the client credentials grant will be attached
+// to any request you make.
+var response =
+    await client.read('https://example.com/api/some_resource.json');
+
+// You can save the client's credentials, which consists of an access token, and
+// potentially a refresh token and expiry date, to a file. This way, subsequent runs
+// do not need to reauthenticate, and you can avoid saving the client identifier and
+// secret.
+await credentialsFile.writeAsString(client.credentials.toJson());
+```
+
+## Resource Owner Password Grant
+
+**Resources:** [Method summary][resourceOwnerPasswordGrantMethod],
+[OAuth documentation][resourceOwnerPasswordGrantDocs]
+
+```dart
+// This URL is an endpoint that's provided by the authorization server. It's
+// usually included in the server's documentation of its OAuth2 API.
+final authorizationEndpoint =
+    Uri.parse('http://example.com/oauth2/authorization');
+
+// The user should supply their own username and password.
+final username = 'example user';
+final password = 'example password';
+
+// The authorization server may issue each client a separate client
+// identifier and secret, which allows the server to tell which client
+// is accessing it. Some servers may also have an anonymous
+// identifier/secret pair that any client may use.
+//
+// Some servers don't require the client to authenticate itself, in which case
+// these should be omitted.
+final identifier = 'my client identifier';
+final secret = 'my client secret';
+
+// Make a request to the authorization endpoint that will produce the fully
+// authenticated Client.
+var client = await oauth2.resourceOwnerPasswordGrant(
+    authorizationEndpoint, username, password,
+    identifier: identifier, secret: secret);
+
+// Once you have the client, you can use it just like any other HTTP client.
+var result = await client.read('http://example.com/protected-resources.txt');
+
+// Once we're done with the client, save the credentials file. This will allow
+// us to re-use the credentials and avoid storing the username and password
+// directly.
+File('~/.myapp/credentials.json').writeAsString(client.credentials.toJson());
+```
+
+[authorizationCodeGrantDocs]: https://oauth.net/2/grant-types/authorization-code/
+[authorizationCodeGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/AuthorizationCodeGrant-class.html
+[authorizationCodeGrantSection]: #authorization-code-grant
+[clientCredentialsGrantDocs]: https://oauth.net/2/grant-types/client-credentials/
+[clientCredentialsGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/clientCredentialsGrant.html
+[clientCredentialsGrantSection]: #client-credentials-grant
+[resourceOwnerPasswordGrantDocs]: https://oauth.net/2/grant-types/password/
+[resourceOwnerPasswordGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/resourceOwnerPasswordGrant.html
+[resourceOwnerPasswordGrantSection]: #resource-owner-password-grant
+[uni_links]: https://pub.dev/packages/uni_links
+[url_launcher]: https://pub.dev/packages/url_launcher
+[webview_flutter]: https://pub.dev/packages/webview_flutter
diff --git a/lib/src/third_party/oauth2/analysis_options.yaml b/lib/src/third_party/oauth2/analysis_options.yaml
new file mode 100644
index 0000000..c8bc59c
--- /dev/null
+++ b/lib/src/third_party/oauth2/analysis_options.yaml
@@ -0,0 +1,40 @@
+include: package:lints/recommended.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - always_declare_return_types
+    - avoid_catching_errors
+    - avoid_dynamic_calls
+    - avoid_private_typedef_functions
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    - cancel_subscriptions
+    - directives_ordering
+    - literal_only_boolean_expressions
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - omit_local_variable_types
+    - only_throw_errors
+    - package_api_docs
+    - prefer_asserts_in_initializer_lists
+    - prefer_const_constructors
+    - prefer_const_declarations
+    - prefer_relative_imports
+    - prefer_single_quotes
+    - sort_pub_dependencies
+    - test_types_in_equals
+    - throw_in_finally
+    - type_annotate_public_apis
+    - unawaited_futures
+    - unnecessary_await_in_return
+    - unnecessary_lambdas
+    - unnecessary_parenthesis
+    - unnecessary_statements
+    - use_is_even_rather_than_modulo
+    - use_string_buffers
+    - use_super_parameters
diff --git a/lib/src/third_party/oauth2/lib/oauth2.dart b/lib/src/third_party/oauth2/lib/oauth2.dart
new file mode 100644
index 0000000..45efc5c
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/oauth2.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/authorization_code_grant.dart';
+export 'src/authorization_exception.dart';
+export 'src/client.dart';
+export 'src/client_credentials_grant.dart';
+export 'src/credentials.dart';
+export 'src/expiration_exception.dart';
+export 'src/resource_owner_password_grant.dart';
diff --git a/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart b/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart
new file mode 100644
index 0000000..fac56ba
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart
@@ -0,0 +1,371 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
+
+import 'package:crypto/crypto.dart';
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'authorization_exception.dart';
+import 'client.dart';
+import 'credentials.dart';
+import 'handle_access_token_response.dart';
+import 'parameters.dart';
+import 'utils.dart';
+
+/// A class for obtaining credentials via an [authorization code grant][].
+///
+/// This method of authorization involves sending the resource owner to the
+/// authorization server where they will authorize the client. They're then
+/// redirected back to your server, along with an authorization code. This is
+/// used to obtain [Credentials] and create a fully-authorized [Client].
+///
+/// To use this class, you must first call [getAuthorizationUrl] to get the URL
+/// to which to redirect the resource owner. Then once they've been redirected
+/// back to your application, call [handleAuthorizationResponse] or
+/// [handleAuthorizationCode] to process the authorization server's response and
+/// construct a [Client].
+///
+/// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1
+class AuthorizationCodeGrant {
+  /// The function used to parse parameters from a host's response.
+  final GetParameters _getParameters;
+
+  /// The client identifier for this client.
+  ///
+  /// The authorization server will issue each client a separate client
+  /// identifier and secret, which allows the server to tell which client is
+  /// accessing it. Some servers may also have an anonymous identifier/secret
+  /// pair that any client may use.
+  ///
+  /// This is usually global to the program using this library.
+  final String identifier;
+
+  /// The client secret for this client.
+  ///
+  /// The authorization server will issue each client a separate client
+  /// identifier and secret, which allows the server to tell which client is
+  /// accessing it. Some servers may also have an anonymous identifier/secret
+  /// pair that any client may use.
+  ///
+  /// This is usually global to the program using this library.
+  ///
+  /// Note that clients whose source code or binary executable is readily
+  /// available may not be able to make sure the client secret is kept a secret.
+  /// This is fine; OAuth2 servers generally won't rely on knowing with
+  /// certainty that a client is who it claims to be.
+  final String? secret;
+
+  /// A URL provided by the authorization server that serves as the base for the
+  /// URL that the resource owner will be redirected to to authorize this
+  /// client.
+  ///
+  /// This will usually be listed in the authorization server's OAuth2 API
+  /// documentation.
+  final Uri authorizationEndpoint;
+
+  /// A URL provided by the authorization server that this library uses to
+  /// obtain long-lasting credentials.
+  ///
+  /// This will usually be listed in the authorization server's OAuth2 API
+  /// documentation.
+  final Uri tokenEndpoint;
+
+  /// Callback to be invoked whenever the credentials are refreshed.
+  ///
+  /// This will be passed as-is to the constructed [Client].
+  final CredentialsRefreshedCallback? _onCredentialsRefreshed;
+
+  /// Whether to use HTTP Basic authentication for authorizing the client.
+  final bool _basicAuth;
+
+  /// A [String] used to separate scopes; defaults to `" "`.
+  final String _delimiter;
+
+  /// The HTTP client used to make HTTP requests.
+  http.Client? _httpClient;
+
+  /// The URL to which the resource owner will be redirected after they
+  /// authorize this client with the authorization server.
+  Uri? _redirectEndpoint;
+
+  /// The scopes that the client is requesting access to.
+  List<String>? _scopes;
+
+  /// An opaque string that users of this library may specify that will be
+  /// included in the response query parameters.
+  String? _stateString;
+
+  /// The current state of the grant object.
+  _State _state = _State.initial;
+
+  /// Allowed characters for generating the _codeVerifier
+  static const String _charset =
+      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
+
+  /// The PKCE code verifier. Will be generated if one is not provided in the
+  /// constructor.
+  final String _codeVerifier;
+
+  /// Creates a new grant.
+  ///
+  /// If [basicAuth] is `true` (the default), the client credentials are sent to
+  /// the server using using HTTP Basic authentication as defined in [RFC 2617].
+  /// Otherwise, they're included in the request body. Note that the latter form
+  /// is not recommended by the OAuth 2.0 spec, and should only be used if the
+  /// server doesn't support Basic authentication.
+  ///
+  /// [RFC 2617]: https://tools.ietf.org/html/rfc2617
+  ///
+  /// [httpClient] is used for all HTTP requests made by this grant, as well as
+  /// those of the [Client] is constructs.
+  ///
+  /// [onCredentialsRefreshed] will be called by the constructed [Client]
+  /// whenever the credentials are refreshed.
+  ///
+  /// [codeVerifier] String to be used as PKCE code verifier. If none is
+  /// provided a random codeVerifier will be generated.
+  /// The codeVerifier must meet requirements specified in [RFC 7636].
+  ///
+  /// [RFC 7636]: https://tools.ietf.org/html/rfc7636#section-4.1
+  ///
+  /// The scope strings will be separated by the provided [delimiter]. This
+  /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+  /// use non-standard delimiters.
+  ///
+  /// By default, this follows the OAuth2 spec and requires the server's
+  /// responses to be in JSON format. However, some servers return non-standard
+  /// response formats, which can be parsed using the [getParameters] function.
+  ///
+  /// This function is passed the `Content-Type` header of the response as well
+  /// as its body as a UTF-8-decoded string. It should return a map in the same
+  /// format as the [standard JSON response][].
+  ///
+  /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+  AuthorizationCodeGrant(
+      this.identifier, this.authorizationEndpoint, this.tokenEndpoint,
+      {this.secret,
+      String? delimiter,
+      bool basicAuth = true,
+      http.Client? httpClient,
+      CredentialsRefreshedCallback? onCredentialsRefreshed,
+      Map<String, dynamic> Function(MediaType? contentType, String body)?
+          getParameters,
+      String? codeVerifier})
+      : _basicAuth = basicAuth,
+        _httpClient = httpClient ?? http.Client(),
+        _delimiter = delimiter ?? ' ',
+        _getParameters = getParameters ?? parseJsonParameters,
+        _onCredentialsRefreshed = onCredentialsRefreshed,
+        _codeVerifier = codeVerifier ?? _createCodeVerifier();
+
+  /// Returns the URL to which the resource owner should be redirected to
+  /// authorize this client.
+  ///
+  /// The resource owner will then be redirected to [redirect], which should
+  /// point to a server controlled by the client. This redirect will have
+  /// additional query parameters that should be passed to
+  /// [handleAuthorizationResponse].
+  ///
+  /// The specific permissions being requested from the authorization server may
+  /// be specified via [scopes]. The scope strings are specific to the
+  /// authorization server and may be found in its documentation. Note that you
+  /// may not be granted access to every scope you request; you may check the
+  /// [Credentials.scopes] field of [Client.credentials] to see which scopes you
+  /// were granted.
+  ///
+  /// An opaque [state] string may also be passed that will be present in the
+  /// query parameters provided to the redirect URL.
+  ///
+  /// It is a [StateError] to call this more than once.
+  Uri getAuthorizationUrl(Uri redirect,
+      {Iterable<String>? scopes, String? state}) {
+    if (_state != _State.initial) {
+      throw StateError('The authorization URL has already been generated.');
+    }
+    _state = _State.awaitingResponse;
+
+    var scopeList = scopes?.toList() ?? <String>[];
+    var codeChallenge = base64Url
+        .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes)
+        .replaceAll('=', '');
+
+    _redirectEndpoint = redirect;
+    _scopes = scopeList;
+    _stateString = state;
+    var parameters = {
+      'response_type': 'code',
+      'client_id': identifier,
+      'redirect_uri': redirect.toString(),
+      'code_challenge': codeChallenge,
+      'code_challenge_method': 'S256'
+    };
+
+    if (state != null) parameters['state'] = state;
+    if (scopeList.isNotEmpty) parameters['scope'] = scopeList.join(_delimiter);
+
+    return addQueryParameters(authorizationEndpoint, parameters);
+  }
+
+  /// Processes the query parameters added to a redirect from the authorization
+  /// server.
+  ///
+  /// Note that this "response" is not an HTTP response, but rather the data
+  /// passed to a server controlled by the client as query parameters on the
+  /// redirect URL.
+  ///
+  /// It is a [StateError] to call this more than once, to call it before
+  /// [getAuthorizationUrl] is called, or to call it after
+  /// [handleAuthorizationCode] is called.
+  ///
+  /// Throws [FormatException] if [parameters] is invalid according to the
+  /// OAuth2 spec or if the authorization server otherwise provides invalid
+  /// responses. If `state` was passed to [getAuthorizationUrl], this will throw
+  /// a [FormatException] if the `state` parameter doesn't match the original
+  /// value.
+  ///
+  /// Throws [AuthorizationException] if the authorization fails.
+  Future<Client> handleAuthorizationResponse(
+      Map<String, String> parameters) async {
+    if (_state == _State.initial) {
+      throw StateError('The authorization URL has not yet been generated.');
+    } else if (_state == _State.finished) {
+      throw StateError('The authorization code has already been received.');
+    }
+    _state = _State.finished;
+
+    if (_stateString != null) {
+      if (!parameters.containsKey('state')) {
+        throw FormatException('Invalid OAuth response for '
+            '"$authorizationEndpoint": parameter "state" expected to be '
+            '"$_stateString", was missing.');
+      } else if (parameters['state'] != _stateString) {
+        throw FormatException('Invalid OAuth response for '
+            '"$authorizationEndpoint": parameter "state" expected to be '
+            '"$_stateString", was "${parameters['state']}".');
+      }
+    }
+
+    if (parameters.containsKey('error')) {
+      var description = parameters['error_description'];
+      var uriString = parameters['error_uri'];
+      var uri = uriString == null ? null : Uri.parse(uriString);
+      throw AuthorizationException(parameters['error']!, description, uri);
+    } else if (!parameters.containsKey('code')) {
+      throw FormatException('Invalid OAuth response for '
+          '"$authorizationEndpoint": did not contain required parameter '
+          '"code".');
+    }
+
+    return _handleAuthorizationCode(parameters['code']);
+  }
+
+  /// Processes an authorization code directly.
+  ///
+  /// Usually [handleAuthorizationResponse] is preferable to this method, since
+  /// it validates all of the query parameters. However, some authorization
+  /// servers allow the user to copy and paste an authorization code into a
+  /// command-line application, in which case this method must be used.
+  ///
+  /// It is a [StateError] to call this more than once, to call it before
+  /// [getAuthorizationUrl] is called, or to call it after
+  /// [handleAuthorizationCode] is called.
+  ///
+  /// Throws [FormatException] if the authorization server provides invalid
+  /// responses while retrieving credentials.
+  ///
+  /// Throws [AuthorizationException] if the authorization fails.
+  Future<Client> handleAuthorizationCode(String authorizationCode) async {
+    if (_state == _State.initial) {
+      throw StateError('The authorization URL has not yet been generated.');
+    } else if (_state == _State.finished) {
+      throw StateError('The authorization code has already been received.');
+    }
+    _state = _State.finished;
+
+    return _handleAuthorizationCode(authorizationCode);
+  }
+
+  /// This works just like [handleAuthorizationCode], except it doesn't validate
+  /// the state beforehand.
+  Future<Client> _handleAuthorizationCode(String? authorizationCode) async {
+    var startTime = DateTime.now();
+
+    var headers = <String, String>{};
+
+    var body = {
+      'grant_type': 'authorization_code',
+      'code': authorizationCode,
+      'redirect_uri': _redirectEndpoint.toString(),
+      'code_verifier': _codeVerifier
+    };
+
+    var secret = this.secret;
+    if (_basicAuth && secret != null) {
+      headers['Authorization'] = basicAuthHeader(identifier, secret);
+    } else {
+      // The ID is required for this request any time basic auth isn't being
+      // used, even if there's no actual client authentication to be done.
+      body['client_id'] = identifier;
+      if (secret != null) body['client_secret'] = secret;
+    }
+
+    var response =
+        await _httpClient!.post(tokenEndpoint, headers: headers, body: body);
+
+    var credentials = handleAccessTokenResponse(
+        response, tokenEndpoint, startTime, _scopes, _delimiter,
+        getParameters: _getParameters);
+    return Client(credentials,
+        identifier: identifier,
+        secret: secret,
+        basicAuth: _basicAuth,
+        httpClient: _httpClient,
+        onCredentialsRefreshed: _onCredentialsRefreshed);
+  }
+
+  // Randomly generate a 128 character string to be used as the PKCE code
+  // verifier.
+  static String _createCodeVerifier() => List.generate(
+        128,
+        (i) => _charset[Random.secure().nextInt(_charset.length)],
+      ).join();
+
+  /// Closes the grant and frees its resources.
+  ///
+  /// This will close the underlying HTTP client, which is shared by the
+  /// [Client] created by this grant, so it's not safe to close the grant and
+  /// continue using the client.
+  void close() {
+    _httpClient?.close();
+    _httpClient = null;
+  }
+}
+
+/// States that [AuthorizationCodeGrant] can be in.
+class _State {
+  /// [AuthorizationCodeGrant.getAuthorizationUrl] has not yet been called for
+  /// this grant.
+  static const initial = _State('initial');
+
+  // [AuthorizationCodeGrant.getAuthorizationUrl] has been called but neither
+  // [AuthorizationCodeGrant.handleAuthorizationResponse] nor
+  // [AuthorizationCodeGrant.handleAuthorizationCode] has been called.
+  static const awaitingResponse = _State('awaiting response');
+
+  // [AuthorizationCodeGrant.getAuthorizationUrl] and either
+  // [AuthorizationCodeGrant.handleAuthorizationResponse] or
+  // [AuthorizationCodeGrant.handleAuthorizationCode] have been called.
+  static const finished = _State('finished');
+
+  final String _name;
+
+  const _State(this._name);
+
+  @override
+  String toString() => _name;
+}
diff --git a/lib/src/third_party/oauth2/lib/src/authorization_exception.dart b/lib/src/third_party/oauth2/lib/src/authorization_exception.dart
new file mode 100644
index 0000000..14a5a3c
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/authorization_exception.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// An exception raised when OAuth2 authorization fails.
+class AuthorizationException implements Exception {
+  /// The name of the error.
+  ///
+  /// Possible names are enumerated in [the spec][].
+  ///
+  /// [the spec]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.2
+  final String error;
+
+  /// The description of the error, provided by the server.
+  ///
+  /// May be `null` if the server provided no description.
+  final String? description;
+
+  /// A URL for a page that describes the error in more detail, provided by the
+  /// server.
+  ///
+  /// May be `null` if the server provided no URL.
+  final Uri? uri;
+
+  /// Creates an AuthorizationException.
+  AuthorizationException(this.error, this.description, this.uri);
+
+  /// Provides a string description of the AuthorizationException.
+  @override
+  String toString() {
+    var header = 'OAuth authorization error ($error)';
+    if (description != null) {
+      header = '$header: $description';
+    } else if (uri != null) {
+      header = '$header: $uri';
+    }
+    return '$header.';
+  }
+}
diff --git a/lib/src/third_party/oauth2/lib/src/client.dart b/lib/src/third_party/oauth2/lib/src/client.dart
new file mode 100644
index 0000000..1dd2282
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/client.dart
@@ -0,0 +1,187 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:collection/collection.dart';
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'authorization_exception.dart';
+import 'credentials.dart';
+import 'expiration_exception.dart';
+
+/// An OAuth2 client.
+///
+/// This acts as a drop-in replacement for an [http.Client], while sending
+/// OAuth2 authorization credentials along with each request.
+///
+/// The client also automatically refreshes its credentials if possible. When it
+/// makes a request, if its credentials are expired, it will first refresh them.
+/// This means that any request may throw an [AuthorizationException] if the
+/// refresh is not authorized for some reason, a [FormatException] if the
+/// authorization server provides ill-formatted responses, or an
+/// [ExpirationException] if the credentials are expired and can't be refreshed.
+///
+/// The client will also throw an [AuthorizationException] if the resource
+/// server returns a 401 response with a WWW-Authenticate header indicating that
+/// the current credentials are invalid.
+///
+/// If you already have a set of [Credentials], you can construct a [Client]
+/// directly. However, in order to first obtain the credentials, you must
+/// authorize. At the time of writing, the only authorization method this
+/// library supports is [AuthorizationCodeGrant].
+class Client extends http.BaseClient {
+  /// The client identifier for this client.
+  ///
+  /// The authorization server will issue each client a separate client
+  /// identifier and secret, which allows the server to tell which client is
+  /// accessing it. Some servers may also have an anonymous identifier/secret
+  /// pair that any client may use.
+  ///
+  /// This is usually global to the program using this library.
+  final String? identifier;
+
+  /// The client secret for this client.
+  ///
+  /// The authorization server will issue each client a separate client
+  /// identifier and secret, which allows the server to tell which client is
+  /// accessing it. Some servers may also have an anonymous identifier/secret
+  /// pair that any client may use.
+  ///
+  /// This is usually global to the program using this library.
+  ///
+  /// Note that clients whose source code or binary executable is readily
+  /// available may not be able to make sure the client secret is kept a secret.
+  /// This is fine; OAuth2 servers generally won't rely on knowing with
+  /// certainty that a client is who it claims to be.
+  final String? secret;
+
+  /// The credentials this client uses to prove to the resource server that it's
+  /// authorized.
+  ///
+  /// This may change from request to request as the credentials expire and the
+  /// client refreshes them automatically.
+  Credentials get credentials => _credentials;
+  Credentials _credentials;
+
+  /// Callback to be invoked whenever the credentials refreshed.
+  final CredentialsRefreshedCallback? _onCredentialsRefreshed;
+
+  /// Whether to use HTTP Basic authentication for authorizing the client.
+  final bool _basicAuth;
+
+  /// The underlying HTTP client.
+  http.Client? _httpClient;
+
+  /// Creates a new client from a pre-existing set of credentials.
+  ///
+  /// When authorizing a client for the first time, you should use
+  /// [AuthorizationCodeGrant] or [resourceOwnerPasswordGrant] instead of
+  /// constructing a [Client] directly.
+  ///
+  /// [httpClient] is the underlying client that this forwards requests to after
+  /// adding authorization credentials to them.
+  ///
+  /// Throws an [ArgumentError] if [secret] is passed without [identifier].
+  Client(this._credentials,
+      {this.identifier,
+      this.secret,
+      CredentialsRefreshedCallback? onCredentialsRefreshed,
+      bool basicAuth = true,
+      http.Client? httpClient})
+      : _basicAuth = basicAuth,
+        _onCredentialsRefreshed = onCredentialsRefreshed,
+        _httpClient = httpClient ?? http.Client() {
+    if (identifier == null && secret != null) {
+      throw ArgumentError('secret may not be passed without identifier.');
+    }
+  }
+
+  /// Sends an HTTP request with OAuth2 authorization credentials attached.
+  ///
+  /// This will also automatically refresh this client's [Credentials] before
+  /// sending the request if necessary.
+  @override
+  Future<http.StreamedResponse> send(http.BaseRequest request) async {
+    if (credentials.isExpired) {
+      if (!credentials.canRefresh) throw ExpirationException(credentials);
+      await refreshCredentials();
+    }
+
+    request.headers['authorization'] = 'Bearer ${credentials.accessToken}';
+    var response = await _httpClient!.send(request);
+
+    if (response.statusCode != 401) return response;
+    if (!response.headers.containsKey('www-authenticate')) return response;
+
+    List<AuthenticationChallenge> challenges;
+    try {
+      challenges = AuthenticationChallenge.parseHeader(
+          response.headers['www-authenticate']!);
+    } on FormatException {
+      return response;
+    }
+
+    var challenge = challenges
+        .firstWhereOrNull((challenge) => challenge.scheme == 'bearer');
+    if (challenge == null) return response;
+
+    var params = challenge.parameters;
+    if (!params.containsKey('error')) return response;
+
+    throw AuthorizationException(params['error']!, params['error_description'],
+        params['error_uri'] == null ? null : Uri.parse(params['error_uri']!));
+  }
+
+  /// A [Future] used to track whether [refreshCredentials] is running.
+  Future<Credentials>? _refreshingFuture;
+
+  /// Explicitly refreshes this client's credentials. Returns this client.
+  ///
+  /// This will throw a [StateError] if the [Credentials] can't be refreshed, an
+  /// [AuthorizationException] if refreshing the credentials fails, or a
+  /// [FormatException] if the authorization server returns invalid responses.
+  ///
+  /// You may request different scopes than the default by passing in
+  /// [newScopes]. These must be a subset of the scopes in the
+  /// [Credentials.scopes] field of [Client.credentials].
+  Future<Client> refreshCredentials([List<String>? newScopes]) async {
+    if (!credentials.canRefresh) {
+      var prefix = 'OAuth credentials';
+      if (credentials.isExpired) prefix = '$prefix have expired and';
+      throw StateError("$prefix can't be refreshed.");
+    }
+
+    // To make sure that only one refresh happens when credentials are expired
+    // we track it using the [_refreshingFuture]. And also make sure that the
+    // _onCredentialsRefreshed callback is only called once.
+    if (_refreshingFuture == null) {
+      try {
+        _refreshingFuture = credentials.refresh(
+          identifier: identifier,
+          secret: secret,
+          newScopes: newScopes,
+          basicAuth: _basicAuth,
+          httpClient: _httpClient,
+        );
+        _credentials = await _refreshingFuture!;
+        _onCredentialsRefreshed?.call(_credentials);
+      } finally {
+        _refreshingFuture = null;
+      }
+    } else {
+      await _refreshingFuture;
+    }
+
+    return this;
+  }
+
+  /// Closes this client and its underlying HTTP client.
+  @override
+  void close() {
+    _httpClient?.close();
+    _httpClient = null;
+  }
+}
diff --git a/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart b/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart
new file mode 100644
index 0000000..045d1a0
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'client.dart';
+import 'handle_access_token_response.dart';
+import 'utils.dart';
+
+/// Obtains credentials using a [client credentials grant](https://tools.ietf.org/html/rfc6749#section-1.3.4).
+///
+/// This mode of authorization uses the client's [identifier] and [secret]
+/// to obtain an authorization token from the authorization server, instead
+/// of sending a user through a dedicated flow.
+///
+/// The client [identifier] and [secret] are required, and are
+/// used to identify and authenticate your specific OAuth2 client. These are
+/// usually global to the program using this library.
+///
+/// The specific permissions being requested from the authorization server may
+/// be specified via [scopes]. The scope strings are specific to the
+/// authorization server and may be found in its documentation. Note that you
+/// may not be granted access to every scope you request; you may check the
+/// [Credentials.scopes] field of [Client.credentials] to see which scopes you
+/// were granted.
+///
+/// The scope strings will be separated by the provided [delimiter]. This
+/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+/// use non-standard delimiters.
+///
+/// By default, this follows the OAuth2 spec and requires the server's responses
+/// to be in JSON format. However, some servers return non-standard response
+/// formats, which can be parsed using the [getParameters] function.
+///
+/// This function is passed the `Content-Type` header of the response as well as
+/// its body as a UTF-8-decoded string. It should return a map in the same
+/// format as the [standard JSON response](https://tools.ietf.org/html/rfc6749#section-5.1)
+Future<Client> clientCredentialsGrant(
+    Uri authorizationEndpoint, String? identifier, String? secret,
+    {Iterable<String>? scopes,
+    bool basicAuth = true,
+    http.Client? httpClient,
+    String? delimiter,
+    Map<String, dynamic> Function(MediaType? contentType, String body)?
+        getParameters}) async {
+  delimiter ??= ' ';
+  var startTime = DateTime.now();
+
+  var body = {'grant_type': 'client_credentials'};
+
+  var headers = <String, String>{};
+
+  if (identifier != null) {
+    if (basicAuth) {
+      headers['Authorization'] = basicAuthHeader(identifier, secret!);
+    } else {
+      body['client_id'] = identifier;
+      if (secret != null) body['client_secret'] = secret;
+    }
+  }
+
+  if (scopes != null && scopes.isNotEmpty) {
+    body['scope'] = scopes.join(delimiter);
+  }
+
+  httpClient ??= http.Client();
+  var response = await httpClient.post(authorizationEndpoint,
+      headers: headers, body: body);
+
+  var credentials = handleAccessTokenResponse(response, authorizationEndpoint,
+      startTime, scopes?.toList() ?? [], delimiter,
+      getParameters: getParameters);
+  return Client(credentials,
+      identifier: identifier, secret: secret, httpClient: httpClient);
+}
diff --git a/lib/src/third_party/oauth2/lib/src/credentials.dart b/lib/src/third_party/oauth2/lib/src/credentials.dart
new file mode 100644
index 0000000..459e63e
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/credentials.dart
@@ -0,0 +1,267 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'handle_access_token_response.dart';
+import 'parameters.dart';
+import 'utils.dart';
+
+/// Type of the callback when credentials are refreshed.
+typedef CredentialsRefreshedCallback = void Function(Credentials);
+
+/// Credentials that prove that a client is allowed to access a resource on the
+/// resource owner's behalf.
+///
+/// These credentials are long-lasting and can be safely persisted across
+/// multiple runs of the program.
+///
+/// Many authorization servers will attach an expiration date to a set of
+/// credentials, along with a token that can be used to refresh the credentials
+/// once they've expired. The [Client] will automatically refresh its
+/// credentials when necessary. It's also possible to explicitly refresh them
+/// via [Client.refreshCredentials] or [Credentials.refresh].
+///
+/// Note that a given set of credentials can only be refreshed once, so be sure
+/// to save the refreshed credentials for future use.
+class Credentials {
+  /// A [String] used to separate scopes; defaults to `" "`.
+  String _delimiter;
+
+  /// The token that is sent to the resource server to prove the authorization
+  /// of a client.
+  final String accessToken;
+
+  /// The token that is sent to the authorization server to refresh the
+  /// credentials.
+  ///
+  /// This may be `null`, indicating that the credentials can't be refreshed.
+  final String? refreshToken;
+
+  /// The token that is received from the authorization server to enable
+  /// End-Users to be Authenticated, contains Claims, represented as a
+  /// JSON Web Token (JWT).
+  ///
+  /// This may be `null`, indicating that the 'openid' scope was not
+  /// requested (or not supported).
+  ///
+  /// [spec]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
+  final String? idToken;
+
+  /// The URL of the authorization server endpoint that's used to refresh the
+  /// credentials.
+  ///
+  /// This may be `null`, indicating that the credentials can't be refreshed.
+  final Uri? tokenEndpoint;
+
+  /// The specific permissions being requested from the authorization server.
+  ///
+  /// The scope strings are specific to the authorization server and may be
+  /// found in its documentation.
+  final List<String>? scopes;
+
+  /// The date at which these credentials will expire.
+  ///
+  /// This is likely to be a few seconds earlier than the server's idea of the
+  /// expiration date.
+  final DateTime? expiration;
+
+  /// The function used to parse parameters from a host's response.
+  final GetParameters _getParameters;
+
+  /// Whether or not these credentials have expired.
+  ///
+  /// Note that it's possible the credentials will expire shortly after this is
+  /// called. However, since the client's expiration date is kept a few seconds
+  /// earlier than the server's, there should be enough leeway to rely on this.
+  bool get isExpired {
+    var expiration = this.expiration;
+    return expiration != null && DateTime.now().isAfter(expiration);
+  }
+
+  /// Whether it's possible to refresh these credentials.
+  bool get canRefresh => refreshToken != null && tokenEndpoint != null;
+
+  /// Creates a new set of credentials.
+  ///
+  /// This class is usually not constructed directly; rather, it's accessed via
+  /// [Client.credentials] after a [Client] is created by
+  /// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized
+  /// form via [Credentials.fromJson].
+  ///
+  /// The scope strings will be separated by the provided [delimiter]. This
+  /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+  /// use non-standard delimiters.
+  ///
+  /// By default, this follows the OAuth2 spec and requires the server's
+  /// responses to be in JSON format. However, some servers return non-standard
+  /// response formats, which can be parsed using the [getParameters] function.
+  ///
+  /// This function is passed the `Content-Type` header of the response as well
+  /// as its body as a UTF-8-decoded string. It should return a map in the same
+  /// format as the [standard JSON response][].
+  ///
+  /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+  Credentials(this.accessToken,
+      {this.refreshToken,
+      this.idToken,
+      this.tokenEndpoint,
+      Iterable<String>? scopes,
+      this.expiration,
+      String? delimiter,
+      Map<String, dynamic> Function(MediaType? mediaType, String body)?
+          getParameters})
+      : scopes = UnmodifiableListView(
+            // Explicitly type-annotate the list literal to work around
+            // sdk#24202.
+            scopes == null ? <String>[] : scopes.toList()),
+        _delimiter = delimiter ?? ' ',
+        _getParameters = getParameters ?? parseJsonParameters;
+
+  /// Loads a set of credentials from a JSON-serialized form.
+  ///
+  /// Throws a [FormatException] if the JSON is incorrectly formatted.
+  factory Credentials.fromJson(String json) {
+    void validate(bool condition, message) {
+      if (condition) return;
+      throw FormatException('Failed to load credentials: $message.\n\n$json');
+    }
+
+    dynamic parsed;
+    try {
+      parsed = jsonDecode(json);
+    } on FormatException {
+      validate(false, 'invalid JSON');
+    }
+
+    validate(parsed is Map, 'was not a JSON map');
+
+    parsed = parsed as Map;
+    validate(parsed.containsKey('accessToken'),
+        'did not contain required field "accessToken"');
+    validate(
+      parsed['accessToken'] is String,
+      'required field "accessToken" was not a string, was '
+      '${parsed["accessToken"]}',
+    );
+
+    for (var stringField in ['refreshToken', 'idToken', 'tokenEndpoint']) {
+      var value = parsed[stringField];
+      validate(value == null || value is String,
+          'field "$stringField" was not a string, was "$value"');
+    }
+
+    var scopes = parsed['scopes'];
+    validate(scopes == null || scopes is List,
+        'field "scopes" was not a list, was "$scopes"');
+
+    var tokenEndpoint = parsed['tokenEndpoint'];
+    Uri? tokenEndpointUri;
+    if (tokenEndpoint != null) {
+      tokenEndpointUri = Uri.parse(tokenEndpoint as String);
+    }
+
+    var expiration = parsed['expiration'];
+    DateTime? expirationDateTime;
+    if (expiration != null) {
+      validate(expiration is int,
+          'field "expiration" was not an int, was "$expiration"');
+      expiration = expiration as int;
+      expirationDateTime = DateTime.fromMillisecondsSinceEpoch(expiration);
+    }
+
+    return Credentials(
+      parsed['accessToken'] as String,
+      refreshToken: parsed['refreshToken'] as String?,
+      idToken: parsed['idToken'] as String?,
+      tokenEndpoint: tokenEndpointUri,
+      scopes: (scopes as List).map((scope) => scope as String),
+      expiration: expirationDateTime,
+    );
+  }
+
+  /// Serializes a set of credentials to JSON.
+  ///
+  /// Nothing is guaranteed about the output except that it's valid JSON and
+  /// compatible with [Credentials.toJson].
+  String toJson() => jsonEncode({
+        'accessToken': accessToken,
+        'refreshToken': refreshToken,
+        'idToken': idToken,
+        'tokenEndpoint': tokenEndpoint?.toString(),
+        'scopes': scopes,
+        'expiration': expiration?.millisecondsSinceEpoch
+      });
+
+  /// Returns a new set of refreshed credentials.
+  ///
+  /// See [Client.identifier] and [Client.secret] for explanations of those
+  /// parameters.
+  ///
+  /// You may request different scopes than the default by passing in
+  /// [newScopes]. These must be a subset of [scopes].
+  ///
+  /// This throws an [ArgumentError] if [secret] is passed without [identifier],
+  /// a [StateError] if these credentials can't be refreshed, an
+  /// [AuthorizationException] if refreshing the credentials fails, or a
+  /// [FormatException] if the authorization server returns invalid responses.
+  Future<Credentials> refresh(
+      {String? identifier,
+      String? secret,
+      Iterable<String>? newScopes,
+      bool basicAuth = true,
+      http.Client? httpClient}) async {
+    var scopes = this.scopes;
+    if (newScopes != null) scopes = newScopes.toList();
+    scopes ??= [];
+    httpClient ??= http.Client();
+
+    if (identifier == null && secret != null) {
+      throw ArgumentError('secret may not be passed without identifier.');
+    }
+
+    var startTime = DateTime.now();
+    var tokenEndpoint = this.tokenEndpoint;
+    if (refreshToken == null) {
+      throw StateError("Can't refresh credentials without a refresh "
+          'token.');
+    } else if (tokenEndpoint == null) {
+      throw StateError("Can't refresh credentials without a token "
+          'endpoint.');
+    }
+
+    var headers = <String, String>{};
+
+    var body = {'grant_type': 'refresh_token', 'refresh_token': refreshToken};
+    if (scopes.isNotEmpty) body['scope'] = scopes.join(_delimiter);
+
+    if (basicAuth && secret != null) {
+      headers['Authorization'] = basicAuthHeader(identifier!, secret);
+    } else {
+      if (identifier != null) body['client_id'] = identifier;
+      if (secret != null) body['client_secret'] = secret;
+    }
+
+    var response =
+        await httpClient.post(tokenEndpoint, headers: headers, body: body);
+    var credentials = handleAccessTokenResponse(
+        response, tokenEndpoint, startTime, scopes, _delimiter,
+        getParameters: _getParameters);
+
+    // The authorization server may issue a new refresh token. If it doesn't,
+    // we should re-use the one we already have.
+    if (credentials.refreshToken != null) return credentials;
+    return Credentials(credentials.accessToken,
+        refreshToken: refreshToken,
+        idToken: credentials.idToken,
+        tokenEndpoint: credentials.tokenEndpoint,
+        scopes: credentials.scopes,
+        expiration: credentials.expiration);
+  }
+}
diff --git a/lib/src/third_party/oauth2/lib/src/expiration_exception.dart b/lib/src/third_party/oauth2/lib/src/expiration_exception.dart
new file mode 100644
index 0000000..d72fcf6
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/expiration_exception.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'credentials.dart';
+
+/// An exception raised when attempting to use expired OAuth2 credentials.
+class ExpirationException implements Exception {
+  /// The expired credentials.
+  final Credentials credentials;
+
+  /// Creates an ExpirationException.
+  ExpirationException(this.credentials);
+
+  /// Provides a string description of the ExpirationException.
+  @override
+  String toString() =>
+      "OAuth2 credentials have expired and can't be refreshed.";
+}
diff --git a/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart b/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart
new file mode 100644
index 0000000..931ae9d
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart
@@ -0,0 +1,156 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'authorization_exception.dart';
+import 'credentials.dart';
+import 'parameters.dart';
+
+/// The amount of time to add as a "grace period" for credential expiration.
+///
+/// This allows credential expiration checks to remain valid for a reasonable
+/// amount of time.
+const _expirationGrace = Duration(seconds: 10);
+
+/// Handles a response from the authorization server that contains an access
+/// token.
+///
+/// This response format is common across several different components of the
+/// OAuth2 flow.
+///
+/// By default, this follows the OAuth2 spec and requires the server's responses
+/// to be in JSON format. However, some servers return non-standard response
+/// formats, which can be parsed using the [getParameters] function.
+///
+/// This function is passed the `Content-Type` header of the response as well as
+/// its body as a UTF-8-decoded string. It should return a map in the same
+/// format as the [standard JSON response][].
+///
+/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+Credentials handleAccessTokenResponse(http.Response response, Uri tokenEndpoint,
+    DateTime startTime, List<String>? scopes, String delimiter,
+    {Map<String, dynamic> Function(MediaType? contentType, String body)?
+        getParameters}) {
+  getParameters ??= parseJsonParameters;
+
+  try {
+    if (response.statusCode != 200) {
+      _handleErrorResponse(response, tokenEndpoint, getParameters);
+    }
+
+    var contentTypeString = response.headers['content-type'];
+    if (contentTypeString == null) {
+      throw const FormatException('Missing Content-Type string.');
+    }
+
+    var parameters =
+        getParameters(MediaType.parse(contentTypeString), response.body);
+
+    for (var requiredParameter in ['access_token', 'token_type']) {
+      if (!parameters.containsKey(requiredParameter)) {
+        throw FormatException(
+            'did not contain required parameter "$requiredParameter"');
+      } else if (parameters[requiredParameter] is! String) {
+        throw FormatException(
+            'required parameter "$requiredParameter" was not a string, was '
+            '"${parameters[requiredParameter]}"');
+      }
+    }
+
+    // TODO(nweiz): support the "mac" token type
+    // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01)
+    if ((parameters['token_type'] as String).toLowerCase() != 'bearer') {
+      throw FormatException(
+          '"$tokenEndpoint": unknown token type "${parameters['token_type']}"');
+    }
+
+    var expiresIn = parameters['expires_in'];
+    if (expiresIn != null) {
+      if (expiresIn is String) {
+        try {
+          expiresIn = double.parse(expiresIn).toInt();
+        } on FormatException {
+          throw FormatException(
+              'parameter "expires_in" could not be parsed as in, was: "$expiresIn"');
+        }
+      } else if (expiresIn is! int) {
+        throw FormatException(
+            'parameter "expires_in" was not an int, was: "$expiresIn"');
+      }
+    }
+
+    for (var name in ['refresh_token', 'id_token', 'scope']) {
+      var value = parameters[name];
+      if (value != null && value is! String) {
+        throw FormatException(
+            'parameter "$name" was not a string, was "$value"');
+      }
+    }
+
+    var scope = parameters['scope'] as String?;
+    if (scope != null) scopes = scope.split(delimiter);
+
+    var expiration = expiresIn == null
+        ? null
+        : startTime.add(Duration(seconds: expiresIn as int) - _expirationGrace);
+
+    return Credentials(
+      parameters['access_token'] as String,
+      refreshToken: parameters['refresh_token'] as String?,
+      idToken: parameters['id_token'] as String?,
+      tokenEndpoint: tokenEndpoint,
+      scopes: scopes,
+      expiration: expiration,
+    );
+  } on FormatException catch (e) {
+    throw FormatException('Invalid OAuth response for "$tokenEndpoint": '
+        '${e.message}.\n\n${response.body}');
+  }
+}
+
+/// Throws the appropriate exception for an error response from the
+/// authorization server.
+void _handleErrorResponse(
+    http.Response response, Uri tokenEndpoint, GetParameters getParameters) {
+  // OAuth2 mandates a 400 or 401 response code for access token error
+  // responses. If it's not a 400 reponse, the server is either broken or
+  // off-spec.
+  if (response.statusCode != 400 && response.statusCode != 401) {
+    var reason = '';
+    var reasonPhrase = response.reasonPhrase;
+    if (reasonPhrase != null && reasonPhrase.isNotEmpty) {
+      reason = ' $reasonPhrase';
+    }
+    throw FormatException('OAuth request for "$tokenEndpoint" failed '
+        'with status ${response.statusCode}$reason.\n\n${response.body}');
+  }
+
+  var contentTypeString = response.headers['content-type'];
+  var contentType =
+      contentTypeString == null ? null : MediaType.parse(contentTypeString);
+
+  var parameters = getParameters(contentType, response.body);
+
+  if (!parameters.containsKey('error')) {
+    throw const FormatException('did not contain required parameter "error"');
+  } else if (parameters['error'] is! String) {
+    throw FormatException('required parameter "error" was not a string, was '
+        '"${parameters["error"]}"');
+  }
+
+  for (var name in ['error_description', 'error_uri']) {
+    var value = parameters[name];
+
+    if (value != null && value is! String) {
+      throw FormatException('parameter "$name" was not a string, was "$value"');
+    }
+  }
+
+  var uriString = parameters['error_uri'] as String?;
+  var uri = uriString == null ? null : Uri.parse(uriString);
+  var description = parameters['error_description'] as String?;
+  throw AuthorizationException(parameters['error'] as String, description, uri);
+}
diff --git a/lib/src/third_party/oauth2/lib/src/parameters.dart b/lib/src/third_party/oauth2/lib/src/parameters.dart
new file mode 100644
index 0000000..ecc6559
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/parameters.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:http_parser/http_parser.dart';
+
+/// The type of a callback that parses parameters from an HTTP response.
+typedef GetParameters = Map<String, dynamic> Function(
+    MediaType? contentType, String body);
+
+/// Parses parameters from a response with a JSON body, as per the
+/// [OAuth2 spec][].
+///
+/// [OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-5.1
+Map<String, dynamic> parseJsonParameters(MediaType? contentType, String body) {
+  // The spec requires a content-type of application/json, but some endpoints
+  // (e.g. Dropbox) serve it as text/javascript instead.
+  if (contentType == null ||
+      (contentType.mimeType != 'application/json' &&
+          contentType.mimeType != 'text/javascript')) {
+    throw FormatException(
+        'Content-Type was "$contentType", expected "application/json"');
+  }
+
+  var untypedParameters = jsonDecode(body);
+  if (untypedParameters is Map<String, dynamic>) {
+    return untypedParameters;
+  }
+
+  throw FormatException('Parameters must be a map, was "$untypedParameters"');
+}
diff --git a/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart b/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart
new file mode 100644
index 0000000..96fb503
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'client.dart';
+import 'credentials.dart';
+import 'handle_access_token_response.dart';
+import 'utils.dart';
+
+/// Obtains credentials using a [resource owner password grant](https://tools.ietf.org/html/rfc6749#section-1.3.3).
+///
+/// This mode of authorization uses the user's username and password to obtain
+/// an authentication token, which can then be stored. This is safer than
+/// storing the username and password directly, but it should be avoided if any
+/// other authorization method is available, since it requires the user to
+/// provide their username and password to a third party (you).
+///
+/// The client [identifier] and [secret] may be issued by the server, and are
+/// used to identify and authenticate your specific OAuth2 client. These are
+/// usually global to the program using this library.
+///
+/// The specific permissions being requested from the authorization server may
+/// be specified via [scopes]. The scope strings are specific to the
+/// authorization server and may be found in its documentation. Note that you
+/// may not be granted access to every scope you request; you may check the
+/// [Credentials.scopes] field of [Client.credentials] to see which scopes you
+/// were granted.
+///
+/// The scope strings will be separated by the provided [delimiter]. This
+/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+/// use non-standard delimiters.
+///
+/// By default, this follows the OAuth2 spec and requires the server's responses
+/// to be in JSON format. However, some servers return non-standard response
+/// formats, which can be parsed using the [getParameters] function.
+///
+/// This function is passed the `Content-Type` header of the response as well as
+/// its body as a UTF-8-decoded string. It should return a map in the same
+/// format as the [standard JSON response][].
+///
+/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+Future<Client> resourceOwnerPasswordGrant(
+    Uri authorizationEndpoint, String username, String password,
+    {String? identifier,
+    String? secret,
+    Iterable<String>? scopes,
+    bool basicAuth = true,
+    CredentialsRefreshedCallback? onCredentialsRefreshed,
+    http.Client? httpClient,
+    String? delimiter,
+    Map<String, dynamic> Function(MediaType? contentType, String body)?
+        getParameters}) async {
+  delimiter ??= ' ';
+  var startTime = DateTime.now();
+
+  var body = {
+    'grant_type': 'password',
+    'username': username,
+    'password': password
+  };
+
+  var headers = <String, String>{};
+
+  if (identifier != null) {
+    if (basicAuth) {
+      headers['Authorization'] = basicAuthHeader(identifier, secret!);
+    } else {
+      body['client_id'] = identifier;
+      if (secret != null) body['client_secret'] = secret;
+    }
+  }
+
+  if (scopes != null && scopes.isNotEmpty) {
+    body['scope'] = scopes.join(delimiter);
+  }
+
+  httpClient ??= http.Client();
+  var response = await httpClient.post(authorizationEndpoint,
+      headers: headers, body: body);
+
+  var credentials = handleAccessTokenResponse(
+      response, authorizationEndpoint, startTime, scopes?.toList(), delimiter,
+      getParameters: getParameters);
+  return Client(credentials,
+      identifier: identifier,
+      secret: secret,
+      httpClient: httpClient,
+      onCredentialsRefreshed: onCredentialsRefreshed);
+}
diff --git a/lib/src/third_party/oauth2/lib/src/utils.dart b/lib/src/third_party/oauth2/lib/src/utils.dart
new file mode 100644
index 0000000..2a22b9f
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/utils.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+/// Adds additional query parameters to [url], overwriting the original
+/// parameters if a name conflict occurs.
+Uri addQueryParameters(Uri url, Map<String, String> parameters) => url.replace(
+    queryParameters: Map.from(url.queryParameters)..addAll(parameters));
+
+String basicAuthHeader(String identifier, String secret) {
+  var userPass = '${Uri.encodeFull(identifier)}:${Uri.encodeFull(secret)}';
+  return 'Basic ${base64Encode(ascii.encode(userPass))}';
+}
diff --git a/lib/src/third_party/oauth2/vendored-pubspec.yaml b/lib/src/third_party/oauth2/vendored-pubspec.yaml
new file mode 100644
index 0000000..bb43bb7
--- /dev/null
+++ b/lib/src/third_party/oauth2/vendored-pubspec.yaml
@@ -0,0 +1,20 @@
+name: oauth2
+version: 2.0.1
+description: >-
+  A client library for authenticating with a remote service via OAuth2 on
+  behalf of a user, and making authorized HTTP requests with the user's
+  OAuth2 credentials.
+repository: https://github.com/dart-lang/oauth2
+  
+environment:
+  sdk: '>=2.17.0 <3.0.0'
+
+dependencies:
+  collection: ^1.15.0
+  crypto: ^3.0.0
+  http: ^0.13.0
+  http_parser: ^4.0.0
+
+dev_dependencies:
+  lints: ^2.0.0
+  test: ^1.16.0
diff --git a/lib/src/third_party/tar/CHANGELOG.md b/lib/src/third_party/tar/CHANGELOG.md
new file mode 100644
index 0000000..a804552
--- /dev/null
+++ b/lib/src/third_party/tar/CHANGELOG.md
@@ -0,0 +1,86 @@
+## 0.5.6
+
+- Allow cancelling a `TarEntry.contents` subscription before reading more files.
+
+## 0.5.5+1
+
+- No user-visible changes.
+
+## 0.5.5
+
+- Fix a crash when pausing a subscription to `TarEntry.contents` right before
+  it ends.
+
+## 0.5.4
+
+- Fix generating corrupt tar files when adding lots of entries at very high
+  speeds [(#20)](https://github.com/simolus3/tar/issues/20).
+- Allow tar files with invalid utf8 content in PAX header values if those
+  values aren't used for anything important.
+
+## 0.5.3
+
+- Improve error messages when reading a tar entry after, or during, a call to
+  `moveNext()`.
+
+## 0.5.2
+
+- This package now supports being compiled to JavaScript.
+
+## 0.5.1
+
+- Improve performance when reading large archives
+
+## 0.5.0
+
+- Support sync encoding with `tarConverter`.
+
+## 0.4.0
+
+- Support generating tar files with GNU-style long link names
+ - Add `format` parameter to `tarWritingSink` and `tarWriterWith`
+
+## 0.3.3
+
+- Drop `chunked_stream` dependency in favor of `package:async`.
+
+## 0.3.2
+
+- Allow arbitrarily many zero bytes at the end of an archive when
+  `disallowTrailingData` is enabled.
+
+## 0.3.1
+
+- Add `disallowTrailingData` parameter to `TarReader`. When the option is set,
+  `readNext` will ensure that the input stream does not emit further data after
+  the tar archive has been read fully.
+
+## 0.3.0
+
+- Remove outdated references in the documentation
+
+## 0.3.0-nullsafety.0
+
+- Remove `TarReader.contents` and `TarReader.header`. Use `current.contents` and `current.header`, respectively.
+- Fix some minor implementation details
+
+## 0.2.0-nullsafety
+
+Most of the tar package has been rewritten, it's now based on the
+implementation written by [Garett Tok Ern Liang](https://github.com/walnutdust)
+in the GSoC 2020.
+
+- Added `tar` prefix to exported symbols.
+- Remove `MemoryEntry`. Use `TarEntry.data` to create a tar entry from bytes.
+- Make `WritingSink` private. Use `tarWritingSink` to create a general `StreamSink<tar.Entry>`.
+- `TarReader` is now a [`StreamIterator`](https://api.dart.dev/stable/2.10.4/dart-async/StreamIterator-class.html),
+  the transformer had some design flaws.
+
+## 0.1.0-nullsafety.1
+
+- Support writing user and group names
+- Better support for PAX-headers and large files
+
+## 0.1.0-nullsafety.0
+
+- Initial version
diff --git a/lib/src/third_party/tar/README.md b/lib/src/third_party/tar/README.md
index 5e12e5a..8d5a334 100644
--- a/lib/src/third_party/tar/README.md
+++ b/lib/src/third_party/tar/README.md
@@ -1,7 +1,214 @@
-# package:tar
+# tar
 
-Vendored elements from `package:tar` for use in creation and extraction of
-tar-archives.
+![Build status](https://github.com/simolus3/tar/workflows/build/badge.svg)
 
- * Repository: `https://github.com/simolus3/tar/`
- * Revision: `23ee71d667f003fba8c80ee126d5e1330d17c141`
+This package provides stream-based readers and writers for tar files.
+
+When working with large tar files, this library consumes considerably less memory
+than [package:archive](https://pub.dev/packages/archive), although it is slightly slower due to the async overhead.
+
+## Reading
+
+To read entries from a tar file, use a `TarReader` with a `Stream` emitting bytes (as `List<int>`):
+
+```dart
+import 'dart:convert';
+import 'dart:io';
+import 'package:tar/tar.dart';
+
+Future<void> main() async {
+  final reader = TarReader(File('file.tar').openRead());
+
+  while (await reader.moveNext()) {
+    final entry = reader.current;
+    // Use reader.header to see the header of the current tar entry
+    print(entry.header.name);
+    // And reader.contents to read the content of the current entry as a stream
+    print(await entry.contents.transform(utf8.decoder).first);
+  }
+  // Note that the reader will automatically close if moveNext() returns false or
+  // throws. If you want to close a tar stream before that happens, use
+  // reader.cancel();
+}
+```
+
+To read `.tar.gz` files, transform the stream with `gzip.decoder` before
+passing it to the `TarReader`.
+
+To easily go through all entries in a tar file, use `TarReader.forEach`:
+
+```dart
+Future<void> main() async {
+  final inputStream = File('file.tar').openRead();
+
+  await TarReader.forEach(inputStream, (entry) {
+    print(header.name);
+    print(await entry.contents.transform(utf8.decoder).first);
+  });
+}
+```
+
+__Warning__: Since the reader is backed by a single stream, concurrent calls to
+`read` are not allowed! Similarly, if you're reading from an entry's `contents`,
+make sure to fully drain the stream before calling `read()` again.
+_Not_ subscribing to `contents` before calling `moveNext()` is acceptable too.
+In this case, the reader will implicitly drain the stream.
+The reader detects concurrency misuses and will throw an error when they occur,
+there's no risk of reading faulty data.
+
+## Writing
+
+When writing archives, `package:tar` expects a `Stream` of tar entries to include in
+the archive.
+This stream can then be converted into a stream of byte-array chunks forming the
+encoded tar archive.
+
+To write a tar stream into a `StreamSink<List<int>>`, such as an `IOSink` returned by
+`File.openWrite`, use `tarWritingSink`:
+
+```dart
+import 'dart:convert';
+import 'dart:io';
+import 'package:tar/tar.dart';
+
+Future<void> main() async {
+  final output = File('test.tar').openWrite();
+  final tarEntries = Stream<TarEntry>.value(
+    TarEntry.data(
+      TarHeader(
+        name: 'hello.txt',
+        mode: int.parse('644', radix: 8),
+      ),
+      utf8.encode('Hello world'),
+    ),
+  );
+
+  await tarEntries.pipe(tarWritingSink(output));
+}
+```
+
+For more complex stream transformations, `tarWriter` can be used as a stream
+transformer converting a stream of tar entries into archive bytes.
+
+Together with the `gzip.encoder` transformer from `dart:io`, this can be used
+to write a `.tar.gz` file:
+
+```dart
+import 'dart:io';
+import 'package:tar/tar.dart';
+
+Future<void> write(Stream<TarEntry> entries) {
+  return entries
+      .transform(tarWriter) // convert entries into a .tar stream
+      .transform(gzip.encoder) // convert the .tar stream into a .tar.gz stream
+      .pipe(File('output.tar.gz').openWrite());
+}
+```
+
+A more complex example for writing files can be found in [`example/archive_self.dart`](example/archive_self.dart).
+
+### Encoding options
+
+By default, tar files are  written in the pax format defined by the
+POSIX.1-2001 specification (`--format=posix` in GNU tar).
+When all entries have file names shorter than 100 chars and a size smaller
+than 8 GB, this is equivalent to the `ustar` format. This library won't write
+PAX headers when there is no reason to do so.
+If you prefer writing GNU-style long filenames instead, you can use the
+`format` option:
+
+```dart
+Future<void> write(Stream<TarEntry> entries) {
+  return entries
+      .pipe(
+        tarWritingSink(
+          File('output.tar').openWrite(),
+          format: OutputFormat.gnuLongName,
+      ));
+}
+```
+
+To change the output format on the `tarWriter` transformer, use
+`tarWriterWith`.
+
+### Synchronous writing
+
+As the content of tar entries is defined as an asynchronous stream, the tar encoder is asynchronous too.
+The more specific `SynchronousTarEntry` class stores tar content as a list of bytes, meaning that it can be
+written synchronously.
+
+To synchronously write tar files, use `tarConverter` (or `tarConverterWith` for options):
+
+```dart
+List<int> createTarArchive(Iterable<SynchronousTarEntry> entries) {
+  late List<int> result;
+  final sink = ByteConversionSink.withCallback((data) => result = data);
+
+  final output = tarConverter.startChunkedConversion(sink);
+  entries.forEach(output.add);
+  output.close();
+
+  return result;
+}
+```
+
+## Features
+
+- Supports v7, ustar, pax, gnu and star archives
+- Supports extended pax headers for long file or link names
+- Supports long file and link names generated by GNU-tar
+- Hardened against denial-of-service attacks with invalid tar files
+- Supports being compiled to JavaScript, tested on Node.js
+
+## Security considerations
+
+Internally, this package contains checks to guard against some invalid tar files.
+In particular,
+
+- The reader doesn't allocate memory based on values in a tar file (so there's
+  a guard against DoS attacks with tar files containing huge headers).
+- When encountering malformed tar files, the reader will throw a `TarException`.
+  Any other exception thrown indicates a bug in `package:tar` or how it's used.
+  The reader should never crash.
+- Reading a tar file can be cancelled mid-stream without leaking resources.
+
+However, the tar reader __does not__ throw exceptions for wellformed archives
+with suspicious contents, such as
+
+- File names beginning with `../`, `/` or names pointing out of the archive by
+  other means.
+- Link references to files outside of the archive.
+- Paths not using forward slashes.
+- Gzip + tar bombs.
+- Invalid permission bits in entries.
+- ...
+
+When reading or extracting untrusted tar files, it is your responsibility to
+detect and handle these cases.
+For instance, this naive extraction function is susceptible to invalid tar
+files containing paths outside of the target directory:
+
+```dart
+Future<void> extractTarGz(File tarGz, Directory target) async {
+  final input = tarGz.openRead().transform(gzip.decoder);
+
+  await TarReader.forEach(input, (entry) async {
+    final destination =
+        // DON'T DO THIS! If `entry.name` contained `../`, this may escape the
+        // target directory.
+        path.joinAll([target.path, ...path.posix.split(entry.name)]);
+
+    final f = File(destination);
+    await f.create(recursive: true);
+    await entry.contents.pipe(f.openWrite());
+  });
+}
+```
+
+For an idea on how to guard against this, see the [extraction logic](https://github.com/dart-lang/pub/blob/3082796f8ba9b3f509265ac3a223312fb5033988/lib/src/io.dart#L904-L991)
+used by the pub client.
+
+-----
+
+Big thanks to [Garett Tok Ern Liang](https://github.com/walnutdust) for writing the initial
+Dart tar reader that this library is based on.
diff --git a/lib/src/third_party/tar/analysis_options.yaml b/lib/src/third_party/tar/analysis_options.yaml
new file mode 100644
index 0000000..8b18047
--- /dev/null
+++ b/lib/src/third_party/tar/analysis_options.yaml
@@ -0,0 +1,20 @@
+include: package:extra_pedantic/analysis_options.2.0.0.yaml
+
+analyzer:
+  strong-mode:
+    implicit-casts: false
+    implicit-dynamic: false
+  language:
+    strict-inference: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    close_sinks: false # This rule has just too many false-positives...
+    comment_references: true
+    literal_only_boolean_expressions: false # Nothing wrong with a little while(true)
+    parameter_assignments: false
+    unnecessary_await_in_return: false
+    no_default_cases: false
+    prefer_asserts_with_message: false # We only use asserts for library-internal invariants
+    prefer_final_parameters: false # Too much noise
diff --git a/lib/src/third_party/tar/src/charcodes.dart b/lib/src/third_party/tar/lib/src/charcodes.dart
similarity index 100%
rename from lib/src/third_party/tar/src/charcodes.dart
rename to lib/src/third_party/tar/lib/src/charcodes.dart
diff --git a/lib/src/third_party/tar/src/constants.dart b/lib/src/third_party/tar/lib/src/constants.dart
similarity index 100%
rename from lib/src/third_party/tar/src/constants.dart
rename to lib/src/third_party/tar/lib/src/constants.dart
diff --git a/lib/src/third_party/tar/src/entry.dart b/lib/src/third_party/tar/lib/src/entry.dart
similarity index 100%
rename from lib/src/third_party/tar/src/entry.dart
rename to lib/src/third_party/tar/lib/src/entry.dart
diff --git a/lib/src/third_party/tar/src/exception.dart b/lib/src/third_party/tar/lib/src/exception.dart
similarity index 100%
rename from lib/src/third_party/tar/src/exception.dart
rename to lib/src/third_party/tar/lib/src/exception.dart
diff --git a/lib/src/third_party/tar/src/format.dart b/lib/src/third_party/tar/lib/src/format.dart
similarity index 100%
rename from lib/src/third_party/tar/src/format.dart
rename to lib/src/third_party/tar/lib/src/format.dart
diff --git a/lib/src/third_party/tar/src/header.dart b/lib/src/third_party/tar/lib/src/header.dart
similarity index 100%
rename from lib/src/third_party/tar/src/header.dart
rename to lib/src/third_party/tar/lib/src/header.dart
diff --git a/lib/src/third_party/tar/src/reader.dart b/lib/src/third_party/tar/lib/src/reader.dart
similarity index 100%
rename from lib/src/third_party/tar/src/reader.dart
rename to lib/src/third_party/tar/lib/src/reader.dart
diff --git a/lib/src/third_party/tar/src/sparse.dart b/lib/src/third_party/tar/lib/src/sparse.dart
similarity index 100%
rename from lib/src/third_party/tar/src/sparse.dart
rename to lib/src/third_party/tar/lib/src/sparse.dart
diff --git a/lib/src/third_party/tar/src/utils.dart b/lib/src/third_party/tar/lib/src/utils.dart
similarity index 100%
rename from lib/src/third_party/tar/src/utils.dart
rename to lib/src/third_party/tar/lib/src/utils.dart
diff --git a/lib/src/third_party/tar/src/writer.dart b/lib/src/third_party/tar/lib/src/writer.dart
similarity index 100%
rename from lib/src/third_party/tar/src/writer.dart
rename to lib/src/third_party/tar/lib/src/writer.dart
diff --git a/lib/src/third_party/tar/tar.dart b/lib/src/third_party/tar/lib/tar.dart
similarity index 100%
rename from lib/src/third_party/tar/tar.dart
rename to lib/src/third_party/tar/lib/tar.dart
diff --git a/lib/src/third_party/tar/vendored-pubspec.yaml b/lib/src/third_party/tar/vendored-pubspec.yaml
new file mode 100644
index 0000000..0556ca8
--- /dev/null
+++ b/lib/src/third_party/tar/vendored-pubspec.yaml
@@ -0,0 +1,24 @@
+name: tar
+description: Memory-efficient, streaming implementation of the tar file format
+version: 0.5.6
+repository: https://github.com/simolus3/tar/
+
+environment:
+  sdk: '>=2.12.0 <3.0.0'
+
+dependencies:
+  async: ^2.6.0
+  meta: ^1.3.0
+  typed_data: ^1.3.0
+
+dev_dependencies:
+  charcode: ^1.2.0
+  extra_pedantic: ^3.0.0
+  file: ^6.1.2
+  node_io: ^2.1.0
+  path: ^1.8.0
+  test: ^1.20.0
+
+dependency_overrides:
+  # Waiting for https://github.com/pulyaevskiy/node-interop/issues/110
+  file: '>=6.1.0 <6.1.3'
diff --git a/lib/src/third_party/vendor-state.yaml b/lib/src/third_party/vendor-state.yaml
new file mode 100644
index 0000000..09c5eeb
--- /dev/null
+++ b/lib/src/third_party/vendor-state.yaml
@@ -0,0 +1,29 @@
+# DO NOT EDIT: This file is generated by package:vendor version 0.9.0
+version: 0.9.0
+config:
+  import_rewrites:
+    oauth2: oauth2
+    tar: tar
+  vendored_dependencies:
+    oauth2:
+      package: oauth2
+      version: 2.0.1
+      import_rewrites: {}
+      include:
+        - pubspec.yaml
+        - README.md
+        - LICENSE
+        - CHANGELOG.md
+        - lib/**
+        - analysis_options.yaml
+    tar:
+      package: tar
+      version: 0.5.6
+      import_rewrites: {}
+      include:
+        - pubspec.yaml
+        - README.md
+        - LICENSE
+        - CHANGELOG.md
+        - lib/**
+        - analysis_options.yaml
diff --git a/pubspec.yaml b/pubspec.yaml
index 00c9829..cc655dc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -18,14 +18,15 @@
   http_multi_server: ^3.0.1
   http_parser: ^4.0.1
   meta: ^1.3.0
-  oauth2: ^2.0.0
   path: ^1.8.0
   pool: ^1.5.0
   pub_semver: ^2.1.0
   shelf: ^1.1.1
   source_span: ^1.8.1
   stack_trace: ^1.10.0
+  typed_data: ^1.3.1
   usage: ^4.0.2
+  vendor: ^0.9.2
   yaml: ^3.1.0
   yaml_edit: ^2.0.0
 
@@ -35,3 +36,4 @@
   test: ^1.21.5
   test_descriptor: ^2.0.0
   test_process: ^2.0.0
+
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 6deecd7..ab3a7ec 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -5,10 +5,10 @@
 /// Pub-specific test descriptors.
 import 'dart:convert';
 
-import 'package:oauth2/oauth2.dart' as oauth2;
 import 'package:path/path.dart' as p;
 import 'package:pub/src/language_version.dart';
 import 'package:pub/src/package_config.dart';
+import 'package:pub/src/third_party/oauth2/lib/oauth2.dart' as oauth2;
 import 'package:test_descriptor/test_descriptor.dart';
 
 import 'descriptor/git.dart';
diff --git a/test/io_test.dart b/test/io_test.dart
index 0657dd6..80bd1be 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -9,7 +9,7 @@
 import 'package:path/path.dart' as path;
 import 'package:pub/src/exceptions.dart';
 import 'package:pub/src/io.dart';
-import 'package:pub/src/third_party/tar/tar.dart';
+import 'package:pub/src/third_party/tar/lib/tar.dart';
 import 'package:test/test.dart';
 
 import 'descriptor.dart' as d;
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 6a2e67b..a932b6b 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -27,7 +27,7 @@
 import 'package:pub/src/package_name.dart';
 import 'package:pub/src/source/hosted.dart';
 import 'package:pub/src/system_cache.dart';
-import 'package:pub/src/third_party/tar/tar.dart';
+import 'package:pub/src/third_party/tar/lib/tar.dart';
 import 'package:pub/src/utils.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub_semver/pub_semver.dart';
diff --git a/vendor.yaml b/vendor.yaml
new file mode 100644
index 0000000..27db379
--- /dev/null
+++ b/vendor.yaml
@@ -0,0 +1,10 @@
+import_rewrites:
+  oauth2: oauth2
+  tar: tar
+vendored_dependencies:
+  oauth2:
+    package: oauth2
+    version: 2.0.1
+  tar:
+    package: tar
+    version: 0.5.6