レッスン23:APIゲートウェイの作成
APIゲートウェイとは、クライアントと複数の必要なサービスとの間のAPI通信を一元的に処理するアプリケーションのコンポーネントです。
APIゲートウェイは、サーバーに保存されてクライアントから呼び出されるGraphQLパーシステッドクエリを通じて実装できます。これらのクエリは1つ以上のバックエンドサービスと連携し、結果をまとめてクライアントに単一のレスポンスとして返します。
GraphQLパーシステッドクエリを使ってAPIゲートウェイを提供する利点には、次のようなものがあります。
- クライアントはバックエンドサービスへの接続を管理する必要がなくなり、ロジックが簡素化されます
- バックエンドサービスへのアクセスが一元化されます
- クライアント側に認証情報が公開されません
- サービスからのレスポンスをクライアントが期待する形式や扱いやすい形式に変換できます
- バックエンドサービスがアップグレードされた場合でも、クライアントに破壊的変更を生じさせることなくパーシステッドクエリを適応できます
- サーバーはバックエンドサービスへのアクセスログを保存し、分析を強化するためのメトリクスを抽出できます
このチュートリアルレッスンでは、GitHub Actions APIから最新のアーティファクトを取得し、クライアントがGitHubにサインインしなくてもダウンロードできるURLを抽出するAPIゲートウェイを示します。
GraphQLを活用したAPIゲートウェイによるGitHub Actionsアーティファクトへのアクセス
以下のGraphQLクエリは、パーシステッドクエリとして保存する必要があります(例:スラッグ retrieve-public-urls-for-github-actions-artifacts を使用)。
このクエリはGitHub Actionsアーティファクトの公開アクセス可能なダウンロードURLを取得します。
- まず、GitHub Actionsから最新のX件のアーティファクトを取得し、それぞれにアクセスするためのプロキシURLを抽出します。(認証済みユーザーのみがアーティファクトにアクセスできるため、これらのURLはまだ実際のアーティファクトを指していません。)
- 次に、これらのプロキシURL(アーティファクトが短期間だけ公開場所にアップロードされている)にアクセスし、HTTPレスポンスの
Locationヘッダーから実際のURLを抽出します - 最後に、公開アクセス可能なURLをすべて出力し、未認証のユーザーがその時間内にGitHubアーティファクトをダウンロードできるようにします
(チュートリアルレッスンはここで終わりますが、続きとして、GraphQLクエリでこれらのURLを使って何かを行うこともできます:メールで送信する、FTPでどこかにファイルをアップロードする、InstaWPサイトにインストールするなど。)
query RetrieveGitHubAccessToken {
githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
@export(as: "githubAccessToken")
@remove
}
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
@depends(on: "RetrieveGitHubAccessToken")
{
githubArtifactsEndpoint: _sprintf(
string: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts?per_page=%s",
values: [$numberArtifacts]
)
@remove
# Retrieve Artifact data from GitHub Actions API
gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
input: {
url: $__githubArtifactsEndpoint,
options: {
auth: {
password: $githubAccessToken
},
headers: [
{
name: "Accept",
value: "application/vnd.github+json"
}
]
}
}
)
@remove
# Extract the URL from within each "artifacts" item
gitHubProxyArtifactDownloadURLs: _objectProperty(
object: $__gitHubArtifactData,
by: {
key: "artifacts"
}
)
@underEachArrayItem(passValueOnwardsAs: "artifactItem")
@applyField(
name: "_objectProperty",
arguments: {
object: $artifactItem,
by: {
key: "archive_download_url"
}
},
setResultInResponse: true
)
@export(as: "gitHubProxyArtifactDownloadURLs")
}
query CreateHTTPRequestInputs
@depends(on: "RetrieveProxyArtifactDownloadURLs")
{
httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
@underEachArrayItem(
passValueOnwardsAs: "url"
)
@applyField(
name: "_objectAddEntry",
arguments: {
object: {
options: {
auth: {
password: $githubAccessToken
},
headers: {
name: "Accept",
value: "application/vnd.github+json"
},
allowRedirects: null
}
},
key: "url",
value: $url
},
setResultInResponse: true
)
@export(as: "httpRequestInputs")
@remove
}
query RetrieveActualArtifactDownloadURLs
@depends(on: "CreateHTTPRequestInputs")
{
_sendHTTPRequests(
inputs: $httpRequestInputs
) {
artifactDownloadURL: header(name: "Location")
@export(as: "artifactDownloadURLs", type: LIST)
}
}
query PrintArtifactDownloadURLsAsList
@depends(on: "RetrieveActualArtifactDownloadURLs")
{
artifactDownloadURLs: _echo(value: $artifactDownloadURLs)
}レスポンスは次のとおりです。
{
"data": {
"gitHubProxyArtifactDownloadURLs": [
"https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444209/zip",
"https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444208/zip",
"https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444207/zip"
],
"_sendHTTPRequests": [
{
"artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9351393Z&urlSigningMethod=HMACV2&urlSignature=8v8cDVZKAnkXoN8z1GdjXLz4SCGkpv%2Fl0qjlDArac5M%3D"
},
{
"artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9333471Z&urlSigningMethod=HMACV2&urlSignature=ffsyy0p97oeQByMD3X6WKbFyIEbh6nbU%2BFsXKHQHYSM%3D"
},
{
"artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9699160Z&urlSigningMethod=HMACV2&urlSignature=gUi%2F39RS7X5YgVZbEu977ufFt1girQKeNI7LP61gxfY%3D"
}
],
"artifactDownloadURLs": [
"https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9351393Z&urlSigningMethod=HMACV2&urlSignature=8v8cDVZKAnkXoN8z1GdjXLz4SCGkpv%2Fl0qjlDArac5M%3D",
"https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9333471Z&urlSigningMethod=HMACV2&urlSignature=ffsyy0p97oeQByMD3X6WKbFyIEbh6nbU%2BFsXKHQHYSM%3D",
"https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9699160Z&urlSigningMethod=HMACV2&urlSignature=gUi%2F39RS7X5YgVZbEu977ufFt1girQKeNI7LP61gxfY%3D"
]
}
}代替案:HTTPリクエストからGitHubの認証情報を取得する
ユーザーが自分のGitHubの認証情報をヘッダーで提供できるようにすることもできます。
このGraphQLクエリは前のクエリを応用したもので、次の点が異なります。
- オペレーション
RetrieveGitHubAccessTokenは現在のHTTPリクエストのX-Github-Access-Tokenヘッダーから値を読み取ってエクスポートし、このヘッダーが提供されていない場合はその旨を示します FailIfGitHubAccessTokenIsMissingはヘッダーがない場合にエラーを発生させます- 他のすべてのオペレーションにはディレクティブ
@skip(if: $isGithubAccessTokenMissing)が追加されており、トークンがない場合には実行されません
query RetrieveGitHubAccessToken {
githubAccessToken: _httpRequestHeader(name: "X-Github-Access-Token")
@export(as: "githubAccessToken")
@remove
isGithubAccessTokenMissing: _isEmpty(value: $__githubAccessToken)
@export(as: "isGithubAccessTokenMissing")
}
query FailIfGitHubAccessTokenIsMissing
@depends(on: "RetrieveGitHubAccessToken")
@include(if: $isGithubAccessTokenMissing)
{
_fail(
message: "Header 'X-Github-Access-Token' has not been provided"
) @remove
}
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
@depends(on: "RetrieveGitHubAccessToken")
@skip(if: $isGithubAccessTokenMissing)
{
# Do same as before
# ...
}
query CreateHTTPRequestInputs
@depends(on: "RetrieveProxyArtifactDownloadURLs")
@skip(if: $isGithubAccessTokenMissing)
{
# Do same as before
# ...
}
query RetrieveActualArtifactDownloadURLs
@depends(on: "CreateHTTPRequestInputs")
@skip(if: $isGithubAccessTokenMissing)
{
# Do same as before
# ...
}
query PrintArtifactDownloadURLsAsList
@depends(on: [
"RetrieveActualArtifactDownloadURLs",
"FailIfGitHubAccessTokenIsMissing"
])
@skip(if: $isGithubAccessTokenMissing)
{
# Do same as before
# ...
}ヘッダー X-Github-Access-Token が提供された場合、レスポンスは上記と同じになります。
提供されなかった場合、レスポンスは次のようになります。
{
"errors": [
{
"message": "Header 'X-Github-Access-Token' has not been provided",
"locations": [
{
"line": 18,
"column": 3
}
],
"extensions": {
"path": [
"_fail(message: \"Header 'X-Github-Access-Token' has not been provided\") @remove",
"query FailIfGitHubAccessTokenIsMissing @depends(on: \"ValidateHasGitHubAccessToken\") @skip(if: $isGithubAccessTokenMissing) { ... }"
],
"type": "QueryRoot",
"field": "_fail(message: \"Header 'X-Github-Access-Token' has not been provided\") @remove",
"id": "root",
"code": "PoPSchema/FailFieldAndDirective@e1"
}
}
],
"data": {
"isGithubAccessTokenMissing": false
}
}APIゲートウェイで使用する複数サービスの認証情報をヘッダーから取得しながら、すべて提供されていることを検証できます。
query RetrieveServiceTokens {
githubAccessToken: _httpRequestHeader(name: "X-Github-Access-Token")
@export(as: "githubAccessToken")
slackAccessToken: _httpRequestHeader(name: "X-Slack-Access-Token")
@export(as: "slackAccessToken")
isGithubAccessTokenMissing: _isEmpty(value: $__githubAccessToken)
isSlackAccessTokenMissing: _isEmpty(value: $__slackAccessToken)
isAnyAccessTokenMissing: _or(values: [
$__isGithubAccessTokenMissing,
$__isSlackAccessTokenMissing
])
@export(as: "isAnyAccessTokenMissing")
}
query FailIfAnyAccessTokenMissing
@depends(on: "RetrieveServiceTokens")
@include(if: $isAnyAccessTokenMissing)
{
_fail(
message: "Access tokens for GitHub and Slack must be provided"
) @remove
}
query RetrieveProxyArtifactDownloadURLs
@depends(on: "RetrieveServiceTokens")
@skip(if: $isAnyAccessTokenMissing)
{
# Do something
# ...
}
# Do something
# ...ステップバイステップ:GraphQLクエリの作成
以下は、このクエリがどのように機能するかの詳細な分析です。
接続先のエンドポイントは動的に生成できます。この場合は _sprintf を使用します。
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
@depends(on: "RetrieveGitHubAccessToken")
{
githubArtifactsEndpoint: _sprintf(
string: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts?per_page=%s",
values: [$numberArtifacts]
)
@remove
# ...
}GitHub Actions APIからのレスポンスはデータ量が多く、私たちには不要なので、@remove を使ってレスポンスから除去します。ただし、開発中はこのディレクティブを無効にして、返されたJSONオブジェクトの形状を可視化・把握し、抽出が必要なデータ項目を特定します。
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
@depends(on: "RetrieveGitHubAccessToken")
{
# ...
# Retrieve Artifact data from GitHub Actions API
gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
input: {
url: $__githubArtifactsEndpoint,
options: {
auth: {
password: $githubAccessToken
},
headers: [
{
name: "Accept",
value: "application/vnd.github+json"
}
]
}
}
)
# @remove <= Disabled to visualize output
}レスポンスは次のとおりです。
{
"data": {
"gitHubArtifactData": {
"total_count": 8344,
"artifacts": [
{
"id": 803739808,
"node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDg=",
"name": "gato-graphql-testing-schema-1.0.0-dev",
"size_in_bytes": 62952,
"url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808",
"archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808/zip",
"expired": false,
"created_at": "2023-07-14T06:25:57Z",
"updated_at": "2023-07-14T06:25:59Z",
"expires_at": "2023-08-13T06:17:15Z",
"workflow_run": {
"id": 5551097653,
"repository_id": 66721227,
"head_repository_id": 66721227,
"head_branch": "Enable-headers-in-GraphiQL",
"head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
}
},
{
"id": 803739806,
"node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDY=",
"name": "gato-graphql-testing-1.0.0-dev",
"size_in_bytes": 123914,
"url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806",
"archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806/zip",
"expired": false,
"created_at": "2023-07-14T06:25:57Z",
"updated_at": "2023-07-14T06:25:59Z",
"expires_at": "2023-08-13T06:17:11Z",
"workflow_run": {
"id": 5551097653,
"repository_id": 66721227,
"head_repository_id": 66721227,
"head_branch": "Enable-headers-in-GraphiQL",
"head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
}
},
{
"id": 803739803,
"node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDM=",
"name": "gato-graphql-1.0.0-dev",
"size_in_bytes": 33394234,
"url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803",
"archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803/zip",
"expired": false,
"created_at": "2023-07-14T06:25:57Z",
"updated_at": "2023-07-14T06:25:59Z",
"expires_at": "2023-08-13T06:21:42Z",
"workflow_run": {
"id": 5551097653,
"repository_id": 66721227,
"head_repository_id": 66721227,
"head_branch": "Enable-headers-in-GraphiQL",
"head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
}
}
]
}
}
}注目するデータ項目はプロパティ "archive_download_url" です。JSONオブジェクト構造内のこれらのデータ項目それぞれに移動し、フィールド _objectProperty(ディレクティブ @applyField を介して適用)を使ってその値を抽出し、引数 setResultInResponse: true を渡すことで反復中の要素を上書きします。
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
@depends(on: "RetrieveGitHubAccessToken")
{
# ...
# Extract the URL from within each "artifacts" item
gitHubProxyArtifactDownloadURLs: _objectProperty(
object: $__gitHubArtifactData,
by: {
key: "artifacts"
}
)
@underEachArrayItem(passValueOnwardsAs: "artifactItem")
@applyField(
name: "_objectProperty",
arguments: {
object: $artifactItem,
by: {
key: "archive_download_url"
}
},
setResultInResponse: true
)
@export(as: "gitHubProxyArtifactDownloadURLs")
}フィールド _sendHTTPRequests を通じて(複数のHTTPリクエストを非同期に送信しながら)抽出したすべてのアーティファクトURLに同時に接続し、それぞれのレスポンスから Location ヘッダーを取得します。
フィールド _sendHTTPRequests は引数 input(型 [HTTPRequestInput])を受け取るため、このinputを動的に生成します。具体的には次の手順で行います。
- アーティファクトの各URL(動的変数
$gitHubProxyArtifactDownloadURLsに格納)を反復処理します - 各URLに対して、必要なすべてのパラメーター(ヘッダー、認証、その他)を含むJSONオブジェクトをフィールド
_objectAddEntryを使って動的に構築します - このJSONオブジェクトにURL(動的変数
$urlで利用可能)を追加します
この動的に作成されたJSONオブジェクトのリストは、_sendHTTPRequests(input:) に引数として渡されるときに [HTTPRequestInput] に変換されます。手順に誤りがあって何らかの項目が HTTPRequestInput に変換できない場合(例:必須プロパティを指定していない、存在しないプロパティを指定しているなど)、GraphQLサーバーは変換エラーを発生させます。
フィールド httpRequestInputs はGitHubトークン(password: $githubAccessToken の下)を含んでいるため、レスポンスに出力しないよう @remove を使う必要があります。ただし、開発中はこのディレクティブを無効にすることができます。
query CreateHTTPRequestInputs
@depends(on: "RetrieveProxyArtifactDownloadURLs")
{
httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
@underEachArrayItem(
passValueOnwardsAs: "url"
)
@applyField(
name: "_objectAddEntry",
arguments: {
object: {
options: {
auth: {
password: $githubAccessToken
},
headers: {
name: "Accept",
value: "application/vnd.github+json"
},
allowRedirects: null
}
},
key: "url",
value: $url
},
setResultInResponse: true
)
@export(as: "httpRequestInputs")
# @remove <= Disabled to visualize output
}
query RetrieveActualArtifactDownloadURLs
@depends(on: "CreateHTTPRequestInputs")
{
_sendHTTPRequests(
inputs: $httpRequestInputs
) {
artifactDownloadURL: header(name: "Location")
@export(as: "artifactDownloadURLs", type: LIST)
}
}@remove がコメントアウトされたことで、レスポンス内の生成されたJSONオブジェクトのinput(エントリ httpRequestInputs の下)と、各HTTPレスポンスの Location ヘッダーの結果(エイリアス artifactDownloadURL の下)を可視化できるようになります。
{
"data": {
"gitHubProxyArtifactDownloadURLs": [
// ...
],
"httpRequestInputs": [
{
"options": {
"auth": {
"password": "ghp_{some_github_access_token}"
},
"headers": {
"name": "Accept",
"value": "application/vnd.github+json"
},
"allowRedirects": null
},
"url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808/zip"
},
{
"options": {
"auth": {
"password": "ghp_{some_github_access_token}"
},
"headers": {
"name": "Accept",
"value": "application/vnd.github+json"
},
"allowRedirects": null
},
"url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806/zip"
},
{
"options": {
"auth": {
"password": "ghp_{some_github_access_token}"
},
"headers": {
"name": "Accept",
"value": "application/vnd.github+json"
},
"allowRedirects": null
},
"url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803/zip"
}
],
"_sendHTTPRequests": [
{
"artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2766840Z&urlSigningMethod=HMACV2&urlSignature=Ype82npdlUlLk4gcGZcBiz80e0ZuvcvnC2rdaSDg9p8%3D"
},
{
"artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2961965Z&urlSigningMethod=HMACV2&urlSignature=FdWAh8JXNPJsVIPNuiYN8R7i0vRnN8eCGc57VZDNUEc%3D"
},
{
"artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2861087Z&urlSigningMethod=HMACV2&urlSignature=0Go8QnkZqIbn0urTQqfbMW4rQtjMfDAR9fSm6fCePjw%3D"
}
]
}
}最後に、_echo を使って、すべての artifactDownloadURL 項目(動的変数 $artifactDownloadURLs で利用可能)をリストとしてまとめて出力します。
query PrintArtifactDownloadURLsAsList
@depends(on: "RetrieveActualArtifactDownloadURLs")
{
artifactDownloadURLs: _echo(value: $artifactDownloadURLs)
}これにより次のように出力されます。
{
"data": {
// ...
"artifactDownloadURLs": [
"https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.4998268Z&urlSigningMethod=HMACV2&urlSignature=1c1qNRfD9KFwSuzMjw9tsumq9B5I1c9H4LWgSbR0Kwg%3D",
"https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.4878741Z&urlSigningMethod=HMACV2&urlSignature=htjc1HrmZpbecECpBQnEHhlP7lkqkdyjzATb0vFnzDE%3D",
"https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.5240496Z&urlSigningMethod=HMACV2&urlSignature=YDuHFqweL9m6LIycLsVy0bJJ4zePc4pWkHz8RfjfzCg%3D"
]
}
}