- make sure that we can generate offline tokens for Epic providers (using dynamic client registration - https://fhir.epic.com/Documentation?docId=Oauth2&section=Standalone-Oauth2-OfflineAccess-0)

- when SourceCredential is sent to the CreateSource API, we'll check if dynamic client regirstion is enabled. If it is, we'll use this token to register a new client, store the client information, and geneate a new Access Token using the key pair associated with this newly registered client.
- added additional fields to SourceCredential table (registration_endpoint, dynamic_client_registration_mode, dynamic_client_jwks, dynamic_client_id)
- renamed RefreshTokens to SetTokens in SourceCredential model - to have consistent naming
- added IsDynamicClient and RefreshDynamicClientAccessToken methods to SourceCredential Model

fixes #178
This commit is contained in:
Jason Kulatunga 2023-07-19 22:45:14 -07:00
parent be30cb046f
commit a5b37159c1
No known key found for this signature in database
12 changed files with 445 additions and 12 deletions

View File

@ -0,0 +1,70 @@
package jwk
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwk"
)
//see https://github.com/lestrrat-go/jwx/blob/v2/docs/04-jwk.md#working-with-key-specific-methods
func JWKGenerate() (jwk.RSAPrivateKey, error) {
raw, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate RSA private key: %s\n", err)
}
key, err := jwk.FromRaw(raw)
if err != nil {
return nil, fmt.Errorf("failed to create jwk.Key from RSA private key: %s\n", err)
}
rsakey, ok := key.(jwk.RSAPrivateKey)
if !ok {
return nil, fmt.Errorf("failed to convert jwk.Key into jwk.RSAPrivateKey (was %T)\n", key)
}
rsakey.Set("kid", uuid.New().String())
//_ = rsakey.D()
//_ = rsakey.DP()
//_ = rsakey.DQ()
//_ = rsakey.E()
//_ = rsakey.N()
//_ = rsakey.P()
//_ = rsakey.Q()
//_ = rsakey.QI()
//// OUTPUT:
return rsakey, nil
}
func JWKSerialize(privateKey jwk.RSAPrivateKey) (map[string]string, error) {
jsonbuf, err := json.Marshal(privateKey)
if err != nil {
return nil, err
}
var dict map[string]string
err = json.Unmarshal(jsonbuf, &dict)
if err != nil {
return nil, err
}
if privateKey.KeyID() != "" {
dict["kid"] = privateKey.KeyID()
}
return dict, err
}
func JWKDeserialize(privateKeyDict map[string]string) (jwk.RSAPrivateKey, error) {
jsonbuf, err := json.Marshal(privateKeyDict)
if err != nil {
return nil, err
}
key, err := jwk.ParseKey(jsonbuf)
if err != nil {
return nil, fmt.Errorf("failed to create jwk.Key from RSA private key: %s\n", err)
}
return key.(jwk.RSAPrivateKey), nil
}

View File

@ -0,0 +1,60 @@
package jwk
import (
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/stretchr/testify/require"
"testing"
)
func TestJWKSerialize(t *testing.T) {
//test
keypair, err := JWKGenerate()
require.NoError(t, err)
dict, err := JWKSerialize(keypair)
require.NoError(t, err)
keys := []string{}
for key, _ := range dict {
keys = append(keys, key)
}
//assert
require.NotEmpty(t, keypair.KeyID())
require.ElementsMatch(t, []string{"d", "dp", "dq", "e", "kty", "n", "p", "q", "qi", "kid"}, keys)
require.Equal(t, "RSA", dict["kty"])
//require.Equal(t, map[string]string{}, dict)
require.Equal(t, string(keypair.N()), dict["n"])
}
func TestJWKDeserialize(t *testing.T) {
//setup
keypairDict := map[string]string{
"kty": "RSA",
"kid": "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df",
"n": "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w",
"e": "AQAB",
"d": "ksDmucdMJXkFGZxiomNHnroOZxe8AmDLDGO1vhs-POa5PZM7mtUPonxwjVmthmpbZzla-kg55OFfO7YcXhg-Hm2OWTKwm73_rLh3JavaHjvBqsVKuorX3V3RYkSro6HyYIzFJ1Ek7sLxbjDRcDOj4ievSX0oN9l-JZhaDYlPlci5uJsoqro_YrE0PRRWVhtGynd-_aWgQv1YzkfZuMD-hJtDi1Im2humOWxA4eZrFs9eG-whXcOvaSwO4sSGbS99ecQZHM2TcdXeAs1PvjVgQ_dKnZlGN3lTWoWfQP55Z7Tgt8Nf1q4ZAKd-NlMe-7iqCFfsnFwXjSiaOa2CRGZn-Q",
"p": "4A5nU4ahEww7B65yuzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ--wwfpRwHvSxtNU9qXb8ewo-BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3InKF4JvIlchyqs0RQ8wx7lULqwnn0",
"q": "ven83GM6SfrmO-TBHbjTk6JhP_3CMsIvmSdo4KrbQNvp4vHO3w1_0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEBpxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA-k4UoH_eQmGKGK44TRzYj5hZYGWIC8",
"dp": "lmmU_AG5SGxBhJqb8wxfNXDPJjf__i92BgJT2Vp4pskBbr5PGoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ-m0_XSWx13v9t9DIbheAtgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpE",
"dq": "mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe__EjuCBbwHfcT8OG3hWOv8vpzokQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p-AF2p6Yfahscjtq-GY9cB85NxLy2IXCC0PF--Sq9LOrTE9QV988SJy_yUrAjcZ5MmECk",
"qi": "ldHXIrEmMZVaNwGzDF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uYiqewXfCKw_UngrJt8Xwfq1Zruz0YY869zPN4GiE9-9rzdZB33RBw8kIOquY3MK74FMwCihYx_LiU2YTHkaoJ3ncvtvg",
}
//test
keypair, err := JWKDeserialize(keypairDict)
require.NoError(t, err)
serialized, err := JWKSerialize(keypair)
require.NoError(t, err)
//assert
require.NotEmpty(t, keypair.KeyID())
require.Equal(t, keypair.KeyID(), "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df")
require.Equal(t, keypair.KeyType(), jwa.KeyType("RSA"))
require.Equal(t, keypairDict, serialized)
}

View File

@ -0,0 +1,17 @@
package models
type ClientRegistrationRequest struct {
SoftwareId string `json:"software_id"`
Jwks ClientRegistrationRequestJwks `json:"jwks"`
}
type ClientRegistrationRequestJwks struct {
Keys []ClientRegistrationRequestJwksKey `json:"keys"`
}
type ClientRegistrationRequestJwksKey struct {
KeyId string `json:"kid"`
KeyType string `json:"kty"`
PublicExponent string `json:"e"`
Modulus string `json:"n"`
}

View File

@ -0,0 +1,31 @@
package models
// ClientRegistrationResponse {
// "redirect_uris": [
// " https://fhir.epic.com/test/smart"
// ],
// "token_endpoint_auth_method": "none",
// "grant_types": [
// "urn:ietf:params:oauth:grant-type:jwt-bearer"
// ],
// "software_id": " d45049c3-3441-40ef-ab4d-b9cd86a17225",
// "client_id": "G65DA2AF4-1C91-11EC-9280-0050568B7514",
// "client_id_issued_at": 1632417134,
// "jwks": {
// "keys": [{
// "kty": "RSA",
// "n": "vGASMnWdI-ManPgJi5XeT15Uf1tgpaNBmxfa-_bKG6G1DDTsYBy2K1uubppWMcl8Ff_2oWe6wKDMx2-bvrQQkR1zcV96yOgNmfDXuSSR1y7xk1Kd-uUhvmIKk81UvKbKOnPetnO1IftpEBm5Llzy-1dN3kkJqFabFSd3ujqi2ZGuvxfouZ-S3lpTU3O6zxNR6oZEbP2BwECoBORL5cOWOu_pYJvALf0njmamRQ2FKKCC-pf0LBtACU9tbPgHorD3iDdis1_cvk16i9a3HE2h4Hei4-nDQRXfVgXLzgr7GdJf1ArR1y65LVWvtuwNf7BaxVkEae1qKVLa2RUeg8imuw",
// "e": "AQAB"
// }
// ]
// }
//}
type ClientRegistrationResponse struct {
RedirectUrls []string `json:"redirect_uris"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
GrantTypes []string `json:"grant_types"`
SoftwareId string `json:"software_id"`
ClientId string `json:"client_id"`
ClientIdIssuedAt int `json:"client_id_issued_at"`
Jwks ClientRegistrationRequestJwks `json:"jwks"`
}

View File

@ -0,0 +1,11 @@
package models
type ClientRegistrationTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
State string `json:"state"`
Patient string `json:"patient"`
EpicDstu2Patient string `json:"__epic.dstu2.patient"`
}

View File

@ -1,8 +1,18 @@
package models
import (
"encoding/json"
"fmt"
"github.com/fastenhealth/fasten-sources/pkg"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/jwk"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"io"
"log"
"net/http"
"net/url"
"time"
)
// SourceCredential Data/Medical Provider Credentials
@ -18,6 +28,7 @@ type SourceCredential struct {
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint"` //optional - required when Dynamic Client Registration mode is set
Scopes []string `json:"scopes_supported" gorm:"type:text;serializer:json"`
Issuer string `json:"issuer"`
@ -33,8 +44,9 @@ type SourceCredential struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"` //lighthouse url the provider will redirect to (registered with App)
Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE)
CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight
Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE)
DynamicClientRegistrationMode string `json:"dynamic_client_registration_mode"` //if enabled, will dynamically register client with provider (https://oauth.net/2/dynamic-client-registration/)
CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight
//SecretKeyPrefix string `json:"-"` //the secret key prefix to use, if empty (default) will use the sourceType value
// auth/credential data
@ -44,6 +56,10 @@ type SourceCredential struct {
ExpiresAt int64 `json:"expires_at"`
CodeChallenge string `json:"code_challenge"`
CodeVerifier string `json:"code_verifier"`
//dynamic client auth/credential data
DynamicClientJWKS []map[string]string `json:"dynamic_client_jwks" gorm:"type:text;serializer:json"`
DynamicClientId string `json:"dynamic_client_id"`
}
func (s *SourceCredential) GetSourceType() pkg.SourceType {
@ -82,7 +98,7 @@ func (s *SourceCredential) GetExpiresAt() int64 {
return s.ExpiresAt
}
func (s *SourceCredential) RefreshTokens(accessToken string, refreshToken string, expiresAt int64) {
func (s *SourceCredential) SetTokens(accessToken string, refreshToken string, expiresAt int64) {
if expiresAt > 0 && expiresAt != s.ExpiresAt {
s.ExpiresAt = expiresAt
}
@ -108,5 +124,83 @@ tokenResponse: payload,
expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()),
codeChallenge: codeChallenge,
codeVerifier: codeVerifier
*/
// IsDynamicClient this method is used to check if this source uses dynamic client registration (used to customize token refresh logic)
func (s *SourceCredential) IsDynamicClient() bool {
return len(s.DynamicClientRegistrationMode) > 0
}
//this will set/update the AccessToken and Expiry using the dynamic client credentials
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
if len(s.DynamicClientRegistrationMode) == 0 {
return fmt.Errorf("dynamic client registration mode not set")
}
if len(s.DynamicClientJWKS) == 0 {
return fmt.Errorf("dynamic client jwks not set")
}
if len(s.DynamicClientId) == 0 {
return fmt.Errorf("dynamic client id not set")
}
//convert the serialized dynamic-client credentials to a jwx.Key
jwkeypair, err := jwk.JWKDeserialize(s.DynamicClientJWKS[0])
if err != nil {
return err
}
// see https://github.com/lestrrat-go/jwx/tree/v2/jwt#token-usage
t := jwt.New()
t.Set("kid", jwkeypair.KeyID())
t.Set(jwt.SubjectKey, s.DynamicClientId)
t.Set(jwt.AudienceKey, s.TokenEndpoint)
t.Set(jwt.JwtIDKey, uuid.New().String())
t.Set(jwt.ExpirationKey, time.Now().Add(time.Minute*2).Unix()) // must be less than 5 minutes from now. Time when this JWT expires
t.Set(jwt.IssuedAtKey, time.Now().Unix())
t.Set(jwt.IssuerKey, s.DynamicClientId)
//sign the jwt with the private key
// Signing a token (using raw rsa.PrivateKey)
signed, err := jwt.Sign(t, jwt.WithKey(jwa.RS256, jwkeypair))
if err != nil {
return fmt.Errorf("failed to sign dynamic-client jwt: %s", err)
}
//send this signed jwt to the token endpoint to get a new access token
// https://fhir.epic.com/Documentation?docId=oauth2&section=JWKS
postForm := url.Values{
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
"assertion": {string(signed)},
"client_id": {s.DynamicClientId},
}
tokenResp, err := http.PostForm(s.TokenEndpoint, postForm)
if err != nil {
return fmt.Errorf("an error occurred while sending dynamic client token request, %s", err)
}
defer tokenResp.Body.Close()
if tokenResp.StatusCode >= 300 || tokenResp.StatusCode < 200 {
b, err := io.ReadAll(tokenResp.Body)
if err != nil {
log.Printf("Response body: %s", string(b))
}
return fmt.Errorf("an error occurred while reading dynamic client token response, status code was not 200: %d", tokenResp.StatusCode)
}
var registrationTokenResponseBytes ClientRegistrationTokenResponse
err = json.NewDecoder(tokenResp.Body).Decode(&registrationTokenResponseBytes)
if err != nil {
return fmt.Errorf("an error occurred while parsing dynamic client token response", err)
}
//update the source credential with the new access token
s.AccessToken = registrationTokenResponseBytes.AccessToken
s.ExpiresAt = time.Now().Add(time.Second * time.Duration(registrationTokenResponseBytes.ExpiresIn)).Unix()
return nil
}

View File

@ -1,16 +1,20 @@
package handler
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/fastenhealth/fasten-sources/clients/factory"
sourceModels "github.com/fastenhealth/fasten-sources/clients/models"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/jwk"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
)
@ -28,6 +32,108 @@ func CreateSource(c *gin.Context) {
logger.Infof("Parsed Create SourceCredential Credentials Payload: %v", sourceCred)
if sourceCred.DynamicClientRegistrationMode == "user-authenticated" {
logger.Warnf("This client requires a dynamice client registration, starting registration process")
if len(sourceCred.RegistrationEndpoint) == 0 {
logger.Errorln("Empty registration endpoint, cannot be used with dynamic-client registration mode:", sourceCred.DynamicClientRegistrationMode)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//this source requires dynamic client registration
// see https://fhir.epic.com/Documentation?docId=Oauth2&section=Standalone-Oauth2-OfflineAccess-0
// Generate a public-private key pair
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
if err != nil {
logger.Errorln("An error occurred while generating device-specific keypair for dynamic client", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//store in sourceCredential
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
if err != nil {
logger.Errorln("An error occurred while serializing keypair for dynamic client", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
sourceCred.DynamicClientJWKS = []map[string]string{
serializedKeypair,
}
//generate dynamic client registration request
payload := models.ClientRegistrationRequest{
SoftwareId: sourceCred.ClientId,
Jwks: models.ClientRegistrationRequestJwks{
Keys: []models.ClientRegistrationRequestJwksKey{
{
KeyType: "RSA",
KeyId: serializedKeypair["kid"],
Modulus: serializedKeypair["n"],
PublicExponent: serializedKeypair["e"],
},
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
logger.Errorln("An error occurred while marshalling dynamic client registration request", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
req, err := http.NewRequest(http.MethodPost, sourceCred.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
if err != nil {
logger.Errorln("An error occurred while generating dynamic client registration request", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sourceCred.AccessToken))
registrationResponse, err := http.DefaultClient.Do(req)
if err != nil {
logger.Errorln("An error occurred while sending dynamic client registration request", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
defer registrationResponse.Body.Close()
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
logger.Errorln("An error occurred while reading dynamic client registration response, status code was not 200", registrationResponse.StatusCode)
b, err := io.ReadAll(registrationResponse.Body)
if err != nil {
logger.Printf("Response body: %s", string(b))
}
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//read response
var registrationResponseBytes models.ClientRegistrationResponse
err = json.NewDecoder(registrationResponse.Body).Decode(&registrationResponseBytes)
if err != nil {
logger.Errorln("An error occurred while parsing dynamic client registration response", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//store the dynamic client id
sourceCred.DynamicClientId = registrationResponseBytes.ClientId
//generate a JWT token and then use it to get an access token for the dynamic client
err = sourceCred.RefreshDynamicClientAccessToken()
if err != nil {
logger.Errorln("An error occurred while retrieving access token for dynamic client", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
}
err := databaseRepo.CreateSource(c, &sourceCred)
if err != nil {
logger.Errorln("An error occurred while storing source credential", err)

View File

@ -141,6 +141,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
token_endpoint: sourceMetadata.token_endpoint,
introspection_endpoint: sourceMetadata.introspection_endpoint,
userinfo_endpoint: sourceMetadata.userinfo_endpoint,
registration_endpoint: sourceMetadata.registration_endpoint,
api_endpoint_base_url: sourceMetadata.api_endpoint_base_url,
client_id: sourceMetadata.client_id,
redirect_uri: sourceMetadata.redirect_uri,
@ -158,6 +159,8 @@ export class MedicalSourcesConnectedComponent implements OnInit {
refresh_token: payload.refresh_token,
id_token: payload.id_token,
dynamic_client_registration_mode: sourceMetadata.dynamic_client_registration_mode,
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
expires_at: parseInt(this.getAccessTokenExpiration(payload)),
})

View File

@ -11,6 +11,10 @@ export class Source extends LighthouseSourceMetadata{
id_token?: string
expires_at: number //seconds since epoch
dynamic_client_jwks?: any[]
dynamic_client_id?: string
constructor(object: any) {
super()
return Object.assign(this, object)

View File

@ -6,6 +6,7 @@ export class LighthouseSourceMetadata extends MetadataSource {
token_endpoint: string
introspection_endpoint: string
userinfo_endpoint: string
registration_endpoint: string
scopes_supported: string[]
issuer: string
@ -20,5 +21,6 @@ export class LighthouseSourceMetadata extends MetadataSource {
redirect_uri: string
confidential: boolean
dynamic_client_registration_mode: string
cors_relay_required: boolean
}

14
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/dave/jennifer v1.6.1
github.com/dominikbraun/graph v0.15.0
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
github.com/fastenhealth/fasten-sources v0.2.1
github.com/fastenhealth/fasten-sources v0.2.3
github.com/fastenhealth/gofhir-models v0.0.5
github.com/gin-gonic/gin v1.9.0
github.com/glebarez/sqlite v1.5.0
@ -15,12 +15,13 @@ require (
github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0
github.com/iancoleman/strcase v0.2.0
github.com/lestrrat-go/jwx/v2 v2.0.11
github.com/philips-software/go-hsdp-api v0.81.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.2
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.11.2
golang.org/x/crypto v0.8.0
golang.org/x/crypto v0.9.0
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
golang.org/x/net v0.10.0
gorm.io/datatypes v1.0.7
@ -34,6 +35,7 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
@ -57,6 +59,11 @@ require (
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
@ -73,6 +80,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.35.0 // indirect
github.com/seborama/govcr v4.5.0+incompatible // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect

37
go.sum
View File

@ -155,6 +155,9 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA=
github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@ -188,8 +191,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/fastenhealth/fasten-sources v0.2.1 h1:ZdQXg3cRelPo/WsguCU+Ic3F/4GfROeCj/LwIm9NpVM=
github.com/fastenhealth/fasten-sources v0.2.1/go.mod h1:B7pVQcwLuL+rgjSHwlu3p0CySyHN262BkfbYMKVKXTk=
github.com/fastenhealth/fasten-sources v0.2.3 h1:a40yp/cim0PUf6DFH3WgMW4a6J7cgUDycdQqur1dq0c=
github.com/fastenhealth/fasten-sources v0.2.3/go.mod h1:B7pVQcwLuL+rgjSHwlu3p0CySyHN262BkfbYMKVKXTk=
github.com/fastenhealth/gofhir-models v0.0.5 h1:wU2Dz+/h9MzZCTRgkQzeq5l0EFuMI6C5xgCbKislFpg=
github.com/fastenhealth/gofhir-models v0.0.5/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -527,6 +530,19 @@ github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLW
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ=
github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -678,6 +694,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seborama/govcr v4.5.0+incompatible h1:XvdHtXi0d4cUAn+0aWolvwfS3nmhNC8Z+yMQwn/M64I=
github.com/seborama/govcr v4.5.0+incompatible/go.mod h1:EgcISudCCYDLzbiAImJ8i7kk4+wTA44Kp+j4S0LhASI=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE=
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@ -723,12 +741,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
github.com/tchap/go-patricia v0.0.0-20160729071656-dd168db6051b/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
@ -819,8 +839,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -860,6 +880,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -910,6 +931,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -935,6 +957,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1007,12 +1030,14 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1025,6 +1050,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1098,6 +1124,7 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=