Add support for Direct Workload Identity auth (#348)

This adds a new authentication mode, Direct Workload Identity
Federation. This new mode permits authenticating to Google Cloud
directly using the GitHub Actions OIDC token instead of proxying through
a Google Cloud Service Account.
This commit is contained in:
Seth Vargo 2023-11-28 10:41:10 -05:00 committed by GitHub
parent f105ef0cdb
commit fe9207673e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2186 additions and 1311 deletions

View File

@ -8,4 +8,9 @@ module.exports = {
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
], ],
// We have many situations where we accept and expect arbitrary JSON payloads.
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
}; };

View File

@ -27,6 +27,10 @@ concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs: jobs:
unit: unit:
name: 'unit' name: 'unit'
@ -48,9 +52,13 @@ jobs:
- name: 'npm test' - name: 'npm test'
run: 'npm run test' run: 'npm run test'
credentials_json:
#
# Direct Workload Identity Federation
#
direct_workload_identity_federation:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }} if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'credentials_json' name: 'direct_workload_identity_federation'
runs-on: '${{ matrix.os }}' runs-on: '${{ matrix.os }}'
strategy: strategy:
fail-fast: false fail-fast: false
@ -60,6 +68,9 @@ jobs:
- 'windows-latest' - 'windows-latest'
- 'macos-latest' - 'macos-latest'
permissions:
id-token: 'write'
steps: steps:
- uses: 'actions/checkout@v4' - uses: 'actions/checkout@v4'
@ -74,56 +85,33 @@ jobs:
name: 'auth-default' name: 'auth-default'
uses: './' uses: './'
with: with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}' project_id: '${{ vars.PROJECT_ID }}'
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
- id: 'setup-gcloud' - uses: 'google-github-actions/setup-gcloud@main'
name: 'setup-gcloud' with:
uses: 'google-github-actions/setup-gcloud@main' version: '>= 363.0.0'
- id: 'gcloud' - name: 'gcloud'
name: 'gcloud'
shell: 'bash'
run: |- run: |-
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}" gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
- id: 'auth-access-token' - id: 'oauth-federated-token'
name: 'auth-access-token' name: 'oauth-federated-token'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'access_token'
- id: 'access-token'
name: 'access-token'
shell: 'bash'
run: |- run: |-
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \ curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-default.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
--silent \ --silent \
--show-error \ --show-error \
--fail \ --fail \
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}" --header "Authorization: Bearer ${{ steps.auth-default.outputs.auth_token }}"
- id: 'auth-id-token'
name: 'auth-id-token'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true
- id: 'auth-sa-retries' #
name: 'auth-sa-retries' # Workload Identity Federation through a Service Account
uses: './' #
with: workload_identity_federation_through_service_account:
retries: '2'
backoff: '200'
backoff_limit: '1000'
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
workload_identity_federation:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }} if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'workload_identity_federation' name: 'workload_identity_federation_through_service_account'
runs-on: '${{ matrix.os }}' runs-on: '${{ matrix.os }}'
strategy: strategy:
fail-fast: false fail-fast: false
@ -153,13 +141,11 @@ jobs:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}' workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
- id: 'setup-gcloud' - uses: 'google-github-actions/setup-gcloud@main'
name: 'setup-gcloud' with:
uses: 'google-github-actions/setup-gcloud@main' version: '>= 363.0.0'
- id: 'gcloud' - name: 'gcloud'
name: 'gcloud'
shell: 'bash'
run: |- run: |-
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}" gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
@ -171,9 +157,74 @@ jobs:
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
token_format: 'access_token' token_format: 'access_token'
- id: 'oauth-token'
name: 'oauth-token'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
--silent \
--show-error \
--fail \
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
- id: 'id-token'
name: 'id-token'
uses: './'
with:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true
#
# Service Account Key JSON
#
credentials_json:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'credentials_json'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
matrix:
os:
- 'ubuntu-latest'
- 'windows-latest'
- 'macos-latest'
steps:
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@v4'
with:
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
- id: 'auth-default'
name: 'auth-default'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
- uses: 'google-github-actions/setup-gcloud@main'
with:
version: '>= 363.0.0'
- name: 'gcloud'
run: |-
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
- id: 'auth-access-token'
name: 'auth-access-token'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'access_token'
- id: 'access-token' - id: 'access-token'
name: 'access-token' name: 'access-token'
shell: 'bash'
run: |- run: |-
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \ curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
--silent \ --silent \
@ -185,26 +236,26 @@ jobs:
name: 'auth-id-token' name: 'auth-id-token'
uses: './' uses: './'
with: with:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}' credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
token_format: 'id_token' token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/' id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true id_token_include_email: true
- id: 'auth-wif-retries' - id: 'auth-sa-retries'
name: 'auth-wif-retries' name: 'auth-sa-retries'
uses: './' uses: './'
with: with:
retries: '2' retries: '2'
backoff: '200' backoff: '200'
backoff_limit: '1000' backoff_limit: '1000'
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}' credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
#
# This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment # This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment
# variable is shared with the container and that the path of the file is on # variable is shared with the container and that the path of the file is on
# the shared filesystem with the container and that the USER for the container # the shared filesystem with the container and that the USER for the container
# has permissions to read the file. # has permissions to read the file.
#
docker: docker:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }} if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'docker' name: 'docker'

780
README.md
View File

@ -1,4 +1,4 @@
# auth # Authenticate to Google Cloud from GitHub Actions
This GitHub Action authenticates to Google Cloud. It supports authentication via This GitHub Action authenticates to Google Cloud. It supports authentication via
a Google Cloud Service Account Key JSON and authentication via [Workload a Google Cloud Service Account Key JSON and authentication via [Workload
@ -7,20 +7,12 @@ Identity Federation][wif].
Workload Identity Federation is recommended over Service Account Keys as it Workload Identity Federation is recommended over Service Account Keys as it
obviates the need to export a long-lived credential and establishes a trust obviates the need to export a long-lived credential and establishes a trust
delegation relationship between a particular GitHub Actions workflow invocation delegation relationship between a particular GitHub Actions workflow invocation
and permissions on Google Cloud. and permissions on Google Cloud. There are three ways to set up this GitHub
Action to authenticate to Google Cloud:
#### With Service Account Key JSON 1. [(Preferred) Direct Workload Identity Federation](#direct-wif)
1. [Workload Identity Federation through a Service Account](#indirect-wif)
1. Create a Google Cloud service account and grant IAM permissions 1. [Service Account Key JSON](#sake)
1. Export the long-lived JSON service account key
1. Upload the JSON service account key to a GitHub secret
#### With Workload Identity Federation
1. Create a Google Cloud service account and grant IAM permissions
1. Create and configure a Workload Identity Provider for GitHub
1. Exchange the GitHub Actions OIDC token for a short-lived Google Cloud access
token
**This is not an officially supported Google product, and it is not covered by a **This is not an officially supported Google product, and it is not covered by a
Google Cloud support contract. To report bugs or request features in a Google Google Cloud support contract. To report bugs or request features in a Google
@ -30,30 +22,25 @@ support](https://cloud.google.com/support).**
## Prerequisites ## Prerequisites
- For authenticating via Google Cloud Service Account Keys, you must create and - Run the `actions/checkout@v4` step _before_ this action. Omitting the
export a Google Cloud Service Account Key in JSON format. checkout step or putting it after `auth` will cause future steps to be
- For authenticating via Workload Identity Federation, you must create and
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
for instructions.
- You must run the `actions/checkout@v4` step _before_ this action. Omitting
the checkout step or putting it after `auth` will cause future steps to be
unable to authenticate. unable to authenticate.
- If you plan to create binaries, containers, pull requests, or other - To create binaries, containers, pull requests, or other releases, add the
releases, add the following to your `.gitignore` to prevent accidentially following to your `.gitignore` to prevent accidentially committing
committing credentials to your release artifact: credentials to your release artifact:
```text ```text
# Ignore generated credentials from google-github-actions/auth # Ignore generated credentials from google-github-actions/auth
gha-creds-*.json gha-creds-*.json
``` ```
- This action runs using Node 20. If you are using self-hosted GitHub Actions - To use the `bq` or `gsutil` tools, use the Google Cloud SDK version 390.0.0
runners, you must use a [runner or newer.
- This action runs using Node 20. Use a [runner
version](https://github.com/actions/virtual-environments) that supports this version](https://github.com/actions/virtual-environments) that supports this
version or newer. version of Node or newer.
## Usage ## Usage
@ -61,99 +48,102 @@ support](https://cloud.google.com/support).**
```yaml ```yaml
jobs: jobs:
job_id: job_id:
# ...
# Add "id-token" with the intended permissions. # Add "id-token" with the intended permissions.
permissions: permissions:
contents: 'read' contents: 'read'
id-token: 'write' id-token: 'write'
steps: steps:
# actions/checkout MUST come before auth
- uses: 'actions/checkout@v4' - uses: 'actions/checkout@v4'
- id: 'auth' - uses: 'google-github-actions/auth@v2'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with: with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# ... further steps are automatically authenticated
``` ```
Note that changing the `permissions` block may remove some default permissions. > **⚠️ NOTE!** Changing the `permissions` block may remove some default
See the [permissions documentation][github-perms] for more information. > permissions. See the [permissions documentation][github-perms] for more
> information.
See [Examples](#examples) for more examples. For help debugging common errors, see [Troubleshooting](docs/TROUBLESHOOTING.md) For more usage options, see the [examples](docs/EXAMPLES.md).
## Inputs ## Inputs
### Authenticating via Workload Identity Federation ### Inputs: Workload Identity Federation
> **⚠️ WARNING!** This option is [not supported by Firebase Admin
> SDK](https://github.com/firebase/firebase-admin-node/issues/1377). Use
> Service Account Key JSON authentication instead.
The following inputs are for _authenticating_ to Google Cloud via Workload The following inputs are for _authenticating_ to Google Cloud via Workload
Identity Federation. Identity Federation.
**⚠️ You must use the Cloud SDK version 390.0.0 or later to authenticate the - `workload_identity_provider`: (Required) The full identifier of the Workload
`bq` and `gsutil` tools.** Identity Provider, including the project number, pool name, and provider
name. If provided, this must be the full identifier which includes all
**⚠️ Firebase users:** If you're using this Action to authenticate the Firebase parts:
Admin Node.js SDK, you must authenticate with a service account key since
Workload Identity Federation is not yet supported. See
[#1377](https://github.com/firebase/firebase-admin-node/issues/1377)
for the status of WLIF support.
- `workload_identity_provider`: (Required) The full identifier of the Workload Identity
Provider, including the project number, pool name, and provider name. If
provided, this must be the full identifier which includes all parts:
```text ```text
projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider
``` ```
- `service_account`: (Required) Email address or unique identifier of the Google Cloud - `service_account`: (Optional) Email address or unique identifier of the
service account for which to generate credentials. For example: Google Cloud service account for which to impersonate and generate
credentials. For example:
```text ```text
my-service-account@my-project.iam.gserviceaccount.com my-service-account@my-project.iam.gserviceaccount.com
``` ```
Without this input, the GitHub Action will use [Direct Workload Identity
Federation](#direct-wif). If this input is provided, the GitHub Action will use
[Workload Identity Federation through a Service Account](#indirect-wif).
- `audience`: (Optional) The value for the audience (`aud`) parameter in the - `audience`: (Optional) The value for the audience (`aud`) parameter in the
generated GitHub Actions OIDC token. This value defaults to the value of generated GitHub Actions OIDC token. This value defaults to the value of
`workload_identity_provider`, which is also the default value Google Cloud `workload_identity_provider`, which is also the default value Google Cloud
expects for the audience parameter on the token. We do not recommend expects for the audience parameter on the token.
changing this value.
### Authenticating via Service Account Key JSON ### Inputs: Service Account Key JSON
> **⚠️ WARNING!** Service Account Key JSON credentials are long-lived
> credentials and must be treated like a password.
The following inputs are for _authenticating_ to Google Cloud via a Service The following inputs are for _authenticating_ to Google Cloud via a Service
Account Key JSON. **We recommend using Workload Identity Federation instead as Account Key JSON.
exporting a long-lived Service Account Key JSON credential poses a security
risk.**
- `credentials_json`: (Required) The Google Cloud JSON service account key to - `credentials_json`: (Required) The Google Cloud Service Account Key JSON to
use for authentication. To generate access tokens or ID tokens using this use for authentication.
service account, you must grant the underlying service account
`roles/iam.serviceAccountTokenCreator` permissions on itself.
Note: we strongly advise that you "compress" your JSON into a single line We advise minifying your JSON into a single line string before storing it in
string before storing it in a GitHub Secret. When a GitHub Secret is used in a GitHub Secret. When a GitHub Secret is used in a GitHub Actions workflow,
a GitHub Actions workflow, _each line_ of the secret is masked in log _each line_ of the secret is masked in log output. This can lead to
output. This can lead to aggressive sanitization of benign characters like aggressive sanitization of benign characters like curly braces (`{}`) and
curly braces (`{}`) and brackets (`[]`). See brackets (`[]`).
[Troubleshooting](docs/TROUBLESHOOTING.md#aggressive-replacement) for more
information.
### Generating OAuth 2.0 access tokens To generate access tokens or ID tokens using this service account, you must
grant the underlying service account `roles/iam.serviceAccountTokenCreator`
permissions on itself.
### Inputs: Generating OAuth 2.0 access tokens
The following inputs are for _generating_ OAuth 2.0 access tokens for The following inputs are for _generating_ OAuth 2.0 access tokens for
authenticating to Google Cloud as an output for use in future steps in the authenticating to Google Cloud as an output for use in future steps in the
workflow. These options only apply to access tokens generated by this action. By workflow. These options only apply to access tokens generated by this action. By
default, this action does not generate any tokens. default, this action does not generate any tokens.
- `token_format`: This value must be `"access_token"` to generate OAuth 2.0 - `service_account`: (Required) Email address or unique identifier of the
access tokens. To skip token generation, omit or set to the empty string "". Google Cloud service account for which to generate the access token. For
example:
```text
my-service-account@my-project.iam.gserviceaccount.com
```
- `token_format`: (Required) This value must be `"access_token"` to generate
OAuth 2.0 access tokens.
- `access_token_lifetime`: (Optional) Desired lifetime duration of the access - `access_token_lifetime`: (Optional) Desired lifetime duration of the access
token, in seconds. This must be specified as the number of seconds with a token, in seconds. This must be specified as the number of seconds with a
@ -188,15 +178,22 @@ default, this action does not generate any tokens.
You will also need to customize the `access_token_scopes` value to You will also need to customize the `access_token_scopes` value to
correspond to the OAuth scopes required for the API(s) you will access. correspond to the OAuth scopes required for the API(s) you will access.
### Generating ID tokens ### Inputs: Generating ID tokens
The following inputs are for _generating_ ID tokens for authenticating to Google The following inputs are for _generating_ ID tokens for authenticating to Google
Cloud as an output for use in future steps in the workflow. These options only Cloud as an output for use in future steps in the workflow. These options only
apply to ID tokens generated by this action. By default, this action does not apply to ID tokens generated by this action. By default, this action does not
generate any tokens. generate any tokens.
- `token_format`: This value must be `"id_token"` to generate ID tokens. To - `service_account`: (Required) Email address or unique identifier of the
skip token generation, omit or set to the empty string "". Google Cloud service account for which to generate the ID token. For
example:
```text
my-service-account@my-project.iam.gserviceaccount.com
```
- `token_format`: This value must be `"id_token"` to generate ID tokens.
- `id_token_audience`: (Required) The audience for the generated ID Token. - `id_token_audience`: (Required) The audience for the generated ID Token.
@ -205,7 +202,11 @@ generate any tokens.
will contain "email" and "email_verified" claims. This is only valid when will contain "email" and "email_verified" claims. This is only valid when
"token_format" is "id_token". The default value is false. "token_format" is "id_token". The default value is false.
### Retry inputs ### Inputs: Retry options
The following inputs are for controlling retry behavior. By default, this GitHub
Action will retry API calls in an attempt to reduce transient failures. You can
control and disable the retry behavior with these inputs.
- `retries`: (Optional) Number of times to retry a failed authentication - `retries`: (Optional) Number of times to retry a failed authentication
attempt. This is useful for automated pipelines that may execute before IAM attempt. This is useful for automated pipelines that may execute before IAM
@ -219,7 +220,7 @@ generate any tokens.
- `backoff_limit`: (Optional) Limits the retry backoff to the specified value. - `backoff_limit`: (Optional) Limits the retry backoff to the specified value.
The default value is no limit. The default value is no limit.
### Other inputs ### Inputs: Miscellaneous
The following inputs are for controlling the behavior of this GitHub Actions, The following inputs are for controlling the behavior of this GitHub Actions,
regardless of the authentication mechanism. regardless of the authentication mechanism.
@ -233,18 +234,18 @@ regardless of the authentication mechanism.
and Google Cloud SDKs in other steps in the workflow. The default is true. and Google Cloud SDKs in other steps in the workflow. The default is true.
The credentials file is exported into `$GITHUB_WORKSPACE`, which makes it The credentials file is exported into `$GITHUB_WORKSPACE`, which makes it
available to all future steps and filesystems (including Docker-based available to all future steps and filesystems (including Docker-based GitHub
GitHub Actions). The file is automatically removed at the end of the job Actions). The file is automatically removed at the end of the job via a post
via a post action. In order to use exported credentials, you **must** add action. In order to use exported credentials, you **must** add the
the `actions/checkout` step before calling `auth`. This is due to how `actions/checkout` step before calling `auth`. This is due to how GitHub
GitHub Actions creates `$GITHUB_WORKSPACE`: Actions creates `$GITHUB_WORKSPACE`:
```yaml ```yaml
jobs: jobs:
job_id: job_id:
steps: steps:
- uses: 'actions/checkout@v4' # Must come first! - uses: 'actions/checkout@v4' # Must come first!
- uses: 'google-github-actions/auth@v1' - uses: 'google-github-actions/auth@v2'
``` ```
- `export_environment_variables`: (Optional) If true, the action will export - `export_environment_variables`: (Optional) If true, the action will export
@ -257,7 +258,7 @@ regardless of the authentication mechanism.
- `GCLOUD_PROJECT` - `GCLOUD_PROJECT`
- `GOOGLE_CLOUD_PROJECT` - `GOOGLE_CLOUD_PROJECT`
If "create_credentials_file" is true, additional environment variables are If `create_credentials_file` is true, additional environment variables are
exported: exported:
- `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` - `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`
@ -284,318 +285,88 @@ regardless of the authentication mechanism.
credentials file resides. This is only available if credentials file resides. This is only available if
"create_credentials_file" was set to true. "create_credentials_file" was set to true.
- `auth_token`: The Google Cloud federated token (for Workload Identity
Federation) or self-signed JWT (for a Service Account Key JSON). This output
is always available.
- `access_token`: The Google Cloud access token for calling other Google Cloud - `access_token`: The Google Cloud access token for calling other Google Cloud
APIs. This is only available when "token_format" is "access_token". APIs. This is only available when "token_format" is "access_token".
- `access_token_expiration`: The RFC3339 UTC "Zulu" format timestamp for the
access token. This is only available when "token_format" is "access_token".
- `id_token`: The Google Cloud ID token. This is only available when - `id_token`: The Google Cloud ID token. This is only available when
"token_format" is "id_token". "token_format" is "id_token".
## Examples
### Authenticating via Workload Identity Federation
This example demonstrates authenticating via Workload Identity Federation. For
more information on how to setup and configure Workload Identity Federation, see
[#setup](#setup).
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
```
### Authenticating via Service Account Key JSON
This example demonstrates authenticating via a Google Cloud Service Account Key
JSON. **We recommend using Workload Identity Federation instead as exporting a
long-lived Service Account Key JSON credential poses a security risk.**
This example assumes you have created a GitHub Secret named 'GOOGLE_CREDENTIALS'
with the contents being an export Google Cloud Service Account Key JSON. See
[Creating and managing Google Cloud Service Account
Keys](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
for more information.
```yaml
jobs:
job_id:
# ...
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}'
```
### Authenticating to Container Registry and Artifact Registry
This example demonstrates authenticating to Google Container Registry (GCR) or
Google Artifact Registry (GAR). The most common way to authenticate to these
services is via a gcloud docker proxy. However, you can authenticate to these
registries directly using the `auth` action:
- **Username:** `oauth2accesstoken`
- **Password:** `${{ steps.auth.outputs.access_token }}`
You must set `token_format: access_token` in your Action YAML. Here are a few
examples:
```yaml
jobs:
job_id:
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: 'access_token'
# Either user Workload Identity Federation or Service Account Keys. See
# above more more examples
# This example uses the docker login action
- uses: 'docker/login-action@v1'
with:
registry: 'gcr.io' # or REGION-docker.pkg.dev
username: 'oauth2accesstoken'
password: '${{ steps.auth.outputs.access_token }}'
# This example runs "docker login" directly to Artifact Registry.
- run: |-
echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://REGION-docker.pkg.dev
# This example runs "docker login" directly to Container Registry.
- run: |-
echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://gcr.io
```
### Configuring gcloud
This example demonstrates using this GitHub Action to configure authentication
for the `gcloud` CLI tool.
**Warning!** Workload Identity Federation requires Cloud SDK (`gcloud`) version
[363.0.0](https://cloud.google.com/sdk/docs/release-notes#36300_2021-11-02) or
later.
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
# Configure Workload Identity Federation via a credentials file.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# Install gcloud, `setup-gcloud` automatically picks up authentication from `auth`.
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
# Now you can run gcloud commands authenticated as the impersonated service account.
- id: 'gcloud'
name: 'gcloud'
run: |-
gcloud secrets versions access "latest" --secret "my-secret"
```
### Generating an OAuth 2.0 Access Token
This example demonstrates using this GitHub Action to generate an OAuth 2.0
Access Token for authenticating to Google Cloud. Most Google Cloud APIs accept
this access token as authentication.
The default lifetime is 1 hour, but you can request up to 12 hours if you set
the [`constraints/iam.allowServiceAccountCredentialLifetimeExtension` organization policy][orgpolicy-creds-lifetime].
Note: If you authenticate via `credentials_json`, the service account must have
`roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
# Configure Workload Identity Federation and generate an access token.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: 'access_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
access_token_lifetime: '300s' # optional, default: '3600s' (1 hour)
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'access-secret'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
--header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}"
```
### Generating an ID Token (JWT)
This example demonstrates using this GitHub Action to generate a Google Cloud ID
Token for authenticating to Google Cloud. This is most commonly used when
invoking a Cloud Run service.
Note: If you authenticate via `credentials_json`, the service account must have
`roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
# Configure Workload Identity Federation and generate an access token.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: 'id_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
id_token_audience: 'https://myapp-uvehjacqzq.a.run.app' # required, value depends on target
id_token_include_email: true # optional
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'invoke-service'
run: |-
curl https://myapp-uvehjacqzq.a.run.app \
--header "Authorization: Bearer ${{ steps.auth.outputs.id_token }}"
```
<a id="setup"></a> <a id="setup"></a>
## Setup
## Setting up Workload Identity Federation This section describes the three configuration options:
To exchange a GitHub Actions OIDC token for a Google Cloud access token, you 1. [(Preferred) Direct Workload Identity Federation](#direct-wif)
must create and configure a Workload Identity Provider. These instructions use 1. [Workload Identity Federation through a Service Account](#indirect-wif)
the [gcloud][gcloud] command-line tool. 1. [Service Account Key JSON](#sake)
Alternatively, you can also use the [gh-oidc](https://github.com/terraform-google-modules/terraform-google-github-actions-runners/tree/master/modules/gh-oidc) > **⚠️ NOTE!** It can take up to 5 minutes for Workload Identity Pools, Workload
Terraform module to automate your infrastructure provisioning. See [examples](https://github.com/terraform-google-modules/terraform-google-github-actions-runners/tree/master/examples/oidc-simple) for usage. > Identity Providers, and IAM permissions to propagate. Please wait at least
> five minutes and follow all [Troubleshooting steps](docs/TROUBLESHOOTING.md)
> before opening an issue.
1. Create or use an existing Google Cloud project. You must have privileges to
create Workload Identity Pools, Workload Identity Providers, and to manage
Service Accounts and IAM permissions. Save your project ID as an environment
variable. The rest of these steps assume this environment variable is set:
```sh <a name="direct-wif" id="direct-wif"></a>
export PROJECT_ID="my-project" # update with your value ### (Preferred) Direct Workload Identity Federation
```
1. (Optional) Create a Google Cloud Service Account. If you already have a In this setup, the Workload Identity Pool has direct IAM permissions on Google
Service Account, take note of the email address and skip this step. Cloud resources; there are no intermediate service accounts or keys. This is
preferred since it directly authenticates GitHub Actions to Google Cloud without
a proxy resource. However, not all Google Cloud resources support `principalSet`
identities. Please see the documentation for your Google Cloud service for more
information.
```sh [![Authenticate to Google Cloud from GitHub Actions with Direct Workload Identity Federation](docs/google-github-actions-auth-direct-workload-identity-federation.svg)](docs/google-github-actions-auth-direct-workload-identity-federation.svg)
gcloud iam service-accounts create "my-service-account" \
--project "${PROJECT_ID}"
```
1. (Optional) Grant the Google Cloud Service Account permissions to access > **⚠️ NOTE!** To generate OAuth 2.0 access tokens or ID tokens, you _must_
Google Cloud resources. This step varies by use case. For demonstration > provide a service account email, and the Workload Identity Pool must have
purposes, you could grant access to a Google Secret Manager secret or Google > `roles/iam.workloadIdentityUser` permissions on the target Google Cloud
Cloud Storage object. > Service Account. Follow the steps for Workload Identity Federation through a
> Service Account instead.
1. Enable the IAM Credentials API: <details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a direct Workload Identity Federation.</summary>
```sh These instructions use the [gcloud][gcloud] command-line tool.
gcloud services enable iamcredentials.googleapis.com \
--project "${PROJECT_ID}"
```
1. Create a Workload Identity Pool: 1. Create a Workload Identity Pool:
```sh ```sh
gcloud iam workload-identity-pools create "my-pool" \ gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \ --project="${PROJECT_ID}" \
--location="global" \ --location="global" \
--display-name="Demo pool" --display-name="GitHub Actions Pool"
``` ```
1. Get the full ID of the Workload Identity **Pool**: 1. Get the full ID of the Workload Identity **Pool**:
```sh ```sh
gcloud iam workload-identity-pools describe "my-pool" \ gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \ --project="${PROJECT_ID}" \
--location="global" \ --location="global" \
--format="value(name)" --format="value(name)"
``` ```
Save this value as an environment variable: This value should be of the format:
```sh ```text
export WORKLOAD_IDENTITY_POOL_ID="..." # value from above projects/123456789/locations/global/workloadIdentityPools/github
# This should look like:
#
# projects/123456789/locations/global/workloadIdentityPools/my-pool
#
``` ```
1. Create a Workload Identity **Provider** in that pool: 1. Create a Workload Identity **Provider** in that pool:
```sh ```sh
gcloud iam workload-identity-pools providers create-oidc "my-provider" \ gcloud iam workload-identity-pools providers create-oidc "my-repo" \
--project="${PROJECT_ID}" \ --project="${PROJECT_ID}" \
--location="global" \ --location="global" \
--workload-identity-pool="my-pool" \ --workload-identity-pool="github" \
--display-name="Demo provider" \ --display-name="My GitHub repo Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \ --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com" --issuer-uri="https://token.actions.githubusercontent.com"
``` ```
@ -605,22 +376,140 @@ Terraform module to automate your infrastructure provisioning. See [examples](ht
the principal invoking the GitHub Action). These can be used to further the principal invoking the GitHub Action). These can be used to further
restrict the authentication using `--attribute-condition` flags. restrict the authentication using `--attribute-condition` flags.
The example above only maps the `actor` and `repository` values. To map > **❗️ NOTE!** You must map any claims in the incoming token to attributes
additional values, add them to the attribute map: > before you can assert on those attributes in a CEL expression or IAM
> policy!**
1. Extract the Workload Identity **Provider** resource name:
```sh ```sh
--attribute-mapping="google.subject=assertion.sub,attribute.repository_owner=assertion.repository_owner" gcloud iam workload-identity-pools providers describe "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github" \
--format="value(name)"
``` ```
**You must map any claims in the incoming token to attributes before you can Use this value as the `workload_identity_provider` value in the GitHub
assert on those attributes in a CEL expression or IAM policy!** Actions YAML:
1. Allow authentications from the Workload Identity Provider originating from ```yaml
your repository to impersonate the Service Account created above: - uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
```
> **⚠️ NOTE!** The `project_id` input is optional, but may be required by
> downstream authentication systems such as the `gcloud` CLI. Unfortunately
> we cannot extract the project ID from the Workload Identity Provider,
> since it requires the project _number_.
>
> It is technically possible to convert a project _number_ into a project
> _ID_, but it requires permissions to call Cloud Resource Manager, and we
> cannot guarantee that the Workload Identity Pool has those permissions.
1. As needed, allow authentications from the Workload Identity Pool to Google
Cloud resources. These can be any Google Cloud resources that support
federated ID tokens, and it can be done after the GitHub Action is
configured.
The following example shows granting access from a GitHub Action in a
specific repository a secret in Google Secret Manager.
```sh ```sh
# TODO(developer): Update this value to your GitHub repository. # TODO(developer): Update this value to your GitHub repository.
export REPO="username/name" # e.g. "google/chrome" export REPO="username/name" # e.g. "google/chrome"
export WORKLOAD_IDENTITY_POOL_ID="value/from/above" # e.g. "projects/123456789/locations/global/workloadIdentityPools/github"
gcloud secrets add-iam-policy-binding "my-secret" \
--project="${PROJECT_ID}" \
--role="roles/secretmanager.secretAccessor" \
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
```
Review the [GitHub documentation][github-oidc] for a complete list of
options and values. This GitHub repository does not seek to enumerate every
possible combination.
</details>
<a name="indirect-wif" id="indirect-wif"></a>
### Workload Identity Federation through a Service Account
In this setup, the Workload Identity Pool impersonates a Google Cloud Service
Account which has IAM permissions on Google Cloud resources. This exchanges the
GitHub Actions OIDC token with a Google Cloud OAuth 2.0 access token by granting
GitHub Actions permissions to mint tokens for the given Service Account. Thus
GitHub Actions inherits that Service Account's permissions by proxy.
[![Authenticate to Google Cloud from GitHub Actions with Workload Identity Federation through a Service Account](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)
<details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Workload Identity Federation through a Service Account.</summary>
These instructions use the [gcloud][gcloud] command-line tool.
1. (Optional) Create a Google Cloud Service Account. If you already have a
Service Account, take note of the email address and skip this step.
```sh
gcloud iam service-accounts create "my-service-account" \
--project "${PROJECT_ID}"
```
1. Create a Workload Identity Pool:
```sh
gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="GitHub Actions Pool"
```
1. Get the full ID of the Workload Identity **Pool**:
```sh
gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \
--location="global" \
--format="value(name)"
```
This value should be of the format:
```text
projects/123456789/locations/global/workloadIdentityPools/github
```
1. Create a Workload Identity **Provider** in that pool:
```sh
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github" \
--display-name="My GitHub repo Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com"
```
The attribute mappings map claims in the GitHub Actions JWT to assertions
you can make about the request (like the repository or GitHub username of
the principal invoking the GitHub Action). These can be used to further
restrict the authentication using `--attribute-condition` flags.
> **❗️ NOTE!** You must map any claims in the incoming token to attributes
> before you can assert on those attributes in a CEL expression or IAM
> policy!**
1. Allow authentications from the Workload Identity Pool to your Google Cloud
Service Account.
```sh
# TODO(developer): Update this value to your GitHub repository.
export REPO="username/name" # e.g. "google/chrome"
export WORKLOAD_IDENTITY_POOL_ID="value/from/above" # e.g. "projects/123456789/locations/global/workloadIdentityPools/github"
gcloud iam service-accounts add-iam-policy-binding "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com" \ gcloud iam service-accounts add-iam-policy-binding "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com" \
--project="${PROJECT_ID}" \ --project="${PROJECT_ID}" \
@ -628,130 +517,93 @@ Terraform module to automate your infrastructure provisioning. See [examples](ht
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}" --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
``` ```
If you want to admit all repos of an owner (user or organization), map on `attribute.repository_owner`: Review the [GitHub documentation][github-oidc] for a complete list of
options and values. This GitHub repository does not seek to enumerate every
```sh possible combination.
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository_owner/${OWNER}"
```
For this to work, you need to make sure that `attribute.repository_owner` is mapped in your attribute mapping (see previous step).
Note that `$WORKLOAD_IDENTITY_POOL_ID` should be the **full** Workload
Identity Pool resource ID, like:
```text
projects/123456789/locations/global/workloadIdentityPools/my-pool
```
1. Extract the Workload Identity **Provider** resource name: 1. Extract the Workload Identity **Provider** resource name:
```sh ```sh
gcloud iam workload-identity-pools providers describe "my-provider" \ gcloud iam workload-identity-pools providers describe "my-repo" \
--project="${PROJECT_ID}" \ --project="${PROJECT_ID}" \
--location="global" \ --location="global" \
--workload-identity-pool="my-pool" \ --workload-identity-pool="github" \
--format="value(name)" --format="value(name)"
``` ```
Use this value as the `workload_identity_provider` value in your GitHub Use this value as the `workload_identity_provider` value in the GitHub
Actions YAML. Actions YAML:
1. Use this GitHub Action with the Workload Identity Provider ID and Service ```yaml
Account email. The GitHub Action will mint a GitHub OIDC token and exchange - uses: 'google-github-actions/auth@v2'
the GitHub token for a Google Cloud access token (assuming the authorization with:
is correct). This all happens without exporting a Google Cloud service workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
account key JSON! ```
Note: It can take **up to 5 minutes** from when you configure the Workload 1. As needed, grant the Google Cloud Service Account permissions to access
Identity Pool mapping until the permissions are available. Google Cloud resources. This step varies by use case. The following example
shows granting access to a secret in Google Secret Manager.
```sh
gcloud secrets add-iam-policy-binding "my-secret" \
--project="${PROJECT_ID}" \
--role="roles/secretmanager.secretAccessor" \
--member="serviceAccount:my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
```
</details>
#### Organizational Policy Constraints <a name="sake" id="sake"></a>
### Service Account Key JSON
By default, Google Cloud allows you to create Workload Identity Pools and In this setup, a Service Account has direct IAM permissions on Google Cloud
Workload Identity Providers for any endpoints. Your organization may restrict resources. You download a Service Account Key JSON file and upload it to GitHub
which external identity providers are permitted on your Google Cloud account. To as a secret.
enable GitHub Actions as a Workload Identity Pool and Provider, add the
`https://token.actions.githubusercontent.com` to the allowed
`iam.workloadIdentityPoolProviders` Org Policy constraint.
```shell [![Authenticate to Google Cloud from GitHub Actions with a Service Account Key](docs/google-github-actions-auth-service-account-key-export.svg)](docs/google-github-actions-auth-service-account-key-export.svg)
gcloud resource-manager org-policies allow "constraints/iam.workloadIdentityPoolProviders" \
https://token.actions.githubusercontent.com
```
You can specify a `--folder` or `--organization`. If you do not have permission > **❗️ WARNING!** Google Cloud Service Account Key JSON files must be secured
to manage these Org Policies, please contact your Google Cloud administrator. > and treated like a password. Anyone with acess to the JSON key can
> authenticate to Google Cloud as the underlying Service Account. By default,
> these credentials never expire, which is why the former authentication options
> are much preferred.
For GitHub Enterprise Server, the endpoint will be your server URL: <details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Service Account Key JSON.</summary>
```shell These instructions use the [gcloud][gcloud] command-line tool.
gcloud resource-manager org-policies allow "constraints/iam.workloadIdentityPoolProviders" \
https://my.github.company
```
1. (Optional) Create a Google Cloud Service Account. If you already have a
Service Account, take note of the email address and skip this step.
## GitHub Token Format ```sh
gcloud iam service-accounts create "my-service-account" \
--project "${PROJECT_ID}"
```
Below is a sample GitHub Token for reference for attribute mappings. For a list of all 1. Create a Service Account Key JSON for the Service Account.
mappings, see the [GitHub OIDC token documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
```json ```sh
{ gcloud iam service-accounts keys create "key.json" \
"jti": "...", --iam-account "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
"sub": "repo:username/reponame:ref:refs/heads/main", ```
"aud": "https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider",
"ref": "refs/heads/main",
"sha": "d11880f4f451ee35192135525dc974c56a3c1b28",
"repository": "username/reponame",
"repository_owner": "username",
"repository_visibility": "private",
"repository_id": "74",
"repository_owner_id": "65",
"run_id": "1238222155",
"run_number": "18",
"run_attempt": "1",
"actor": "username",
"actor_id": "12",
"workflow": "oidc",
"head_ref": "",
"base_ref": "",
"event_name": "push",
"ref_type": "branch",
"job_workflow_ref": "username/reponame/.github/workflows/token.yml@refs/heads/main",
"iss": "https://token.actions.githubusercontent.com",
"nbf": 1631718827,
"exp": 1631719727,
"iat": 1631719427
}
```
1. Upload the contents of this file as a [GitHub Actions
Secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
## Versioning Use the name of the GitHub Actios secret as the `credentials_json` value in
the GitHub Actions YAML:
We recommend pinning to the latest available major version: ```yaml
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' # Replace with the name of your GitHub Actions secret
```
</details>
```yaml
- uses: 'google-github-actions/auth@v1'
```
While this action attempts to follow semantic versioning, but we're ultimately
human and sometimes make mistakes. To prevent accidental breaking changes, you
can also pin to a specific version:
```yaml
- uses: 'google-github-actions/auth@v1.0.0'
```
However, you will not get automatic security updates or new features without
explicitly updating your version number. Note that we only publish `MAJOR` and
`MAJOR.MINOR.PATCH` versions. There is **not** a floating alias for
`MAJOR.MINOR`.
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation
[gcloud]: https://cloud.google.com/sdk
[map-external]: https://cloud.google.com/iam/docs/access-resources-oidc#impersonate
[github-perms]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
[dwd]: https://developers.google.com/admin-sdk/directory/v1/guides/delegation [dwd]: https://developers.google.com/admin-sdk/directory/v1/guides/delegation
[orgpolicy-creds-lifetime]: https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints [gcloud]: https://cloud.google.com/sdk
[github-oidc]: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token
[github-perms]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
[map-external]: https://cloud.google.com/iam/docs/access-resources-oidc#impersonate
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation

View File

@ -168,14 +168,14 @@ outputs:
description: |- description: |-
Path on the local filesystem where the generated credentials file resides. Path on the local filesystem where the generated credentials file resides.
This is only available if "create_credentials_file" was set to true. This is only available if "create_credentials_file" was set to true.
auth_token:
description: |-
The intermediate authentication token, which could be used to call other
Google Cloud APIs, depending on how you configured IAM.
access_token: access_token:
description: |- description: |-
The Google Cloud access token for calling other Google Cloud APIs. This is The Google Cloud access token for calling other Google Cloud APIs. This is
only available when "token_format" is "access_token". only available when "token_format" is "access_token".
access_token_expiration:
description: |-
The RFC3339 UTC "Zulu" format timestamp for the access token. This is only
available when "token_format" is "access_token".
id_token: id_token:
description: |- description: |-
The Google Cloud ID token. This is only available when "token_format" is The Google Cloud ID token. This is only available when "token_format" is

6
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

6
dist/post/index.js vendored

File diff suppressed because one or more lines are too long

187
docs/EXAMPLES.md Normal file
View File

@ -0,0 +1,187 @@
# Examples for Authenticating to Google Cloud from GitHub Actions
> Consider using the [Markdown TOC][github-markdown-toc] to make browsing these
> samples easier.
These examples assume you have completed all corresponding [Setup
Instructions](../README.md#setup).
## Direct Workload Identity Federation
This example shows authenticating directly with Workload Identity Federation.
Google Cloud Resources must have the Workload Identity Pool as a `principalSet`
as an IAM permission.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
# Use 'steps.auth.outputs.auth_token' in subsequent steps as a bearer token.
#
# - run: |-
# curl -H 'Bearer: ${{ steps.auth.outputs.auth_token }}' https://...
#
```
## Workload Identity Federation through a Service Account
This example shows authenticating to Google Cloud by proxying through a Service
Account. Future authentication calls will be made with the Service Account's
OAuth 2.0 Access token.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
- uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# NOTE: 'steps.auth.outputs.auth_token' will be a federated authentication
# token, it does not correspond to the service account. To get a token for
# the service account, specify the 'token_format' parameter and use the
# 'accesss_token' output.
#
# - uses: 'google-github-actions/auth@v2'
# with:
# workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
# service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# token_format: 'access_token'
#
# - run: |-
# curl -H 'Bearer: ${{ steps.auth.outputs.access_token }}' https://...
#
```
## Service Account Key JSON
This example demonstrates authenticating via a Google Cloud Service Account Key
JSON. After you [export a Google Cloud Service Account Key][sake], insert the
value into a GitHub Secret named 'GOOGLE_CREDENTIALS'.
```yaml
jobs:
job_id:
steps:
- uses: 'actions/checkout@v4'
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}'
```
### Configuring gcloud
This example demonstrates using this GitHub Action to configure authentication
for the `gcloud` CLI tool.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
```
### Generating an OAuth 2.0 Access Token
This example demonstrates using this GitHub Action to generate an OAuth 2.0
Access Token for authenticating to Google Cloud.
> **⚠️ NOTE!** The default lifetime is 1 hour, but you can request up to 12
> hours if you set the
> [`constraints/iam.allowServiceAccountCredentialLifetimeExtension` organization
> policy][orgpolicy-creds-lifetime].
> **⚠️ NOTE!** If you authenticate via `credentials_json`, the service account
> must have `roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
token_format: 'access_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
access_token_lifetime: '300s' # optional, default: '3600s' (1 hour)
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'access-secret'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
--header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}"
```
### Generating an ID Token (JWT)
This example demonstrates using this GitHub Action to generate a Google Cloud ID
Token for authenticating to Google Cloud. This is commonly used when invoking a
Cloud Run service.
> **⚠️ NOTE!** If you authenticate via `credentials_json`, the service account
> must have `roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
token_format: 'id_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
id_token_audience: 'https://myapp-uvehjacqzq.a.run.app' # required, value depends on target
id_token_include_email: true
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'invoke-service'
run: |-
curl https://myapp-uvehjacqzq.a.run.app \
--header "Authorization: Bearer ${{ steps.auth.outputs.id_token }}"
```
[github-markdown-toc]: https://github.blog/changelog/2021-04-13-table-of-contents-support-in-markdown-files/
[orgpolicy-creds-lifetime]: https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints
[sake]: https://cloud.google.com/iam/docs/creating-managing-service-account-keys

View File

@ -2,46 +2,33 @@
## Permission denied ## Permission denied
When troubleshooting "permission denied" errors from `auth` for Workload
Identity, the first step is to ask the `auth` plugin to generate an OAuth access
token. Do this by adding `token_format: 'access_token'` to your YAML:
```yaml
- uses: 'google-github-actions/auth@v1'
with:
# ...
token_format: 'access_token'
```
If your workflow _succeeds_ after adding the step to generate an access token,
it means Workload Identity Federation is configured correctly and the issue is
in subsequent actions. You can remove the `token_format` from your YAML. To
further debug:
1. Enable [GitHub Actions debug logging][debug-logs] and re-run the workflow to 1. Enable [GitHub Actions debug logging][debug-logs] and re-run the workflow to
see exactly which step is failing. Ensure you are using the latest version see exactly which step is failing. Ensure you are using the latest version
of that GitHub Action. of the GitHub Action.
1. Make sure you use `actions/checkout@v4` **before** the `auth` action in your > **❗️ WARNING!** Enabling debug logging increases the chances of a secret
> being accidentially logged. While GitHub Actions will scrub secrets,
> please take extra caution when sharing these debug logs in publicly
> accessible places like GitHub issues.
>
> If you do not feel comfortable attaching the debug logs to a GitHub issue,
> please create the issue and then email the debug logs to
> google-github-actions@google.com, including the GitHub issue number in the
> subject line and email body.
1. Ensure you have waited at least 5 minutes between making changes to the
Workload Identity Pool, Workload Identity Provider, or IAM policies. Changes
to these resources are eventually consistent. Usually they happen
immediately, but sometimes they can take up to 5 minutes to propagate.
1. Ensure `actions/checkout@v4` is **before** the `auth` action in your
workflow. workflow.
1. If the failing action is from `google-github-action/*`, please file an issue ```yaml
in the corresponding repository. steps:
- uses: 'actions/checkout@v4'
1. If the failing action is from an external action, please file an issue - uses: 'google-github-actions/auth@v2'
against that repository. The `auth` action exports Google Application ```
Default Credentials (ADC). Ask the action author to ensure they are
processing ADC correctly and using the latest versions of the Google client
libraries. Please note that we do not have control over actions outside of
`google-github-actions`.
If your workflow _fails_ after adding the step to generate an access token,
it likely means there is a misconfiguration with Workload Identity. Here are
some common sources of errors:
1. Enable [GitHub Actions debug logging][debug-logs] and re-run the workflow to
see exactly which step is failing. Ensure you are using the latest version
of that GitHub Action.
1. Ensure the value for `workload_identity_provider` is the full _Provider_ 1. Ensure the value for `workload_identity_provider` is the full _Provider_
name, **not** the _Pool_ name: name, **not** the _Pool_ name:
@ -55,8 +42,13 @@ some common sources of errors:
**number**. Workload Identity Federation does not accept Google Cloud **number**. Workload Identity Federation does not accept Google Cloud
Project IDs. Project IDs.
1. Ensure that you have the correct `permissions:` for the job in your workflow, per ```diff
the [usage](../README.md#usage) docs, i.e. - projects/my-project/locations/global/workloadIdentityPools/my-pool/providers/my-provider
+ projects/1234567890/locations/global/workloadIdentityPools/my-pool/providers/
```
1. Ensure that you have the correct `permissions:` for the job in your
workflow, per the [usage](../README.md#usage) docs:
```yaml ```yaml
permissions: permissions:
@ -67,43 +59,47 @@ some common sources of errors:
1. Ensure you have created an **Attribute Mapping** for any **Attribute 1. Ensure you have created an **Attribute Mapping** for any **Attribute
Conditions** or **Service Account Impersonation** principals. You cannot Conditions** or **Service Account Impersonation** principals. You cannot
create an Attribute Condition unless you map that value from the incoming create an Attribute Condition unless you map that value from the incoming
GitHub OIDC token. You cannot grant permissions to impersonate a Service GitHub OIDC token. You cannot grant permissions on an attribute unless you
Account on an attribute unless you map that value from the incoming GitHub map that value from the incoming GitHub OIDC token.
OIDC token.
You can use the [GitHub Actions OIDC Debugger][oidc-debugger] to print the > **📝 TIP!** Use the [GitHub Actions OIDC Debugger][oidc-debugger] to print
list of token claims and compare them to your Attribute Mappings and > the list of token claims and compare them to your Attribute Mappings and
Attribute Conditions. > Attribute Conditions.
1. Ensure you have the correct casing and capitalization. GitHub does not 1. Ensure you have the correct character casing and capitalization. GitHub does
distinguish between "foobar" and "FooBar", but Google Cloud does. Ensure any not distinguish between "foobar" and "FooBar", but Google Cloud does. Ensure
**Attribute Conditions** use the correct capitalization. any **Attribute Conditions** use the correct capitalization. The
capitalization must match what is in the GitHub Actions OIDC token.
1. Check the specific error message that is returned. 1. Check the specific error message that is returned.
- If the error message includes "failed to generate Google Cloud federated - If the error message includes "Failed to generate Google Cloud federated
token", it means admission into the Workload Identity Pool failed. Check token", it means admission into the Workload Identity Pool failed. Check
your [**Attribute Conditions**][attribute-conditions]. your [**Attribute Conditions**][attribute-conditions].
- If the error message inclues "failed to generate Google Cloud access - If the error message inclues "Failed to generate OAuth 2.0 Access
token", it means Service Account Impersonation failed. Check your Token", it means Service Account Impersonation failed. Check your
[**Service Account Impersonation**][sa-impersonation] settings and [**Service Account Impersonation**][sa-impersonation] settings and
ensure the principalSet is correct. ensure the principalSet is correct.
1. Enable `Admin Read`, `Data Read`, and `Data Write` [Audit Logging][cal] for 1. Enable `Admin Read`, `Data Read`, and `Data Write` [Audit Logging][cal] for
Identity and Access Management (IAM) in your Google Cloud project. Identity and Access Management (IAM) in your Google Cloud project.
**Warning!** This will increase log volume which may increase costs. To keep > **❗️ WARNING!** This will increase log volume which may increase costs.
costs low, you can disable this audit logging after you have debugged the > You can disable this audit logging after you have debugged the issue.
issue.
Try to authenticate again, and then explore the logs for your Workload Try to authenticate again, and then explore the logs for your Workload
Identity Provider and Workload Identity Pool. Sometimes these error messages Identity Provider and Workload Identity Pool. Sometimes these error messages
are helpful in identifying the root cause. are helpful in identifying the root cause.
1. Ensure you have waited at least 5 minutes between making changes to the 1. If failures are coming from a different GitHub Action step, please file an
Workload Identity Pool and Workload Identity Provider. Changes to these issue against that repository. The `auth` action exports Google Application
resources are eventually consistent. Default Credentials (ADC). Ask the action author to ensure they are
processing ADC correctly and using the latest versions of the Google client
libraries.
> **⚠️ NOTE!** We do not have control over GitHub Actions outside of the
> `google-github-actions` GitHub organization.
## Subject exceeds the 127 byte limit ## Subject exceeds the 127 byte limit
@ -234,6 +230,33 @@ tool like `jq`:
cat credentials.json | jq -r tostring cat credentials.json | jq -r tostring
``` ```
## Organizational Policy Constraints
**⚠️ NOTE!** Your Google Cloud organization administrator controls these
policies. You must work with your internal IT department to resolve OrgPolicy
violations and constraints.
### Workload Identity Providers
Your organization may restrict which external identity providers are permitted
on your Google Cloud account. To enable GitHub Actions as a Workload Identity
Pool and Provider, add the `https://token.actions.githubusercontent.com` to the
allowed `iam.workloadIdentityPoolProviders` Org Policy constraint.
```shell
gcloud resource-manager org-policies allow "constraints/iam.workloadIdentityPoolProviders" \
https://token.actions.githubusercontent.com
```
### Service Account Key Export
Your organization may restrict exporting Service Account Keys. To enable Service
Account Key export, set the `iam.disableServiceAccountCreation` to false.
```shell
gcloud resource-manager org-policies disable-enforce "constraints/iam.disableServiceAccountCreation"
```
[attribute-conditions]: https://cloud.google.com/iam/docs/workload-identity-federation#conditions [attribute-conditions]: https://cloud.google.com/iam/docs/workload-identity-federation#conditions
[sa-impersonation]: https://cloud.google.com/iam/docs/workload-identity-federation#impersonation [sa-impersonation]: https://cloud.google.com/iam/docs/workload-identity-federation#impersonation

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 771 365">
<style>
path {
fill: #333;
stroke: #333;
stroke-width: 0.5;
}
@media (prefers-color-scheme: dark) {
path {
fill: #ccc;
stroke: #ccc;
}
}
</style>
<path d="M2.9 13.3V6.4h3.8v1h-3v6z"/>
<path d="M2.9 26.3v-14h.8v14z"/>
<path d="M2.9 39.3v-14h.8v14z"/>
<path d="M2.9 52.3v-14h.8v14z"/>
<path d="M2.9 65.3v-14h.8v14z"/>
<path d="M2.9 78.3v-14h.8v14z"/>
<path d="M2.9 91.3v-14h.8v14z"/>
<path d="M2.9 104.3v-14h.8v14z"/>
<path d="M2.9 117.3v-14h.8v14z"/>
<path d="M2.9 130.3v-14h.8v14z"/>
<path d="M2.9 143.3v-14h.8v14z"/>
<path d="M2.9 156.3v-14h.8v14z"/>
<path d="M2.9 169.3v-14h.8v14z"/>
<path d="M2.9 182.3v-14h.8v14z"/>
<path d="M2.9 195.3v-14h.8v14z"/>
<path d="M2.9 208.3v-14h.8v14z"/>
<path d="M2.9 221.3v-14h.8v14z"/>
<path d="M2.9 234.3v-14h.8v14z"/>
<path d="M2.9 247.3v-14h.8v14z"/>
<path d="M2.9 260.3v-14h.8v14z"/>
<path d="M2.9 273.3v-14h.8v14z"/>
<path d="M2.9 286.3v-14h.8v14z"/>
<path d="M2.9 299.3v-14h.8v14z"/>
<path d="M2.9 312.3v-14h.8v14z"/>
<path d="M2.9 325.3v-14h.8v14z"/>
<path d="M2.9 338.3v-14h.8v14z"/>
<path d="M2.9 351.3v-14h.8v14z"/>
<path d="M2.9 358.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm38 0v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm38 0v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zM20 347.3l-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1H20zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4H48v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8T65 344q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4H76v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7z"/>
<path d="M233.9 351.3v-14h.8v14zm35 0v-14h.8v14zm17.1-4-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm2.7-5.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm4.4-.3h1.1v3h3v-3h1v8h-1v-4h-3v4h-1zm7.3 5.7V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11-.7q0-1.2-.4-1.7-.4-.6-1.1-.6-.7 0-1.1.6-.4.6-.4 1.7t.4 1.7q.4.6 1 .6.8 0 1.2-.6.3-.6.3-1.7zm-3-2.2q.3-.5.7-.7.4-.3 1-.3 1 0 1.6.9.7.8.7 2.3 0 1.5-.7 2.3-.6.9-1.7.9-.5 0-1-.3l-.6-.7v.8h-1v-8.4h1zm182.9 8.5v-14h.8v14zm35 0v-14h.8v14zm17.1-4-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm141.2 6.3v-14h.8v14zm-735-312v-6.9h3.8v1h-3v6z"/>
<path d="M30.9 52.3v-14h.8v14z"/>
<path d="M30.9 65.3v-14h.8v14z"/>
<path d="M30.9 78.3v-14h.8v14z"/>
<path d="M30.9 91.3v-14h.8v14z"/>
<path d="M30.9 104.3v-14h.8v14z"/>
<path d="M30.9 117.3v-14h.8v14z"/>
<path d="M30.9 130.3v-14h.8v14z"/>
<path d="M30.9 137.3v-8h.8v7.1h3v1zm0 84v-6.9h3.8v1h-3v6z"/>
<path d="M30.9 234.3v-14h.8v14z"/>
<path d="M30.9 247.3v-14h.8v14z"/>
<path d="M30.9 260.3v-14h.8v14z"/>
<path d="M30.9 273.3v-14h.8v14z"/>
<path d="M30.9 286.3v-14h.8v14z"/>
<path d="M30.9 299.3v-14h.8v14z"/>
<path d="M30.9 312.3v-14h.8v14z"/>
<path d="M30.9 319.3v-8h.8v7.1h3v1zm203 19v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M233.9 325.3v-14h.8v14zm35 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm35 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm-711.5-63v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L51 270q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm151.5 24v-14h.8v14z"/>
<path d="M205.9 299.3v-14h.8v14z"/>
<path d="M205.9 312.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M233.9 299.3v-14h.8v14z"/>
<path d="M233.9 312.3v-14h.8v14zm35-26v-14h.8v14z"/>
<path d="M268.9 299.3v-14h.8v14z"/>
<path d="M268.9 312.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M296.9 299.3v-14h.8v14z"/>
<path d="M296.9 312.3v-14h.8v14zm175-26v-14h.8v14z"/>
<path d="M471.9 299.3v-14h.8v14z"/>
<path d="M471.9 312.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M499.9 299.3v-14h.8v14z"/>
<path d="M499.9 312.3v-14h.8v14zm14-19v-8h.8v7.1h3v1zm21 19v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-241-19v-.9h6.8v1z"/>
<path d="M534.9 299.3v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V290l6.6 3.2zm9.9 2.9v-14h.8v14zm76.1-5.5h2v2.2h-2zm7 0h2v2.2h-2zm7 0h2v2.2h-2zm84.9 5.5v-14h.8v14zm28 0v-14h.8v14z"/>
<path d="M534.9 286.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zM62 266.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm12.3-3.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H107l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-5q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V264h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V264h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm11.4-.3h1.1v3.5l3.4-3.5h1.3l-3.1 3.3 3.2 4.7h-1.3l-2.6-4-.9.9v3.1h-1zm12.3 4.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6 1.8-.6 1.7-.7 1.8q-.3.4-.7.6-.3.2-.8.2h-.8v-.8h.6q.4 0 .7-.3l.6-1.3-2.3-6h1l1.8 4.8 1.7-4.7h1zm26.3 5.3v-14h.8v14zm4-6v-.9h6.8v1zm14 0v-.9h6.8v1zm10 6v-14h.8v14zm4-6v-.9h6.8v1zm14 0v-.9h6.8v1zm17.8 0v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm10.2 0v-.9h6.8v1zm7.1 3.1V264l6.6 3.2zm9.9 2.9v-14h.8v14zm45.1-4-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm2.7-5.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm4.4-.3h1.1v3h3v-3h1v8h-1v-4h-3v4h-1zm7.3 5.7V264h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V264h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11-.7q0-1.2-.4-1.7-.4-.6-1.1-.6-.7 0-1.1.6-.4.6-.4 1.7t.4 1.7q.4.6 1 .6.8 0 1.2-.6.3-.6.3-1.7zm-3-2.2q.3-.5.7-.7.4-.3 1-.3 1 0 1.6.9.7.8.7 2.3 0 1.5-.7 2.3-.6.9-1.7.9-.5 0-1-.3l-.6-.7v.8h-1v-8.4h1zm17.4-2.5v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.7 3.5-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.5-4.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm6.8 1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm5.3-3.9v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm8.9 1.9v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm39.7 9.1v-14h.8v14zm28 0v-14h.8v14zm14 0v-14h.8v14z"/>
<path d="M534.9 273.3v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm-560-39v-14h.8v14z"/>
<path d="M205.9 247.3v-14h.8v14z"/>
<path d="M205.9 260.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M233.9 247.3v-14h.8v14z"/>
<path d="M233.9 260.3v-14h.8v14zm35-26v-14h.8v14z"/>
<path d="M268.9 247.3v-14h.8v14z"/>
<path d="M268.9 260.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M296.9 247.3v-14h.8v14z"/>
<path d="M296.9 260.3v-14h.8v14zm175-26v-14h.8v14z"/>
<path d="M471.9 247.3v-14h.8v14z"/>
<path d="M471.9 260.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M499.9 247.3v-14h.8v14z"/>
<path d="M499.9 260.3v-14h.8v14zm14-13v-14h.8v14zm21 13v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="M534.9 247.3v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14z"/>
<path d="M534.9 234.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm14 0v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M534.9 221.3v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V212l6.6 3.2zm9.9 2.9v-14h.8v14zm23.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm17.7-4.7v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm4.9 0v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm7 2.4q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.6 1.8h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.9.4q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm7.4-3.1v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm11-.2v3.1h1.3q1 0 1.3-.3.4-.4.4-1.1 0-1-.4-1.4-.4-.3-1.3-.3zm0-3.1v2.2h1.2q.8 0 1.2-.3.3-.3.3-.8 0-.6-.3-.9-.4-.2-1.2-.2zm-1.1-1h2.4q1.2 0 1.9.6.6.5.6 1.5 0 .5-.3 1-.4.4-1 .5.7.1 1.2.7.4.5.4 1.6 0 1-.7 1.6-.7.6-2.1.6h-2.4zm7.1 5.8V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.7 2-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm2.6-8.1h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm5.3-3.9v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm20.6 11v-14h.8v14zm28 0v-14h.8v14zm-644-65v-14h.8v14zm0 26v-14h.8v14zm0 26v-14h.8v14zm112 0v-14h.8v14zm35 0v-14h.8v14zm119.4-2.9L385 199h6.6zm111.6 2.9v-14h.8v14z"/>
<path d="M534.9 208.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm119 0v-14h.8v14zm112 0v-14h.8v14zm14 0v-14h.8v14z"/>
<path d="M534.9 195.3v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm117.6-4.3h1.6v-6l-2 1v-1l2-1h1.1v7h1.7v1h-4.4zm113.4 4.3v-14h.8v14z"/>
<path d="M534.9 182.3v-14h.8v14zm231 0v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm119 0v-14h.8v14zm112 0v-14h.8v14zm14 0v-14h.8v14z"/>
<path d="M534.9 169.3v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm116.1-2.9 3.3-6.5 3.3 6.5zm114.9 2.9v-14h.8v14z"/>
<path d="M534.9 156.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm35 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm14 0v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M534.9 143.3v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V134l6.6 3.2zm9.9 2.9v-14h.8v14zm30.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm16.3-.8q.5.1.8.4.3.3.7 1.2l1.1 2.2h-1.1l-1-2q-.4-.9-.7-1.1-.4-.3-.9-.3h-1v3.4h-1.1v-8h2.2q1.3 0 2 .6t.7 1.7q0 .8-.4 1.3t-1.2.6zm-2-3.2v2.8h1.1q.8 0 1.2-.3.4-.4.4-1.1 0-.7-.4-1-.4-.4-1.2-.4zm6 4.7V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm13.8-4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm32.9 7.1v-14h.8v14zm28 0v-14h.8v14zm-697.5-63v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L65 88q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm137.5 24v-14h.8v14z"/>
<path d="M205.9 117.3v-14h.8v14z"/>
<path d="M205.9 130.3v-14h.8v14zm4-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 19v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14z"/>
<path d="M234.7 111.3v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm11.3-1.6q.8.2 1.2.8.4.5.4 1.3 0 1-.7 1.7-.8.7-2.1.7l-1.1-.1-1.2-.3v-1.1l1.1.4 1 .1q1 0 1.4-.4.5-.4.5-1.1 0-.7-.5-1.1-.4-.4-1.3-.4h-.8v-1h.8q.8 0 1.2-.3.4-.3.4-.9 0-.6-.4-1-.3-.3-1-.3l-1 .2q-.6 0-1.1.3v-1l1.1-.3h1q1.1 0 1.8.5.7.6.7 1.6 0 .6-.4 1-.3.5-1 .7zm2.7 1.6v-.9h6.8v1zm7 0v-.9h6.8v1zm7.1 3.1V108l6.6 3.2zm9.9 2.9v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm14 0v-14h.8v14zm21 0v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14z"/>
<path d="M233.9 104.3v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zM76 84.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm12.3-3.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H121l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-5q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V82h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V82h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm41.6 11v-14h.8v14z"/>
<path d="M233.9 91.3v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zM332.3 81l-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H331l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-7.4V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm5.4 1.7h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1 3q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.3 2.5V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm6.6-2.1v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zM385 80h1l.8 6.5 1-4.3h1l1 4.3.8-6.5h1l-1.2 8h-1l-1-4.8-1.2 4.8h-1zm10.3 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm2.1-3.6h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7V88h-1zm11.4 0v.9h-1.1q-.5 0-.8.2-.2.2-.2.8v.5h2.1v.8h-2V88h-1v-5.2H414V82h1.7v-.4q0-1 .4-1.5.5-.5 1.4-.5zm5.2 6.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm3.7.2h1l1 4.8.9-3h.8l.9 3 1-4.8h1l-1.4 6h-1l-.9-3.3-1 3.3h-.9zm37.9 9.3v-14h.8v14zm11-6v-.9h6.8v1zM501 81l-2.6 4.2h2.5zm-.2-1h1.2v5.2h1.1v.9h-1V88h-1.2v-2h-3.4v-1zm13.1 11.3v-6h-3v-.9h3v-7.1h.8v14zm21 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm-560-39v-14h.8v14z"/>
<path d="M205.9 65.3v-14h.8v14z"/>
<path d="M205.9 78.3v-14h.8v14zm4.1-19.2 6.6-3.2v6.5zm23.9 19.2v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-549-19v-.9h6.8v1zm7 0v-.9h6.8v1zM233 61h3.7v1h-4.9v-1l1.8-1.8 1-1.1.7-1 .2-1q0-.6-.4-1t-1-.4l-1 .2-1.2.6v-1.1l1-.4 1.1-.2q1.2 0 2 .7.6.6.6 1.6l-.2 1q-.2.5-.8 1.2l-.9 1L233 61zm4.9-1.7v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1z"/>
<path d="M269.7 59.3v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm14 0v-6.9h3.8v1h-3v6zm11-6v-.9h6.8v1zm10 6v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V56l6.6 3.2zm9.9 2.9v-14h.8v14zm35.4-4.3 1.9-7h1.1l-2.3 8h-1.3l-2.4-8h1.1zm5.4-5h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm10 3.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.1-2.9V56h2.2v.8h-2.2V60q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V56h1.6v-1.7zm4.7 5.4V56h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V56h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm9.8-.7h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.2 1.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm10.3-5.8h1.4l1.4 4 1.4-4h1.5v8h-1v-7l-1.5 4.1h-.8l-1.4-4.2V62h-1zm10.6 5h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm7 3.1-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm6.9-3.4V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-8.4h1V57q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm3.1-2.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.4 4.6V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm7.4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm39.9 7.1v-14h.8v14zm28 0v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M268.9 52.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14z"/>
<path d="M268.9 39.3v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M268.9 26.3v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm-759-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 13.3V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm35 0V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
</svg>

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,241 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 806 430">
<style>
path {
fill: #333;
stroke: #333;
stroke-width: 0.5;
}
@media (prefers-color-scheme: dark) {
path {
fill: #ccc;
stroke: #ccc;
}
}
</style>
<path d="M1.3 171H6v.9H2.3v2.1h3.3v1H2.3v4h-1z"/>
<path d="M37.9 195.3v-14h.8v14z"/>
<path d="M37.9 208.3v-14h.8v14z"/>
<path d="M37.9 221.3v-14h.8v14z"/>
<path d="M37.9 234.3v-14h.8v14z"/>
<path d="M37.9 247.3v-14h.8v14z"/>
<path d="M37.9 260.3v-14h.8v14z"/>
<path d="M37.9 273.3v-14h.8v14z"/>
<path d="M37.9 286.3v-14h.8v14z"/>
<path d="M37.9 299.3v-14h.8v14z"/>
<path d="M37.9 312.3v-14h.8v14z"/>
<path d="M37.9 325.3v-14h.8v14z"/>
<path d="M37.9 338.3v-14h.8v14z"/>
<path d="M37.9 351.3v-14h.8v14z"/>
<path d="M37.9 364.3v-14h.8v14z"/>
<path d="M37.9 377.3v-14h.8v14z"/>
<path d="M37.9 390.3v-14h.8v14z"/>
<path d="M37.9 403.3v-14h.8v14z"/>
<path d="M37.9 416.3v-14h.8v14z"/>
<path d="M37.9 423.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="m55 412.3-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1H55zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4H83v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V407h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V407h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7z"/>
<path d="M268.9 416.3v-14h.8v14z"/>
<path d="M65.9 221.3v-6.9h3.8v1h-3v6z"/>
<path d="M65.9 234.3v-14h.8v14z"/>
<path d="M65.9 247.3v-14h.8v14z"/>
<path d="M65.9 260.3v-14h.8v14z"/>
<path d="M65.9 267.3v-8h.8v7.1h3v1zm0 71v-6.9h3.8v1h-3v6z"/>
<path d="M65.9 351.3v-14h.8v14z"/>
<path d="M65.9 364.3v-14h.8v14z"/>
<path d="M65.9 377.3v-14h.8v14z"/>
<path d="M65.9 384.3v-8h.8v7.1h3v1z"/>
<path d="M268.9 403.3v-14h.8v14z"/>
<path d="M69.9 384.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M268.9 390.3v-14h.8v14z"/>
<path d="M293.9 384.3v-.9h6.8v1zm14 0v-.9h6.8v1zm14.1 3.1V381l6.6 3.2zm18.9-4.4q0-1.8-.4-2.5-.4-.8-1.2-.8-.8 0-1.2.8-.3.7-.3 2.5t.3 2.5q.4.8 1.2.8.8 0 1.2-.8.4-.7.4-2.5zm1 0q0 2-.6 3.1-.6 1-2 1t-2-1q-.7-1-.7-3.1t.7-3.1q.7-1 2-1 1.4 0 2 1 .7 1 .7 3.1zm4.4-3-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H345l-.6 2h-1.1zm5.3 5.7V381h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V381h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm9.3-5.4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm9.3 4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-8.4h1v3.3q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zM380 386h3.7v1h-4.9v-1l1.8-1.8 1-1.1.7-1 .2-1q0-.6-.4-1t-1-.4l-1 .2-1.2.6v-1.1l1-.4 1.1-.2q1.2 0 2 .7.6.6.6 1.6l-.2 1q-.2.5-.8 1.2l-.9 1-1.7 1.8zm7-1.2h2v2.2h-2zm8.3-5.9q1.3 0 2 1 .6 1 .6 3.1 0 2-.6 3.1-.7 1-2 1t-2-1q-.6-1-.6-3 0-2.1.7-3.2.6-1 2-1zm0 7.4q.8 0 1.1-.8.4-.8.4-2.5v-1.6l-2.6 4q.4.9 1.1.9zm0-6.6q-.7 0-1.1.8-.4.8-.4 2.5l.1 1.4 2.5-4q-.4-.7-1-.7zm14 .3-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H408l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.2-2v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm7 0v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm9.1-2.2h6v.9H459v7.1h-1.1v-7.1h-2.5zm10 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm5-2.2h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm-385.2-30v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm137.5 24v-14h.8v14z"/>
<path d="M268.9 377.3v-14h.8v14z"/>
<path d="M289.9 377.3v-14h.8v14zM111 357.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm12.3-3.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H156l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-5q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V355h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V355h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm41.6 11v-14h.8v14z"/>
<path d="M268.9 364.3v-14h.8v14zm35-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm38 0v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M240.9 351.3v-14h.8v14z"/>
<path d="M268.9 351.3v-14h.8v14z"/>
<path d="M289.9 351.3v-14h.8v14z"/>
<path d="M303.9 351.3v-14h.8v14z"/>
<path d="m321 347.3-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm2.7-5.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm4.4-.3h1.1v3h3v-3h1v8h-1v-4h-3v4h-1zm7.3 5.7V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11-.7q0-1.2-.4-1.7-.4-.6-1.1-.6-.7 0-1.1.6-.4.6-.4 1.7t.4 1.7q.4.6 1 .6.8 0 1.2-.6.3-.6.3-1.7zm-3-2.2q.3-.5.7-.7.4-.3 1-.3 1 0 1.6.9.7.8.7 2.3 0 1.5-.7 2.3-.6.9-1.7.9-.5 0-1-.3l-.6-.7v.8h-1v-8.4h1z"/>
<path d="M534.9 351.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="m587 347.3-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7z"/>
<path d="M800.9 351.3v-14h.8v14z"/>
<path d="M69.9 332.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 338.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="M156.9 286.3v-14h.8v14zm1.1 2.7-2.6 4.2h2.5zm-.2-1h1.2v5.2h1.1v.9h-1v1.9h-1.2v-2h-3.4v-1zm-.9 24.3v-14h.8v14zm.4 10.1L154 316h6.6z"/>
<path d="M268.9 325.3v-14h.8v14z"/>
<path d="M289.9 325.3v-14h.8v14z"/>
<path d="M303.9 325.3v-14h.8v14z"/>
<path d="M331.9 319.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M534.9 325.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 319.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 325.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 312.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 312.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 312.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 312.3v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M289.9 299.3v-14h.8v14z"/>
<path d="M303.9 299.3v-14h.8v14z"/>
<path d="M331.9 299.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 299.3v-14h.8v14z"/>
<path d="M548.9 293.3v-8h.8v7.1h3v1zm11 0v-.9h6.8v1z"/>
<path d="M569.9 299.3v-14h.8v14z"/>
<path d="M573.9 293.3v-.9h6.8v1zm14.1 3.1V290l6.6 3.2zm9.9 2.9v-14h.8v14zm76.1-5.5h2v2.2h-2zm7 0h2v2.2h-2zm7 0h2v2.2h-2zm84.9 5.5v-14h.8v14z"/>
<path d="M800.9 299.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 286.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 286.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 286.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 286.3v-14h.8v14z"/>
<path d="M69.9 267.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M268.9 273.3v-14h.8v14z"/>
<path d="M289.9 273.3v-14h.8v14z"/>
<path d="M303.9 273.3v-14h.8v14z"/>
<path d="M331.9 273.3v-14h.8v14zm51-7.3q0-1.8-.4-2.5-.4-.8-1.2-.8-.8 0-1.2.8-.3.7-.3 2.5t.3 2.5q.4.8 1.2.8.8 0 1.2-.8.4-.7.4-2.5zm1 0q0 2-.6 3.1-.6 1-2 1t-2-1q-.7-1-.7-3.1t.7-3.1q.7-1 2-1 1.4 0 2 1 .7 1 .7 3.1zm2.1-4h4.5v.9H389v6.2h1.6v.9h-4.4v-1h1.7v-6H386zm8.3 7.1q1.4 0 2-.7.5-.6.5-2.4 0-1.8-.5-2.5-.6-.6-2-.6h-.5v6.2zm0-7.1q1.9 0 2.8 1 .8 1 .8 3t-.8 3q-.9 1-2.7 1h-1.7v-8zm10.5 7.7-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm13.6-7.4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm46.9 7.1v-14h.8v14z"/>
<path d="M534.9 273.3v-14h.8v14z"/>
<path d="M548.9 273.3v-14h.8v14z"/>
<path d="M569.9 273.3v-14h.8v14z"/>
<path d="M597.9 273.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M800.9 273.3v-14h.8v14z"/>
<path d="M85 236h4.5v.9H88v6.2h1.6v.9h-4.4v-1h1.7v-6H85zm155.9 24.3v-14h.8v14z"/>
<path d="M268.9 260.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 260.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 260.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="m94.3 237-1.1 4h2.3zm-.6-1H95l2.4 8h-1.1l-.6-2H93l-.6 2h-1.1zm4.5 0h1.4l1.4 4 1.4-4h1.5v8h-1v-7l-1.5 4.1h-.8l-1.4-4.2v7.1h-1zm19.6 7.7-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm7.4-4.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm6.8 1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6-1.4v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm11.3-.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm5.4 1.7h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.6 5.3h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.2 1.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm8.3-3.6v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm12.1-1.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H198l-.6 2h-1.1zm6.4.9v3h1.3q.8 0 1.2-.4.4-.4.4-1.1 0-.7-.4-1.1-.4-.4-1.2-.4zm-1-1h2.3q1.3 0 2 .7.7.6.7 1.8 0 1.2-.7 1.8-.7.6-2 .6h-1.3v3.2h-1zm6.9.1h4.5v.9H214v6.2h1.6v.9h-4.4v-1h1.7v-6H211zm29.9 11.3v-14h.8v14z"/>
<path d="M268.9 247.3v-14h.8v14z"/>
<path d="M289.9 247.3v-14h.8v14z"/>
<path d="M303.9 247.3v-14h.8v14z"/>
<path d="M331.9 247.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 247.3v-14h.8v14z"/>
<path d="M548.9 247.3v-14h.8v14z"/>
<path d="M569.9 247.3v-14h.8v14z"/>
<path d="M597.9 241.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 247.3v-14h.8v14z"/>
<path d="M240.9 234.3v-14h.8v14z"/>
<path d="M268.9 234.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 234.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 234.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 234.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 234.3v-14h.8v14z"/>
<path d="M69.9 215.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 221.3v-14h.8v14z"/>
<path d="M289.9 221.3v-14h.8v14z"/>
<path d="M303.9 221.3v-14h.8v14z"/>
<path d="M331.9 221.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M534.9 221.3v-14h.8v14z"/>
<path d="M548.9 221.3v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M569.9 221.3v-14h.8v14z"/>
<path d="M573.9 215.3v-.9h6.8v1zm14.1 3.1V212l6.6 3.2zm9.9 2.9v-14h.8v14zm23.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm17.7-4.7v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm4.9 0v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm7 2.4q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.6 1.8h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.9.4q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm7.4-3.1v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm11-.2v3.1h1.3q1 0 1.3-.3.4-.4.4-1.1 0-1-.4-1.4-.4-.3-1.3-.3zm0-3.1v2.2h1.2q.8 0 1.2-.3.3-.3.3-.8 0-.6-.3-.9-.4-.2-1.2-.2zm-1.1-1h2.4q1.2 0 1.9.6.6.5.6 1.5 0 .5-.3 1-.4.4-1 .5.7.1 1.2.7.4.5.4 1.6 0 1-.7 1.6-.7.6-2.1.6h-2.4zm7.1 5.8V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.7 2-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm2.6-8.1h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm5.3-3.9v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm20.6 11v-14h.8v14z"/>
<path d="M800.9 221.3v-14h.8v14z"/>
<path d="M156.9 195.3v-14h.8v14zm.4 10.1L154 199h6.6z"/>
<path d="M268.9 208.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M423.3 205.4 420 199h6.6z"/>
<path d="M534.9 208.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 208.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 208.3v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M289.9 195.3v-14h.8v14z"/>
<path d="M303.9 195.3v-14h.8v14z"/>
<path d="M422.9 195.3v-14h.8v14z"/>
<path d="M534.9 195.3v-14h.8v14z"/>
<path d="M548.9 195.3v-14h.8v14z"/>
<path d="M569.9 195.3v-14h.8v14z"/>
<path d="M597.9 195.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M800.9 195.3v-14h.8v14z"/>
<path d="M13 175.5v.5H8.7q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6-1.4v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm11.3-.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.6 1.8h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm4.6-4.3v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm9.7 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6-1.4v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm12.6-5h6v.9H74v7.1h-1.1v-7.1h-2.5zm10 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm5-2.2h1v4.9l2.6-2.5H90l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm8.4.8 6.6-3.2v6.5zm6.9.2v-.9h6.8v1zm14 0v-.9h6.8v1zm25.3-1.6q.8.2 1.2.8.4.5.4 1.3 0 1-.7 1.7-.8.7-2.1.7l-1.1-.1-1.2-.3v-1.1l1.1.4 1 .1q1 0 1.4-.4.5-.4.5-1.1 0-.7-.5-1.1-.4-.4-1.3-.4h-.8v-1h.8q.8 0 1.2-.3.4-.3.4-.9 0-.6-.4-1-.3-.3-1-.3l-1 .2q-.6 0-1.1.3v-1l1.1-.3h1q1.1 0 1.8.5.7.6.7 1.6 0 .6-.4 1-.3.5-1 .7z"/>
<path d="M268.9 182.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M421.5 178h1.6v-6l-2 1v-1l2-1h1.1v7h1.7v1h-4.4z"/>
<path d="M534.9 182.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm-763-169V6.4h3.8v1h-3v6z"/>
<path d="M37.9 26.3v-14h.8v14z"/>
<path d="M37.9 39.3v-14h.8v14z"/>
<path d="M37.9 52.3v-14h.8v14z"/>
<path d="M37.9 65.3v-14h.8v14z"/>
<path d="M37.9 78.3v-14h.8v14z"/>
<path d="M37.9 91.3v-14h.8v14z"/>
<path d="M37.9 104.3v-14h.8v14z"/>
<path d="M37.9 117.3v-14h.8v14z"/>
<path d="M37.9 130.3v-14h.8v14z"/>
<path d="M37.9 143.3v-14h.8v14z"/>
<path d="M37.9 156.3v-14h.8v14z"/>
<path d="M37.9 169.3v-14h.8v14z"/>
<path d="M65.9 39.3v-6.9h3.8v1h-3v6z"/>
<path d="M65.9 52.3v-14h.8v14z"/>
<path d="M65.9 65.3v-14h.8v14z"/>
<path d="M65.9 78.3v-14h.8v14z"/>
<path d="M65.9 91.3v-14h.8v14z"/>
<path d="M65.9 104.3v-14h.8v14z"/>
<path d="M65.9 117.3v-14h.8v14z"/>
<path d="M65.9 130.3v-14h.8v14z"/>
<path d="M65.9 137.3v-8h.8v7.1h3v1zm88.1 16.1 3.3-6.5 3.3 6.5zm2.9 15.9v-14h.8v14z"/>
<path d="M268.9 169.3v-14h.8v14z"/>
<path d="M289.9 169.3v-14h.8v14z"/>
<path d="M303.9 169.3v-14h.8v14z"/>
<path d="M422.9 169.3v-14h.8v14z"/>
<path d="M534.9 169.3v-14h.8v14z"/>
<path d="M548.9 169.3v-14h.8v14z"/>
<path d="M569.9 169.3v-14h.8v14z"/>
<path d="M597.9 163.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 169.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="m420 153.4 3.3-6.5 3.3 6.5z"/>
<path d="M534.9 156.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 156.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 156.3v-14h.8v14z"/>
<path d="M69.9 137.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M268.9 143.3v-14h.8v14z"/>
<path d="M289.9 143.3v-14h.8v14z"/>
<path d="M303.9 143.3v-14h.8v14z"/>
<path d="M331.9 137.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M534.9 143.3v-14h.8v14z"/>
<path d="M548.9 143.3v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M569.9 143.3v-14h.8v14z"/>
<path d="M573.9 137.3v-.9h6.8v1zm14.1 3.1V134l6.6 3.2zm9.9 2.9v-14h.8v14zm30.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm16.3-.8q.5.1.8.4.3.3.7 1.2l1.1 2.2h-1.1l-1-2q-.4-.9-.7-1.1-.4-.3-.9-.3h-1v3.4h-1.1v-8h2.2q1.3 0 2 .6t.7 1.7q0 .8-.4 1.3t-1.2.6zm-2-3.2v2.8h1.1q.8 0 1.2-.3.4-.4.4-1.1 0-.7-.4-1-.4-.4-1.2-.4zm6 4.7V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm13.8-4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm32.9 7.1v-14h.8v14z"/>
<path d="M800.9 143.3v-14h.8v14z"/>
<path d="M82.4 80.3v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L79 88q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm158.5 24v-14h.8v14z"/>
<path d="M240.9 117.3v-14h.8v14z"/>
<path d="M240.9 130.3v-14h.8v14zm4-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1z"/>
<path d="M268.9 130.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 130.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 130.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 130.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 130.3v-14h.8v14z"/>
<path d="M269.7 111.3v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h6.8v1h-3v6zm4-6v-.9h6.8v1zm8.2-5.3h4v.9h-3v2l.5-.2h.4q1.3 0 2 .7.7.8.7 2 0 1.3-.7 2-.8.8-2.1.8l-1.2-.1-1-.3v-1l1 .3 1 .1q1 0 1.5-.4.5-.5.5-1.4 0-.8-.6-1.3-.5-.5-1.3-.5l-.9.1-.8.3zm5.8 5.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7.1 3.1V108l6.6 3.2zm9.9 2.9v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 117.3v-14h.8v14z"/>
<path d="M548.9 117.3v-14h.8v14z"/>
<path d="M569.9 117.3v-14h.8v14z"/>
<path d="M597.9 117.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M800.9 117.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 104.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 104.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="M90 84.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.7 3.5-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm2.3-2V82h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V82h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm12.2-2.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm2.5-1.2h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm8.3 5.7-.6 1.7-.7 1.8q-.3.4-.7.6-.3.2-.8.2h-.8v-.8h.6q.4 0 .7-.3l.6-1.3-2.3-6h1l1.8 4.8L131 82h1zm9.7-6h6v.9H144V88h-1.1v-7.1h-2.5zm10 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm5-2.2h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7V88h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm13.8-4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L184 88q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm11.9 7.1v-14h.8v14z"/>
<path d="M268.9 91.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 91.3v-14h.8v14zM367.3 81l-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H366l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-7.4V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm5.4 1.7h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1 3q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.3 2.5V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm6.6-2.1v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zM420 80h1l.8 6.5 1-4.3h1l1 4.3.8-6.5h1l-1.2 8h-1l-1-4.8-1.2 4.8h-1zm10.3 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm2.1-3.6h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7V88h-1zm11.4 0v.9h-1.1q-.5 0-.8.2-.2.2-.2.8v.5h2.1v.8h-2V88h-1v-5.2H449V82h1.7v-.4q0-1 .4-1.5.5-.5 1.4-.5zm5.2 6.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm3.7.2h1l1 4.8.9-3h.8l.9 3 1-4.8h1l-1.4 6h-1l-.9-3.3-1 3.3h-.9zm37.9 9.3v-14h.8v14zm11-6v-.9h6.8v1zm19.4-5.1v1l-.7-.3-.8-.2q-1 0-1.6.8-.5.8-.5 2.3.3-.5.7-.8.5-.3 1-.3 1.2 0 1.9.7.6.7.6 2t-.6 2q-.7.8-1.9.8-1.4 0-2-1-.7-1-.7-3.2 0-2 .8-3.1.8-1 2.3-1h.8l.7.3zm-2 3.3q-.6 0-1 .5t-.4 1.4q0 1 .4 1.4.4.5 1 .5.8 0 1.2-.5.3-.4.3-1.4 0-1-.3-1.4-.4-.5-1.1-.5zm13.6 7.8v-6h-3v-.9h3v-7.1h.8v14z"/>
<path d="M569.9 91.3v-14h.8v14z"/>
<path d="M597.9 85.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 91.3v-14h.8v14z"/>
<path d="M240.9 52.3v-14h.8v14z"/>
<path d="M240.9 65.3v-14h.8v14z"/>
<path d="M240.9 78.3v-14h.8v14zm4.1-19.2 6.6-3.2v6.5z"/>
<path d="M268.9 78.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 78.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 78.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 78.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 78.3v-14h.8v14z"/>
<path d="M251.9 59.3v-.9h6.8v1zm7 0v-.9h6.8v1zM268 61h3.7v1h-4.9v-1l1.8-1.8 1-1.1.7-1 .2-1q0-.6-.4-1t-1-.4l-1 .2-1.2.6v-1.1l1-.4 1.1-.2q1.2 0 2 .7.6.6.6 1.6l-.2 1q-.2.5-.8 1.2l-.9 1L268 61zm4.9-1.7v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10.8 0v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 65.3v-14h.8v14z"/>
<path d="M548.9 65.3v-6.9h3.8v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M569.9 65.3v-14h.8v14z"/>
<path d="M573.9 59.3v-.9h6.8v1zm14.1 3.1V56l6.6 3.2zm9.9 2.9v-14h.8v14zm35.4-4.3 1.9-7h1.1l-2.3 8h-1.3l-2.4-8h1.1zm5.4-5h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm10 3.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.1-2.9V56h2.2v.8h-2.2V60q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V56h1.6v-1.7zm4.7 5.4V56h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V56h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm9.8-.7h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.2 1.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm10.3-5.8h1.4l1.4 4 1.4-4h1.5v8h-1v-7l-1.5 4.1h-.8l-1.4-4.2V62h-1zm10.6 5h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm7 3.1-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm6.9-3.4V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-8.4h1V57q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm3.1-2.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.4 4.6V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm7.4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm39.9 7.1v-14h.8v14z"/>
<path d="M800.9 65.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 52.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 52.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 52.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 52.3v-14h.8v14z"/>
<path d="M69.9 33.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 39.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 39.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M534.9 39.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 39.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<g>
<path d="M800.9 39.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M800.9 26.3v-14h.8v14zm-759-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm35 0V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm35 0V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1z"/>
<path d="M800.9 13.3v-6h-3v-.9h3.8v7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 46 KiB

144
package-lock.json generated
View File

@ -14,16 +14,16 @@
"@google-github-actions/actions-utils": "^0.4.10" "@google-github-actions/actions-utils": "^0.4.10"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.9.0", "@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.11.0", "@typescript-eslint/parser": "^6.12.0",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
"eslint": "^8.53.0", "eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.2.2" "typescript": "^5.3.2"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -113,9 +113,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.53.0", "version": "8.54.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
"integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -281,31 +281,31 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.9.0", "version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.5", "version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
"integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz",
"integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.11.0", "@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/type-utils": "6.11.0", "@typescript-eslint/type-utils": "6.12.0",
"@typescript-eslint/utils": "6.11.0", "@typescript-eslint/utils": "6.12.0",
"@typescript-eslint/visitor-keys": "6.11.0", "@typescript-eslint/visitor-keys": "6.12.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@ -331,15 +331,15 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz",
"integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "6.11.0", "@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/types": "6.11.0", "@typescript-eslint/types": "6.12.0",
"@typescript-eslint/typescript-estree": "6.11.0", "@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/visitor-keys": "6.11.0", "@typescript-eslint/visitor-keys": "6.12.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -359,13 +359,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz",
"integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.11.0", "@typescript-eslint/types": "6.12.0",
"@typescript-eslint/visitor-keys": "6.11.0" "@typescript-eslint/visitor-keys": "6.12.0"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@ -376,13 +376,13 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz",
"integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "6.11.0", "@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/utils": "6.11.0", "@typescript-eslint/utils": "6.12.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
}, },
@ -403,9 +403,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz",
"integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@ -416,13 +416,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz",
"integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.11.0", "@typescript-eslint/types": "6.12.0",
"@typescript-eslint/visitor-keys": "6.11.0", "@typescript-eslint/visitor-keys": "6.12.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -443,17 +443,17 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz",
"integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.11.0", "@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/types": "6.11.0", "@typescript-eslint/types": "6.12.0",
"@typescript-eslint/typescript-estree": "6.11.0", "@typescript-eslint/typescript-estree": "6.12.0",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"engines": { "engines": {
@ -468,12 +468,12 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "6.11.0", "version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz",
"integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.11.0", "@typescript-eslint/types": "6.12.0",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
}, },
"engines": { "engines": {
@ -597,9 +597,9 @@
"dev": true "dev": true
}, },
"node_modules/big-integer": { "node_modules/big-integer": {
"version": "1.6.51", "version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -838,15 +838,15 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.53.0", "version": "8.54.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
"integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3", "@eslint/eslintrc": "^2.1.3",
"@eslint/js": "8.53.0", "@eslint/js": "8.54.0",
"@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@ -1274,9 +1274,9 @@
} }
}, },
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.2.4", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">= 4" "node": ">= 4"
@ -2242,9 +2242,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.2.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@ -2255,9 +2255,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.27.2", "version": "5.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.0.tgz",
"integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", "integrity": "sha512-gM12DkXhlAc5+/TPe60iy9P6ETgVfqTuRJ6aQ4w8RYu0MqKuXhaq3/b86GfzDQnNA3NUO6aUNdvevrKH59D0Nw==",
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
}, },

View File

@ -28,15 +28,15 @@
"@google-github-actions/actions-utils": "^0.4.10" "@google-github-actions/actions-utils": "^0.4.10"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.9.0", "@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.11.0", "@typescript-eslint/parser": "^6.12.0",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
"eslint": "^8.53.0", "eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.2.2" "typescript": "^5.3.2"
} }
} }

View File

@ -12,157 +12,210 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { HttpClient } from '@actions/http-client';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import {
GoogleAccessTokenParameters,
GoogleAccessTokenResponse,
GoogleIDTokenParameters,
GoogleIDTokenResponse,
} from './client/auth_client';
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement. import { HttpClient } from '@actions/http-client';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version: appVersion } = require('../package.json');
// userAgent is the default user agent. import { Logger } from './logger';
const userAgent = `google-github-actions:auth/${appVersion}`; import { expandEndpoint, userAgent } from './utils';
/** /**
* BaseClient is the default HTTP client for interacting with the IAM * GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
* credentials API.
*/ */
export class BaseClient { export interface GenerateAccessTokenParameters {
/** readonly serviceAccount: string;
* client is the HTTP client. readonly delegates?: string[];
*/ readonly scopes?: string[];
protected readonly client: HttpClient; readonly lifetime?: number;
}
constructor() { /**
this.client = new HttpClient(userAgent); * GenerateIDTokenParameters are the inputs to the generateIDToken call.
*/
export interface GenerateIDTokenParameters {
readonly serviceAccount: string;
readonly audience: string;
readonly delegates?: string[];
readonly includeEmail?: boolean;
}
/**
* IAMCredentialsClientParameters are the inputs to the IAM client.
*/
export interface IAMCredentialsClientParameters {
readonly authToken: string;
}
/**
* IAMCredentialsClient is a thin HTTP client around the Google Cloud IAM
* Credentials API.
*/
export class IAMCredentialsClient {
readonly #logger: Logger;
readonly #httpClient: HttpClient;
readonly #authToken: string;
readonly #universe: string = 'googleapis.com';
readonly #endpoints = {
iamcredentials: 'https://iamcredentials.{universe}/v1',
oauth2: 'https://oauth2.{universe}',
};
constructor(logger: Logger, opts: IAMCredentialsClientParameters) {
this.#logger = logger.withNamespace(this.constructor.name);
this.#httpClient = new HttpClient(userAgent);
this.#authToken = opts.authToken;
const endpoints = this.#endpoints;
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
}
this.#logger.debug(`Computed endpoints`, this.#endpoints);
} }
/** /**
* googleIDToken generates a Google Cloud ID token for the provided * generateAccessToken generates a new OAuth 2.0 Access Token for a service
* service account email or unique id. * account.
*/ */
async googleIDToken( async generateAccessToken({
token: string, serviceAccount,
{ serviceAccount, audience, delegates, includeEmail }: GoogleIDTokenParameters, delegates,
): Promise<GoogleIDTokenResponse> { scopes,
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`; lifetime,
}: GenerateAccessTokenParameters): Promise<string> {
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
const data = { const headers = { Authorization: `Bearer ${this.#authToken}` };
delegates: delegates,
audience: audience,
includeEmail: includeEmail,
};
const headers = { const body: Record<string, string | Array<string>> = {};
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
};
try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);
return {
token: parsed['token'],
};
} catch (err) {
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
}
}
/**
* googleAccessToken generates a Google Cloud access token for the provided
* service account email or unique id.
*/
async googleAccessToken(
token: string,
{ serviceAccount, delegates, scopes, lifetime }: GoogleAccessTokenParameters,
): Promise<GoogleAccessTokenResponse> {
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
const data: Record<string, string | Array<string>> = {};
if (delegates && delegates.length > 0) { if (delegates && delegates.length > 0) {
data.delegates = delegates; body.delegates = delegates;
} }
if (scopes && scopes.length > 0) { if (scopes && scopes.length > 0) {
// Not a typo, the API expects the field to be "scope" (singular). // Not a typo, the API expects the field to be "scope" (singular).
data.scope = scopes; body.scope = scopes;
} }
if (lifetime && lifetime > 0) { if (lifetime && lifetime > 0) {
data.lifetime = `${lifetime}s`; body.lifetime = `${lifetime}s`;
} }
const headers = { this.#logger.withNamespace('generateAccessToken').debug({
'Authorization': `Bearer ${token}`, method: `POST`,
'Accept': 'application/json', path: pth,
'Content-Type': 'application/json', headers: headers,
}; body: body,
});
try { try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers); const resp = await this.#httpClient.postJson<{ accessToken: string }>(pth, body, headers);
const body = await resp.readBody(); const statusCode = resp.statusCode || 500;
const statusCode = resp.message.statusCode || 500; if (statusCode < 200 || statusCode > 299) {
if (statusCode >= 400) { throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
throw new Error(`(${statusCode}) ${body}`);
} }
const parsed = JSON.parse(body);
return { const result = resp.result;
accessToken: parsed['accessToken'], if (!result) {
expiration: parsed['expireTime'], throw new Error(`Successfully called ${pth}, but the result was empty`);
}; }
return result.accessToken;
} catch (err) { } catch (err) {
throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${err}`); throw new Error(
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${err}`,
);
} }
} }
/** async generateDomainWideDelegationAccessToken(assertion: string): Promise<string> {
* googleOAuthToken generates a Google Cloud OAuth token using the legacy const pth = `${this.#endpoints.oauth2}/token`;
* OAuth endpoints.
*
* @param assertion A signed JWT.
*/
async googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse> {
const pth = `https://oauth2.googleapis.com/token`;
const headers = { const headers = {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}; };
const data = new URLSearchParams(); const body = new URLSearchParams();
data.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'); body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
data.append('assertion', assertion); body.append('assertion', assertion);
this.#logger.withNamespace('generateDomainWideDelegationAccessToken').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try { try {
const resp = await this.client.request('POST', pth, data.toString(), headers); const resp = await this.#httpClient.post(pth, body.toString(), headers);
const body = await resp.readBody(); const respBody = await resp.readBody();
const statusCode = resp.message.statusCode || 500; const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) { if (statusCode < 200 || statusCode > 299) {
throw new Error(`(${statusCode}) ${body}`); throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${respBody || '[no body]'}`);
} }
const parsed = JSON.parse(body); const parsed = JSON.parse(respBody) as { accessToken: string };
return parsed.accessToken;
// Normalize the expiration to be a timestamp like the iamcredentials API.
// This API returns the number of seconds until expiration, so convert
// that into a date.
const expiration = new Date(new Date().getTime() + parsed['expires_in'] * 10000);
return {
accessToken: parsed['access_token'],
expiration: expiration.toISOString(),
};
} catch (err) { } catch (err) {
throw new Error(`Failed to generate Google Cloud OAuth token: ${err}`); throw new Error(
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`,
);
}
}
/**
* generateIDToken generates a new OpenID Connect ID token for a service
* account.
*/
async generateIDToken({
serviceAccount,
audience,
delegates,
includeEmail,
}: GenerateIDTokenParameters): Promise<string> {
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
const headers = { Authorization: `Bearer ${this.#authToken}` };
const body: Record<string, string | string[] | boolean> = {
audience: audience,
includeEmail: includeEmail ? true : false,
};
if (delegates && delegates.length > 0) {
body.delegates = delegates;
}
this.#logger.withNamespace('generateIDToken').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try {
const resp = await this.#httpClient.postJson<{ token: string }>(pth, body, headers);
const statusCode = resp.statusCode || 500;
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
}
const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
return result.token;
} catch (err) {
throw new Error(
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${err}`,
);
} }
} }
} }
export { AuthClient } from './client/auth_client';
export {
ServiceAccountKeyClientParameters,
ServiceAccountKeyClient,
} from './client/credentials_json_client';
export {
WorkloadIdentityFederationClientParameters,
WorkloadIdentityFederationClient,
} from './client/workload_identity_client';

View File

@ -13,83 +13,24 @@
// limitations under the License. // limitations under the License.
/** /**
* Defines the main interface for all clients that generate credentials. * Client is the default HTTP client for interacting with the IAM credentials
* API.
*/ */
export interface AuthClient { export interface AuthClient {
getAuthToken(): Promise<string>; /**
signJWT(unsignedJWT: string, delegates?: Array<string>): Promise<string>; * getToken() gets or generates the best token for the auth client.
getProjectID(): Promise<string>; */
getServiceAccount(): Promise<string>; getToken(): Promise<string>;
createCredentialsFile(outputDir: string): Promise<string>;
/** /**
* Provided by BaseClient. * createCredentialsFile creates a credential file (for use with gcloud and
* other Google Cloud tools) that instructs the tool how to perform identity
* federation.
*/ */
googleIDToken(token: string, params: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse>; createCredentialsFile(outputPath: string): Promise<string>;
googleAccessToken(
token: string,
params: GoogleAccessTokenParameters,
): Promise<GoogleAccessTokenResponse>;
googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse>;
}
/** /**
* GoogleAccessTokenParameters are the parameters to generate a Google Cloud * signJWT signs a JWT using the auth provider.
* access token as described in:
*
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
*
* @param serviceAccount Optional email address or unique identifier of the
* service account.
* @param delegates Optional sequence of service accounts in the delegation
* chain.
* @param lifetime Optional validity period as a number representing the number
* of seconds.
*/ */
export interface GoogleAccessTokenParameters { signJWT(claims: any): Promise<string>;
serviceAccount?: string;
delegates?: Array<string>;
scopes?: Array<string>;
lifetime?: number;
}
/**
* GoogleAccessTokenResponse is the response from generating an access token.
*
* @param accessToken OAuth 2.0 access token.
* @param expiration A timestamp in RFC3339 UTC "Zulu" format when the token
* expires.
*/
export interface GoogleAccessTokenResponse {
accessToken: string;
expiration: string;
}
/**
* GoogleIDTokenParameters are the parameters to generate a Google Cloud
* ID token as described in:
*
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
*
* @param serviceAccount Email address or unique identifier of the service
* account.
* @param audience The audience for the token.
* @param delegates Optional sequence of service accounts in the delegation
* chain.
*/
export interface GoogleIDTokenParameters {
serviceAccount?: string;
audience: string;
delegates?: Array<string>;
includeEmail?: boolean;
}
/**
* GoogleIDTokenResponse is the response from generating an ID token.
*
* @param token ID token.
* expires.
*/
export interface GoogleIDTokenResponse {
token: string;
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { createSign } from 'crypto'; import { createSign } from 'crypto';
import { import {
isServiceAccountKey, isServiceAccountKey,
parseCredential, parseCredential,
@ -22,123 +23,112 @@ import {
} from '@google-github-actions/actions-utils'; } from '@google-github-actions/actions-utils';
import { AuthClient } from './auth_client'; import { AuthClient } from './auth_client';
import { BaseClient } from '../base'; import { expandEndpoint } from '../utils';
import { Logger } from '../logger';
/** /**
* Available options to create the CredentialsJSONClient. * ServiceAccountKeyClientParameters is used as input to the
* * ServiceAccountKeyClient.
* @param projectID User-supplied value for project ID. If not provided, the
* project ID is extracted from the credentials JSON.
* @param credentialsJSON Raw JSON credentials blob.
*/ */
interface CredentialsJSONClientOptions { export interface ServiceAccountKeyClientParameters {
projectID?: string; readonly serviceAccountKey: string;
credentialsJSON: string;
} }
/** /**
* CredentialsJSONClient is a client that accepts a service account key JSON * ServiceAccountKeyClient is an authentication client that expects a Service
* credential. * Account Key JSON file.
*/ */
export class CredentialsJSONClient extends BaseClient implements AuthClient { export class ServiceAccountKeyClient implements AuthClient {
readonly #projectID: string; readonly #logger: Logger;
readonly #credentials: ServiceAccountKey; readonly #serviceAccountKey: ServiceAccountKey;
constructor(opts: CredentialsJSONClientOptions) { readonly #universe: string = 'googleapis.com';
super(); readonly #endpoints = {
iamcredentials: 'https://iamcredentials.{universe}/v1',
};
readonly #audience: string;
const credentials = parseCredential(opts.credentialsJSON); constructor(logger: Logger, opts: ServiceAccountKeyClientParameters) {
if (!isServiceAccountKey(credentials)) { this.#logger = logger.withNamespace(this.constructor.name);
throw new Error(`Provided credential is not a valid service account key JSON`);
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
if (!isServiceAccountKey(serviceAccountKey)) {
throw new Error(`Provided credential is not a valid Google Service Account Key JSON`);
} }
this.#credentials = credentials; this.#serviceAccountKey = serviceAccountKey;
this.#projectID = opts.projectID || this.#credentials.project_id; const endpoints = this.#endpoints;
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
}
this.#logger.debug(`Computed endpoints`, this.#endpoints);
this.#audience = new URL(this.#endpoints.iamcredentials).origin + `/`;
this.#logger.debug(`Computed audience`, this.#audience);
} }
/** /**
* getAuthToken generates a token capable of calling the iamcredentials API. * getToken generates a self-signed JWT that, by default, is capable of
* calling the iamcredentials API to mint OAuth 2.0 Access Tokens and ID
* Tokens. However, users can theoretically override the audience value and
* use the JWT to call other endpoints without calling iamcredentials.
*/ */
async getAuthToken(): Promise<string> { async getToken(): Promise<string> {
const header = { try {
alg: 'RS256',
typ: 'JWT',
kid: this.#credentials.private_key_id,
};
const now = Math.floor(new Date().getTime() / 1000); const now = Math.floor(new Date().getTime() / 1000);
const body = { const claims = {
iss: this.#credentials.client_email, iss: this.#serviceAccountKey.client_email,
sub: this.#credentials.client_email, sub: this.#serviceAccountKey.client_email,
aud: 'https://iamcredentials.googleapis.com/', aud: this.#audience,
iat: now, iat: now,
exp: now + 3599, exp: now + 3599,
}; };
const message = toBase64(JSON.stringify(header)) + '.' + toBase64(JSON.stringify(body)); this.#logger.withNamespace('getToken').debug({
claims: claims,
});
try { return await this.signJWT(claims);
const signer = createSign('RSA-SHA256');
signer.write(message);
signer.end();
const signature = signer.sign(this.#credentials.private_key);
return message + '.' + toBase64(signature);
} catch (err) { } catch (err) {
throw new Error(`Failed to sign auth token using ${await this.getServiceAccount()}: ${err}`); throw new Error(
`Failed to sign auth token using ${this.#serviceAccountKey.client_email}: ${err}`,
);
} }
} }
/** /**
* signJWT signs the given JWT with the private key. * signJWT signs a JWT using the Service Account's private key.
*
* @param unsignedJWT The JWT to sign.
*/ */
async signJWT(unsignedJWT: string): Promise<string> { async signJWT(claims: any): Promise<string> {
const header = { const header = {
alg: 'RS256', alg: `RS256`,
typ: 'JWT', typ: `JWT`,
kid: this.#credentials.private_key_id, kid: this.#serviceAccountKey.private_key_id,
}; };
const message = toBase64(JSON.stringify(header)) + '.' + toBase64(unsignedJWT); const message = toBase64(JSON.stringify(header)) + `.` + toBase64(JSON.stringify(claims));
try { this.#logger.withNamespace('signJWT').debug({
const signer = createSign('RSA-SHA256'); header: header,
claims: claims,
message: message,
});
const signer = createSign(`RSA-SHA256`);
signer.write(message); signer.write(message);
signer.end(); signer.end();
const signature = signer.sign(this.#credentials.private_key); const signature = signer.sign(this.#serviceAccountKey.private_key);
const jwt = message + '.' + toBase64(signature); return message + '.' + toBase64(signature);
return jwt;
} catch (err) {
throw new Error(`Failed to sign JWT using ${await this.getServiceAccount()}: ${err}`);
}
} }
/** /**
* getProjectID returns the project ID. If an override was given, the override * createCredentialsFile writes the Service Account Key JSON back to disk at
* is returned. Otherwise, this will be the project ID that was extracted from * the specified outputPath.
* the service account key JSON.
*/
async getProjectID(): Promise<string> {
return this.#projectID;
}
/**
* getServiceAccount returns the service account email for the authentication,
* extracted from the Service Account Key JSON.
*/
async getServiceAccount(): Promise<string> {
return this.#credentials.client_email;
}
/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/ */
async createCredentialsFile(outputPath: string): Promise<string> { async createCredentialsFile(outputPath: string): Promise<string> {
return await writeSecureFile(outputPath, JSON.stringify(this.#credentials)); this.#logger.withNamespace('createCredentialsFile').debug({ outputPath: outputPath });
return await writeSecureFile(outputPath, JSON.stringify(this.#serviceAccountKey));
} }
} }

View File

@ -12,211 +12,208 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { URL } from 'url'; import { HttpClient } from '@actions/http-client';
import { writeSecureFile } from '@google-github-actions/actions-utils'; import { writeSecureFile } from '@google-github-actions/actions-utils';
import { AuthClient } from './auth_client'; import { AuthClient } from './auth_client';
import { BaseClient } from '../base'; import { expandEndpoint, userAgent } from '../utils';
import { Logger } from '../logger';
/** /**
* Available options to create the WorkloadIdentityClient. * WorkloadIdentityFederationClientParameters is used as input to the
* * WorkloadIdentityFederationClient.
* @param projectID User-supplied value for project ID. If not provided, the
* project ID is extracted from the service account email.
* @param providerID Full path (including project, location, etc) to the Google
* Cloud Workload Identity Provider.
* @param serviceAccount Email address or unique identifier of the service
* account to impersonate
* @param token GitHub OIDC token to use for exchanging with Workload Identity
* Federation.
* @param audience The value for the audience parameter in the generated GitHub
* Actions OIDC token, defaults to the value of workload_identity_provider
*/ */
interface WorkloadIdentityClientOptions { export interface WorkloadIdentityFederationClientParameters {
projectID?: string; readonly githubOIDCToken: string;
providerID: string; readonly githubOIDCTokenRequestURL: string;
serviceAccount: string; readonly githubOIDCTokenRequestToken: string;
token: string; readonly githubOIDCTokenAudience: string;
audience: string; readonly workloadIdentityProviderName: string;
readonly audience?: string;
oidcTokenRequestURL: string; readonly serviceAccount?: string;
oidcTokenRequestToken: string;
} }
/** /**
* WorkloadIdentityClient is a client that uses the GitHub Actions runtime to * WorkloadIdentityFederationClient is an authentication client that configures
* authentication via Workload Identity. * a Workload Identity authentication scheme.
*/ */
export class WorkloadIdentityClient extends BaseClient implements AuthClient { export class WorkloadIdentityFederationClient implements AuthClient {
readonly #projectID: string; readonly #logger: Logger;
readonly #providerID: string; readonly #httpClient: HttpClient;
readonly #serviceAccount: string;
readonly #token: string; readonly #githubOIDCToken: string;
readonly #githubOIDCTokenRequestURL: string;
readonly #githubOIDCTokenRequestToken: string;
readonly #githubOIDCTokenAudience: string;
readonly #workloadIdentityProviderName: string;
readonly #serviceAccount?: string;
#cachedToken?: string;
#cachedAt?: number;
readonly #universe: string = 'googleapis.com';
readonly #endpoints = {
iam: 'https://iam.{universe}/v1',
iamcredentials: 'https://iamcredentials.{universe}/v1',
sts: 'https://sts.{universe}/v1',
www: 'https://www.{universe}',
};
readonly #audience: string; readonly #audience: string;
readonly #oidcTokenRequestURL: string; constructor(logger: Logger, opts: WorkloadIdentityFederationClientParameters) {
readonly #oidcTokenRequestToken: string; this.#logger = logger.withNamespace(this.constructor.name);
this.#httpClient = new HttpClient(userAgent);
constructor(opts: WorkloadIdentityClientOptions) { this.#githubOIDCToken = opts.githubOIDCToken;
super(); this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
this.#githubOIDCTokenRequestToken = opts.githubOIDCTokenRequestToken;
this.#providerID = opts.providerID; this.#githubOIDCTokenAudience = opts.githubOIDCTokenAudience;
this.#workloadIdentityProviderName = opts.workloadIdentityProviderName;
this.#serviceAccount = opts.serviceAccount; this.#serviceAccount = opts.serviceAccount;
this.#token = opts.token;
this.#audience = opts.audience;
this.#oidcTokenRequestURL = opts.oidcTokenRequestURL; const endpoints = this.#endpoints;
this.#oidcTokenRequestToken = opts.oidcTokenRequestToken; for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
}
this.#logger.debug(`Computed endpoints`, this.#endpoints);
this.#projectID = const iamHost = new URL(this.#endpoints.iam).host;
opts.projectID || this.extractProjectIDFromServiceAccountEmail(this.#serviceAccount); this.#audience = `//${iamHost}/${this.#workloadIdentityProviderName}`;
this.#logger.debug(`Computed audience`, this.#audience);
} }
/** /**
* extractProjectIDFromServiceAccountEmail extracts the project ID from the * getToken gets a Google Cloud Federated Token that can call other Google
* service account email address. * Cloud APIs directly or impersonate an existing Service Account. Direct
* Workload Identity Federation will use the Federated Token directly.
* Workload Identity Federation through a Service Account will use
* impersonation.
*/ */
extractProjectIDFromServiceAccountEmail(str: string): string { async getToken(): Promise<string> {
if (!str) { const now = new Date().getTime();
return ''; if (this.#cachedToken && this.#cachedAt && now - this.#cachedAt > 60_000) {
this.#logger.debug(`Using cached token`);
return this.#cachedToken;
} }
const [, dn] = str.split('@', 2); const pth = `${this.#endpoints.sts}/token`;
if (!str.endsWith('.iam.gserviceaccount.com')) {
throw new Error(
`Service account email ${str} is not of the form ` +
`"[name]@[project].iam.gserviceaccount.com. You must manually ` +
`specify the "project_id" parameter in your GitHub Actions workflow.`,
);
}
const [project] = dn.split('.', 2); const body = {
return project; audience: this.#audience,
} grantType: `urn:ietf:params:oauth:grant-type:token-exchange`,
requestedTokenType: `urn:ietf:params:oauth:token-type:access_token`,
/** scope: `${this.#endpoints.www}/auth/cloud-platform`,
* getAuthToken generates a Google Cloud federated token using the provided subjectTokenType: `urn:ietf:params:oauth:token-type:jwt`,
* OIDC token and Workload Identity Provider. subjectToken: this.#githubOIDCToken,
*/
async getAuthToken(): Promise<string> {
const pth = `https://sts.googleapis.com/v1/token`;
const data = {
audience: '//iam.googleapis.com/' + this.#providerID,
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
scope: 'https://www.googleapis.com/auth/cloud-platform',
subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt',
subjectToken: this.#token,
}; };
const headers = { this.#logger.withNamespace('getToken').debug({
'Accept': 'application/json', method: `POST`,
'Content-Type': 'application/json', path: pth,
}; body: body,
});
try { try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers); const resp = await this.#httpClient.postJson<{ access_token: string }>(pth, body);
const body = await resp.readBody(); const statusCode = resp.statusCode || 500;
const statusCode = resp.message.statusCode || 500; if (statusCode < 200 || statusCode > 299) {
if (statusCode >= 400) { throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
throw new Error(`(${statusCode}) ${body}`);
} }
const parsed = JSON.parse(body);
return parsed['access_token']; const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
this.#cachedToken = result.access_token;
this.#cachedAt = now;
return result.access_token;
} catch (err) { } catch (err) {
throw new Error( throw new Error(
`Failed to generate Google Cloud federated token for ${this.#providerID}: ${err}`, `Failed to generate Google Cloud federated token for ${this.#audience}: ${err}`,
); );
} }
} }
/** /**
* signJWT signs the given JWT using the IAM credentials endpoint. * signJWT signs a JWT using the Service Account's private key.
*
* @param unsignedJWT The JWT to sign.
* @param delegates List of service account email address to use for
* impersonation in the delegation chain to sign the JWT.
*/ */
async signJWT(unsignedJWT: string, delegates?: Array<string>): Promise<string> { async signJWT(claims: any): Promise<string> {
const serviceAccount = await this.getServiceAccount(); if (!this.#serviceAccount) {
const federatedToken = await this.getAuthToken(); throw new Error(`Cannot sign JWTs without specifying a service account`);
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signJwt`;
const data: Record<string, string | Array<string>> = {
payload: unsignedJWT,
};
if (delegates && delegates.length > 0) {
data.delegates = delegates;
} }
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
const headers = { const headers = {
'Accept': 'application/json', Authorization: `Bearer ${this.getToken()}`,
'Authorization': `Bearer ${federatedToken}`,
'Content-Type': 'application/json',
}; };
const body = {
payload: claims,
};
this.#logger.withNamespace('signJWT').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try { try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers); const resp = await this.#httpClient.postJson<{ signedJwt: string }>(pth, body, headers);
const body = await resp.readBody(); const statusCode = resp.statusCode || 500;
const statusCode = resp.message.statusCode || 500; if (statusCode < 200 || statusCode > 299) {
if (statusCode >= 400) { throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
throw new Error(`(${statusCode}) ${body}`);
} }
const parsed = JSON.parse(body);
return parsed['signedJwt']; const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
return result.signedJwt;
} catch (err) { } catch (err) {
throw new Error(`Failed to sign JWT using ${serviceAccount}: ${err}`); throw new Error(`Failed to sign JWT using ${this.#serviceAccount}: ${err}`);
} }
} }
/** /**
* getProjectID returns the project ID. If an override was given, the override * createCredentialsFile writes a Workload Identity Federation credential file
* is returned. Otherwise, this will be the project ID that was extracted from * to disk at the specific outputPath.
* the service account key JSON.
*/
async getProjectID(): Promise<string> {
return this.#projectID;
}
/**
* getServiceAccount returns the service account email for the authentication,
* extracted from the input parameter.
*/
async getServiceAccount(): Promise<string> {
return this.#serviceAccount;
}
/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/ */
async createCredentialsFile(outputPath: string): Promise<string> { async createCredentialsFile(outputPath: string): Promise<string> {
const requestURL = new URL(this.#oidcTokenRequestURL); const requestURL = new URL(this.#githubOIDCTokenRequestURL);
// Append the audience value to the request. // Append the audience value to the request.
const params = requestURL.searchParams; const params = requestURL.searchParams;
params.set('audience', this.#audience); params.set('audience', this.#githubOIDCTokenAudience);
requestURL.search = params.toString(); requestURL.search = params.toString();
const data = {
type: 'external_account', const data: Record<string, any> = {
audience: `//iam.googleapis.com/${this.#providerID}`, type: `external_account`,
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', audience: this.#audience,
token_url: 'https://sts.googleapis.com/v1/token', subject_token_type: `urn:ietf:params:oauth:token-type:jwt`,
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`, token_url: `${this.#endpoints.sts}/token`,
credential_source: { credential_source: {
url: requestURL, url: requestURL,
headers: { headers: {
Authorization: `Bearer ${this.#oidcTokenRequestToken}`, Authorization: `Bearer ${this.#githubOIDCTokenRequestToken}`,
}, },
format: { format: {
type: 'json', type: `json`,
subject_token_field_name: 'value', subject_token_field_name: `value`,
}, },
}, },
}; };
// Only request impersonation if a service account was given, otherwise use
// the WIF identity directly.
if (this.#serviceAccount) {
data.service_account_impersonation_url = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`;
}
this.#logger.withNamespace('createCredentialsFile').debug({ outputPath: outputPath });
return await writeSecureFile(outputPath, JSON.stringify(data)); return await writeSecureFile(outputPath, JSON.stringify(data));
} }
} }

122
src/logger.ts Normal file
View File

@ -0,0 +1,122 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AnnotationProperties,
debug as logDebug,
error as logError,
info as logInfo,
notice as logNotice,
warning as logWarning,
} from '@actions/core';
/**
* LoggerFunction is the type signature of a log function for the GitHub Actions
* SDK.
*/
type LoggerFunction = (message: string, properties?: AnnotationProperties) => void;
/**
* Logger is a class that handles namespaced logging.
*/
export class Logger {
readonly #namespace?: string;
constructor(namespace?: string) {
this.#namespace = namespace;
}
withNamespace(namespace: string): Logger {
const { constructor } = Object.getPrototypeOf(this);
if (this.#namespace) {
return new constructor(`${this.#namespace}.${namespace}`);
}
return new constructor(namespace);
}
debug(...args: any[]) {
this.logMessage(logDebug, ...args);
}
error(...args: any[]) {
this.logMessage(logError, ...args);
}
info(...args: any[]) {
this.logMessage(logInfo, ...args);
}
notice(...args: any[]) {
this.logMessage(logNotice, ...args);
}
warning(...args: any[]) {
this.logMessage(logWarning, ...args);
}
protected logMessage(loggerFn: LoggerFunction, ...args: object[]) {
if (!args || args.length === 0) {
return;
}
let message = '';
if (this.#namespace) {
message += this.#namespace + ': ';
}
for (let i = 0; i < args.length; i++) {
const obj = args[i];
if (typeof obj === 'undefined' || obj === undefined || obj === null) {
continue;
}
if (typeof obj === 'string' || obj instanceof String) {
message += obj;
} else {
message += JSON.stringify(obj, null, 2);
}
if (i < args.length - 1) {
message += ', ';
}
}
loggerFn(message);
}
}
/**
* NullLogger is a logger that doesn't actually emit any output.
*/
export class NullLogger extends Logger {
debug(...args: any[]) {
this.logMessage(() => {}, ...args);
}
error(...args: any[]) {
this.logMessage(() => {}, ...args);
}
info(...args: any[]) {
this.logMessage(() => {}, ...args);
}
notice(...args: any[]) {
this.logMessage(() => {}, ...args);
}
warning(...args: any[]) {
this.logMessage(() => {}, ...args);
}
}

View File

@ -15,16 +15,13 @@
import { join as pathjoin } from 'path'; import { join as pathjoin } from 'path';
import { import {
debug as logDebug,
exportVariable, exportVariable,
getBooleanInput, getBooleanInput,
getIDToken, getIDToken,
getInput, getInput,
info as logInfo,
setFailed, setFailed,
setOutput, setOutput,
setSecret, setSecret,
warning as logWarning,
} from '@actions/core'; } from '@actions/core';
import { import {
errorMessage, errorMessage,
@ -37,10 +34,19 @@ import {
withRetries, withRetries,
} from '@google-github-actions/actions-utils'; } from '@google-github-actions/actions-utils';
import { WorkloadIdentityClient } from './client/workload_identity_client'; import {
import { CredentialsJSONClient } from './client/credentials_json_client'; AuthClient,
import { AuthClient } from './client/auth_client'; IAMCredentialsClient,
import { buildDomainWideDelegationJWT, generateCredentialsFilename } from './utils'; ServiceAccountKeyClient,
WorkloadIdentityFederationClient,
} from './base';
import { Logger } from './logger';
import {
buildDomainWideDelegationJWT,
computeProjectID,
computeServiceAccountEmail,
generateCredentialsFilename,
} from './utils';
const secretsWarning = const secretsWarning =
`If you are specifying input values via GitHub secrets, ensure the secret ` + `If you are specifying input values via GitHub secrets, ensure the secret ` +
@ -57,9 +63,11 @@ const oidcWarning =
* Executes the main action. * Executes the main action.
*/ */
async function run(): Promise<void> { async function run(): Promise<void> {
const logger = new Logger();
// Warn if pinned to HEAD // Warn if pinned to HEAD
if (isPinnedToHead()) { if (isPinnedToHead()) {
logWarning(pinnedToHeadWarning('v1')); logger.warning(pinnedToHeadWarning('v2'));
} }
const retries = Number(getInput('retries')); const retries = Number(getInput('retries'));
@ -69,7 +77,7 @@ async function run(): Promise<void> {
const backoffLimit = Number(getInput('backoff_limit')) || undefined; const backoffLimit = Number(getInput('backoff_limit')) || undefined;
try { try {
const mainWithRetries = withRetries(main, { const mainWithRetries = withRetries(async () => main(logger), {
retries: retries, retries: retries,
backoff: backoff, backoff: backoff,
backoffLimit: backoffLimit, backoffLimit: backoffLimit,
@ -85,17 +93,25 @@ async function run(): Promise<void> {
/** /**
* Main wraps the main action logic into a function to be used as a parameter to the withRetries function. * Main wraps the main action logic into a function to be used as a parameter to the withRetries function.
*/ */
async function main() { async function main(logger: Logger) {
// Load configuration. // Load configuration.
const projectID = getInput('project_id'); const projectID = computeProjectID(
const workloadIdentityProvider = getInput('workload_identity_provider'); getInput(`project_id`),
const serviceAccount = getInput('service_account'); getInput(`service_account`),
const audience = getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`; getInput(`credentials_json`),
const credentialsJSON = getInput('credentials_json'); );
const createCredentialsFile = getBooleanInput('create_credentials_file'); const workloadIdentityProvider = getInput(`workload_identity_provider`);
const exportEnvironmentVariables = getBooleanInput('export_environment_variables'); const serviceAccount = computeServiceAccountEmail(
const tokenFormat = getInput('token_format'); getInput(`service_account`),
const delegates = parseCSV(getInput('delegates')); getInput('credentials_json'),
);
const oidcTokenAudience =
getInput(`audience`) || `https://iam.googleapis.com/${workloadIdentityProvider}`;
const credentialsJSON = getInput(`credentials_json`);
const createCredentialsFile = getBooleanInput(`create_credentials_file`);
const exportEnvironmentVariables = getBooleanInput(`export_environment_variables`);
const tokenFormat = getInput(`token_format`);
const delegates = parseCSV(getInput(`delegates`));
// Ensure exactly one of workload_identity_provider and credentials_json was // Ensure exactly one of workload_identity_provider and credentials_json was
// provided. // provided.
@ -107,19 +123,10 @@ async function main() {
); );
} }
// Ensure a service_account was provided if using WIF.
if (workloadIdentityProvider && !serviceAccount) {
throw new Error(
'The GitHub Action workflow must specify a "service_account" to ' +
'impersonate when using "workload_identity_provider"! ' +
secretsWarning,
);
}
// Instantiate the correct client based on the provided input parameters. // Instantiate the correct client based on the provided input parameters.
let client: AuthClient; let client: AuthClient;
if (workloadIdentityProvider) { if (workloadIdentityProvider) {
logDebug(`Using workload identity provider "${workloadIdentityProvider}"`); logger.debug(`Using workload identity provider "${workloadIdentityProvider}"`);
// If we're going to do the OIDC dance, we need to make sure these values // If we're going to do the OIDC dance, we need to make sure these values
// are set. If they aren't, core.getIDToken() will fail and so will // are set. If they aren't, core.getIDToken() will fail and so will
@ -130,21 +137,19 @@ async function main() {
throw new Error(oidcWarning); throw new Error(oidcWarning);
} }
const token = await getIDToken(audience); const oidcToken = await getIDToken(oidcTokenAudience);
client = new WorkloadIdentityClient({ client = new WorkloadIdentityFederationClient(logger, {
projectID: projectID, githubOIDCToken: oidcToken,
providerID: workloadIdentityProvider, githubOIDCTokenRequestURL: oidcTokenRequestURL,
githubOIDCTokenRequestToken: oidcTokenRequestToken,
githubOIDCTokenAudience: oidcTokenAudience,
workloadIdentityProviderName: workloadIdentityProvider,
serviceAccount: serviceAccount, serviceAccount: serviceAccount,
token: token,
audience: audience,
oidcTokenRequestToken: oidcTokenRequestToken,
oidcTokenRequestURL: oidcTokenRequestURL,
}); });
} else { } else {
logDebug(`Using credentials JSON`); logger.debug(`Using credentials JSON`);
client = new CredentialsJSONClient({ client = new ServiceAccountKeyClient(logger, {
projectID: projectID, serviceAccountKey: credentialsJSON,
credentialsJSON: credentialsJSON,
}); });
} }
@ -153,7 +158,7 @@ async function main() {
// fails, which means continue-on-error actions will still have the file // fails, which means continue-on-error actions will still have the file
// available. // available.
if (createCredentialsFile) { if (createCredentialsFile) {
logDebug(`Creating credentials file`); logger.debug(`Creating credentials file`);
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE // Note: We explicitly and intentionally export to GITHUB_WORKSPACE
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with // instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
@ -180,7 +185,7 @@ async function main() {
// repository. // repository.
const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace); const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace);
if (githubWorkspaceIsEmpty) { if (githubWorkspaceIsEmpty) {
logWarning( logger.warning(
`The "create_credentials_file" option is true, but the current ` + `The "create_credentials_file" option is true, but the current ` +
`GitHub workspace is empty. Did you forget to use ` + `GitHub workspace is empty. Did you forget to use ` +
`"actions/checkout" before this step? If you do not intend to ` + `"actions/checkout" before this step? If you do not intend to ` +
@ -193,7 +198,7 @@ async function main() {
const outputFile = generateCredentialsFilename(); const outputFile = generateCredentialsFilename();
const outputPath = pathjoin(githubWorkspace, outputFile); const outputPath = pathjoin(githubWorkspace, outputFile);
const credentialsPath = await client.createCredentialsFile(outputPath); const credentialsPath = await client.createCredentialsFile(outputPath);
logInfo(`Created credentials file at "${credentialsPath}"`); logger.info(`Created credentials file at "${credentialsPath}"`);
// Output to be available to future steps. // Output to be available to future steps.
setOutput('credentials_file_path', credentialsPath); setOutput('credentials_file_path', credentialsPath);
@ -202,28 +207,47 @@ async function main() {
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to // CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to
// use a specific credential file (subject to change and equivalent to // use a specific credential file (subject to change and equivalent to
// auth/credential_file_override). // auth/credential_file_override).
exportVariableAndWarn('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath); exportVariable('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath);
// GOOGLE_APPLICATION_CREDENTIALS is used by Application Default // GOOGLE_APPLICATION_CREDENTIALS is used by Application Default
// Credentials in all GCP client libraries. // Credentials in all GCP client libraries.
exportVariableAndWarn('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath); exportVariable('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
// GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions. // GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions.
exportVariableAndWarn('GOOGLE_GHA_CREDS_PATH', credentialsPath); exportVariable('GOOGLE_GHA_CREDS_PATH', credentialsPath);
} }
} }
// Set the project ID environment variables to the computed values. // Set the project ID environment variables to the computed values.
const computedProjectID = await client.getProjectID(); if (!projectID) {
setOutput('project_id', computedProjectID); logger.warning(
`Unable to compute project ID from inputs, skipping export. Please ` +
`specify the "project_id" input directly.`,
);
} else {
setOutput('project_id', projectID);
if (exportEnvironmentVariables) { if (exportEnvironmentVariables) {
exportVariableAndWarn('CLOUDSDK_CORE_PROJECT', computedProjectID); exportVariable('CLOUDSDK_CORE_PROJECT', projectID);
exportVariableAndWarn('CLOUDSDK_PROJECT', computedProjectID); exportVariable('CLOUDSDK_PROJECT', projectID);
exportVariableAndWarn('GCLOUD_PROJECT', computedProjectID); exportVariable('GCLOUD_PROJECT', projectID);
exportVariableAndWarn('GCP_PROJECT', computedProjectID); exportVariable('GCP_PROJECT', projectID);
exportVariableAndWarn('GOOGLE_CLOUD_PROJECT', computedProjectID); exportVariable('GOOGLE_CLOUD_PROJECT', projectID);
} }
}
// Attempt to generate a token. This will ensure the action correctly errors
// if the credentials are misconfigured. This is also required so the value
// can be set as an output for future authentication calls.
const authToken = await client.getToken();
logger.debug(`Successfully generated auth token`);
setSecret(authToken);
setOutput('auth_token', authToken);
// Create the credential client, we might not use it, but it's basically free.
const iamCredentialsClient = new IAMCredentialsClient(logger, {
authToken: authToken,
});
switch (tokenFormat) { switch (tokenFormat) {
case '': { case '': {
@ -233,20 +257,28 @@ async function main() {
break; break;
} }
case 'access_token': { case 'access_token': {
logDebug(`Creating access token`); logger.debug(`Creating access token`);
const accessTokenLifetime = parseDuration(getInput('access_token_lifetime')); const accessTokenLifetime = parseDuration(getInput('access_token_lifetime'));
const accessTokenScopes = parseCSV(getInput('access_token_scopes')); const accessTokenScopes = parseCSV(getInput('access_token_scopes'));
const accessTokenSubject = getInput('access_token_subject'); const accessTokenSubject = getInput('access_token_subject');
const serviceAccount = await client.getServiceAccount();
// Ensure a service_account was provided if using WIF.
if (!serviceAccount) {
throw new Error(
'The GitHub Action workflow must specify a "service_account" to ' +
'use when generating an OAuth 2.0 Access Token. ' +
secretsWarning,
);
}
// If a subject was provided, use the traditional OAuth 2.0 flow to // If a subject was provided, use the traditional OAuth 2.0 flow to
// perform Domain-Wide Delegation. Otherwise, use the modern IAM // perform Domain-Wide Delegation. Otherwise, use the modern IAM
// Credentials endpoints. // Credentials endpoints.
let accessToken, expiration; let accessToken;
if (accessTokenSubject) { if (accessTokenSubject) {
if (accessTokenLifetime > 3600) { if (accessTokenLifetime > 3600) {
logInfo( logger.info(
`An access token subject was specified, triggering Domain-Wide ` + `An access token subject was specified, triggering Domain-Wide ` +
`Delegation flow. This flow does not support specifying an ` + `Delegation flow. This flow does not support specifying an ` +
`access token lifetime of greater than 1 hour.`, `access token lifetime of greater than 1 hour.`,
@ -259,39 +291,45 @@ async function main() {
accessTokenScopes, accessTokenScopes,
accessTokenLifetime, accessTokenLifetime,
); );
const signedJWT = await client.signJWT(unsignedJWT, delegates); const signedJWT = await client.signJWT(unsignedJWT);
({ accessToken, expiration } = await client.googleOAuthToken(signedJWT));
accessToken = await iamCredentialsClient.generateDomainWideDelegationAccessToken(signedJWT);
} else { } else {
const authToken = await client.getAuthToken(); accessToken = await iamCredentialsClient.generateAccessToken({
({ accessToken, expiration } = await client.googleAccessToken(authToken, {
serviceAccount, serviceAccount,
delegates, delegates,
scopes: accessTokenScopes, scopes: accessTokenScopes,
lifetime: accessTokenLifetime, lifetime: accessTokenLifetime,
})); });
} }
setSecret(accessToken); setSecret(accessToken);
setOutput('access_token', accessToken); setOutput('access_token', accessToken);
setOutput('access_token_expiration', expiration);
break; break;
} }
case 'id_token': { case 'id_token': {
logDebug(`Creating id token`); logger.debug(`Creating id token`);
const idTokenAudience = getInput('id_token_audience', { required: true }); const idTokenAudience = getInput('id_token_audience', { required: true });
const idTokenIncludeEmail = getBooleanInput('id_token_include_email'); const idTokenIncludeEmail = getBooleanInput('id_token_include_email');
const serviceAccount = await client.getServiceAccount();
const authToken = await client.getAuthToken(); // Ensure a service_account was provided if using WIF.
const { token } = await client.googleIDToken(authToken, { if (!serviceAccount) {
throw new Error(
'The GitHub Action workflow must specify a "service_account" to ' +
'use when generating an OAuth 2.0 Access Token. ' +
secretsWarning,
);
}
const idToken = await iamCredentialsClient.generateIDToken({
serviceAccount, serviceAccount,
audience: idTokenAudience, audience: idTokenAudience,
delegates, delegates,
includeEmail: idTokenIncludeEmail, includeEmail: idTokenIncludeEmail,
}); });
setSecret(token); setSecret(idToken);
setOutput('id_token', token); setOutput('id_token', idToken);
break; break;
} }
default: { default: {
@ -300,26 +338,4 @@ async function main() {
} }
} }
/**
* exportVariableAndWarn exports the given key as an environment variable set to
* the provided value. If a value already exists, it is overwritten and an
* warning is emitted.
*
* @param key Environment variable key.
* @param value Environment variable value.
*/
function exportVariableAndWarn(key: string, value: string) {
const existing = process.env[key];
if (existing && existing !== value) {
logWarning(
`Overwriting existing environment variable ${key}:
- ${JSON.stringify(existing)}
+ ${JSON.stringify(value)}
`.trim(),
);
}
exportVariable(key, value);
}
run(); run();

View File

@ -12,23 +12,36 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { getBooleanInput, setFailed, info as logInfo } from '@actions/core'; import { getBooleanInput, setFailed } from '@actions/core';
import { errorMessage, forceRemove } from '@google-github-actions/actions-utils'; import { errorMessage, forceRemove } from '@google-github-actions/actions-utils';
import { Logger } from './logger';
/** /**
* Executes the post action, documented inline. * Executes the post action, documented inline.
*/ */
export async function run(): Promise<void> { export async function run() {
const logger = new Logger();
try { try {
main(logger);
} catch (err) {
const msg = errorMessage(err);
setFailed(`google-github-actions/auth post failed with: ${msg}`);
}
}
async function main(logger: Logger) {
const createCredentials = getBooleanInput('create_credentials_file'); const createCredentials = getBooleanInput('create_credentials_file');
if (!createCredentials) { if (!createCredentials) {
logInfo(`Skipping credential cleanup - "create_credentials_file" is false.`); logger.info(`Skipping credential cleanup - "create_credentials_file" is false.`);
return; return;
} }
const cleanupCredentials = getBooleanInput('cleanup_credentials'); const cleanupCredentials = getBooleanInput('cleanup_credentials');
if (!cleanupCredentials) { if (!cleanupCredentials) {
logInfo(`Skipping credential cleanup - "cleanup_credentials" is false.`); logger.info(`Skipping credential cleanup - "cleanup_credentials" is false.`);
return; return;
} }
@ -38,17 +51,13 @@ export async function run(): Promise<void> {
// another environment variable manually. // another environment variable manually.
const credentialsPath = process.env['GOOGLE_GHA_CREDS_PATH']; const credentialsPath = process.env['GOOGLE_GHA_CREDS_PATH'];
if (!credentialsPath) { if (!credentialsPath) {
logInfo(`Skipping credential cleanup - $GOOGLE_GHA_CREDS_PATH is not set.`); logger.info(`Skipping credential cleanup - $GOOGLE_GHA_CREDS_PATH is not set.`);
return; return;
} }
// Remove the file. // Remove the file.
await forceRemove(credentialsPath); await forceRemove(credentialsPath);
logInfo(`Removed exported credentials at "${credentialsPath}".`); logger.info(`Removed exported credentials at "${credentialsPath}".`);
} catch (err) {
const msg = errorMessage(err);
setFailed(`google-github-actions/auth post failed with: ${msg}`);
}
} }
run(); run();

View File

@ -12,7 +12,18 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { randomFilename } from '@google-github-actions/actions-utils'; import {
isServiceAccountKey,
parseCredential,
randomFilename,
} from '@google-github-actions/actions-utils';
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const { version: appVersion } = require('../package.json');
// userAgent is the default user agent.
export const userAgent = `google-github-actions:auth/${appVersion}`;
/** /**
* buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a * buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a
@ -50,6 +61,86 @@ export function buildDomainWideDelegationJWT(
return JSON.stringify(body); return JSON.stringify(body);
} }
/**
* computeProjectID attempts to compute the best project ID from the given
* inputs.
*/
export function computeProjectID(
projectID?: string,
serviceAccount?: string,
serviceAccountKeyJSON?: string,
): string | undefined {
if (projectID) {
return projectID;
}
// sa-name@<project-id>.iam.gserviceaccount.com
const fromEmail = projectIDFromServiceAccountEmail(serviceAccount);
if (fromEmail) {
return fromEmail;
}
// Extract from the key
if (serviceAccountKeyJSON) {
const credential = parseCredential(serviceAccountKeyJSON);
if (isServiceAccountKey(credential) && credential.project_id) {
return credential.project_id;
}
}
return undefined;
}
/**
* getServiceAccountEmail extracts the service account email from the given
* fields.
*/
export function computeServiceAccountEmail(
serviceAccountEmail?: string,
serviceAccountKeyJSON?: string,
): string | undefined {
if (serviceAccountEmail) {
return serviceAccountEmail;
}
if (serviceAccountKeyJSON) {
const credential = parseCredential(serviceAccountKeyJSON);
if (isServiceAccountKey(credential) && credential.client_email) {
return credential.client_email;
}
}
return undefined;
}
/**
* projectIDFromServiceAccountEmail attempts to extract the project ID from the
* service account email.
*/
export function projectIDFromServiceAccountEmail(serviceAccount?: string): string | null {
if (!serviceAccount) {
return null;
}
const emailParts = serviceAccount.split('@');
if (emailParts.length !== 2) {
return null;
}
const addressParts = emailParts[1].split('.');
if (addressParts.length < 2) {
return null;
}
return addressParts[0];
}
/**
* expandEndpoint expands the input url relative to the universe.
*/
export function expandEndpoint(input: string, universe: string): string {
return (input || '').replace(/{universe}/g, universe).replace(/\/+$/, '');
}
/** /**
* generateCredentialsFilename creates a predictable filename under which * generateCredentialsFilename creates a predictable filename under which
* credentials are written. This string is the filename, not the filepath. It must match the format: * credentials are written. This string is the filename, not the filepath. It must match the format:

View File

@ -21,7 +21,8 @@ import { tmpdir } from 'os';
import { randomFilename } from '@google-github-actions/actions-utils'; import { randomFilename } from '@google-github-actions/actions-utils';
import { CredentialsJSONClient } from '../../src/client/credentials_json_client'; import { NullLogger } from '../../src/logger';
import { ServiceAccountKeyClient } from '../../src/client/credentials_json_client';
// Yes, this is a real private key. No, it's not valid for authenticating // Yes, this is a real private key. No, it's not valid for authenticating
// Google Cloud. // Google Cloud.
@ -40,40 +41,40 @@ const credentialsJSON = `
} }
`; `;
describe('CredentialsJSONClient', () => { describe('ServiceAccountKeyClient', () => {
describe('#parseServiceAccountKeyJSON', () => { describe('#parseServiceAccountKeyJSON', () => {
it('throws exception on invalid json', async () => { it('throws exception on invalid json', async () => {
assert.rejects(async () => { assert.rejects(async () => {
new CredentialsJSONClient({ new ServiceAccountKeyClient(new NullLogger(), {
credentialsJSON: 'invalid json', serviceAccountKey: 'invalid json',
}); });
}, SyntaxError); }, SyntaxError);
}); });
it('handles base64', async () => { it('handles base64', async () => {
assert.rejects(async () => { assert.rejects(async () => {
new CredentialsJSONClient({ new ServiceAccountKeyClient(new NullLogger(), {
credentialsJSON: 'base64', serviceAccountKey: 'base64',
}); });
}, SyntaxError); }, SyntaxError);
}); });
}); });
describe('#getAuthToken', () => { describe('#getToken', () => {
it('signs a jwt', async () => { it('gets a token', async () => {
const client = new CredentialsJSONClient({ const client = new ServiceAccountKeyClient(new NullLogger(), {
credentialsJSON: credentialsJSON, serviceAccountKey: credentialsJSON,
}); });
const token = await client.getAuthToken(); const token = await client.getToken();
assert.ok(token); assert.ok(token);
}); });
}); });
describe('#signJWT', () => { describe('#signJWT', () => {
it('signs a jwt', async () => { it('signs a jwt', async () => {
const client = new CredentialsJSONClient({ const client = new ServiceAccountKeyClient(new NullLogger(), {
credentialsJSON: credentialsJSON, serviceAccountKey: credentialsJSON,
}); });
const token = await client.signJWT('thisismy.jwt'); const token = await client.signJWT('thisismy.jwt');
@ -81,43 +82,11 @@ describe('CredentialsJSONClient', () => {
}); });
}); });
describe('#getProjectID', () => {
it('extracts project ID from the json', async () => {
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-project');
});
it('prefers the override if given', async () => {
const client = new CredentialsJSONClient({
projectID: 'my-other-project',
credentialsJSON: credentialsJSON,
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-other-project');
});
});
describe('#getServiceAccount', () => {
it('extracts service account from the json', async () => {
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
});
const result = await client.getServiceAccount();
assert.deepStrictEqual(result, 'my-service-account@my-project.iam.gserviceaccount.com');
});
});
describe('#createCredentialsFile', () => { describe('#createCredentialsFile', () => {
it('writes the file', async () => { it('writes the file', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename()); const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new CredentialsJSONClient({ const client = new ServiceAccountKeyClient(new NullLogger(), {
credentialsJSON: credentialsJSON, serviceAccountKey: credentialsJSON,
}); });
const exp = JSON.parse(credentialsJSON); const exp = JSON.parse(credentialsJSON);

View File

@ -21,80 +21,54 @@ import { readFileSync } from 'fs';
import { randomFilename } from '@google-github-actions/actions-utils'; import { randomFilename } from '@google-github-actions/actions-utils';
import { WorkloadIdentityClient } from '../../src/client/workload_identity_client'; import { NullLogger } from '../../src/logger';
import { WorkloadIdentityFederationClient } from '../../src/client/workload_identity_client';
describe('WorkloadIdentityClient', () => {
describe('#getProjectID', () => {
it('extracts project ID from the service account email', async () => {
const client = new WorkloadIdentityClient({
providerID: 'my-provider',
token: 'my-token',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-project');
});
it('prefers the override if given', async () => {
const client = new WorkloadIdentityClient({
projectID: 'my-other-project',
providerID: 'my-provider',
token: 'my-token',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-other-project');
});
it('throws an error when extraction fails', async () => {
assert.rejects(async () => {
return new WorkloadIdentityClient({
providerID: 'my-provider',
token: 'my-token',
serviceAccount: 'my-service@developers.google.com',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
}, Error);
});
});
describe('#getServiceAccount', () => {
it('returns the provided value', async () => {
const client = new WorkloadIdentityClient({
projectID: 'my-project',
providerID: 'my-provider',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
token: 'my-token',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const result = await client.getServiceAccount();
assert.deepStrictEqual(result, 'my-service@my-project.iam.gserviceaccount.com');
});
});
describe('WorkloadIdentityFederationClient', () => {
describe('#createCredentialsFile', () => { describe('#createCredentialsFile', () => {
it('writes the file', async () => { it('writes the file', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename()); const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new WorkloadIdentityClient({ const client = new WorkloadIdentityFederationClient(new NullLogger(), {
projectID: 'my-project', githubOIDCToken: 'my-token',
providerID: 'my-provider', githubOIDCTokenRequestURL: 'https://example.com/',
githubOIDCTokenRequestToken: 'token',
githubOIDCTokenAudience: 'my-aud',
workloadIdentityProviderName: 'my-provider',
});
const exp = {
audience: '//iam.googleapis.com/my-provider',
credential_source: {
format: {
subject_token_field_name: 'value',
type: 'json',
},
headers: {
Authorization: 'Bearer token',
},
url: 'https://example.com/?audience=my-aud',
},
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
type: 'external_account',
};
const pth = await client.createCredentialsFile(outputFile);
const data = readFileSync(pth);
const got = JSON.parse(data.toString('utf8'));
assert.deepStrictEqual(got, exp);
});
it('writes the file with impersonation', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new WorkloadIdentityFederationClient(new NullLogger(), {
githubOIDCToken: 'my-token',
githubOIDCTokenRequestURL: 'https://example.com/',
githubOIDCTokenRequestToken: 'token',
githubOIDCTokenAudience: 'my-aud',
workloadIdentityProviderName: 'my-provider',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com', serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
token: 'my-token',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
}); });
const exp = { const exp = {

View File

@ -15,10 +15,17 @@
import { describe, it } from 'node:test'; import { describe, it } from 'node:test';
import assert from 'node:assert'; import assert from 'node:assert';
import { buildDomainWideDelegationJWT, generateCredentialsFilename } from '../src/utils'; import {
buildDomainWideDelegationJWT,
computeProjectID,
computeServiceAccountEmail,
expandEndpoint,
generateCredentialsFilename,
projectIDFromServiceAccountEmail,
} from '../src/utils';
describe('Utils', () => { describe('Utils', async () => {
describe('#buildDomainWideDelegationJWT', () => { describe('#buildDomainWideDelegationJWT', async () => {
const cases = [ const cases = [
{ {
name: 'default', name: 'default',
@ -57,7 +64,152 @@ describe('Utils', () => {
}); });
}); });
describe('#generateCredentialsFilename', () => { describe('#computeProjectID', async () => {
const cases = [
{
name: 'directly given',
projectID: 'my-project',
exp: 'my-project',
},
{
name: 'from service account email',
serviceAccountEmail: 'my-account@my-project.iam.gserviceaccount.com',
exp: 'my-project',
},
{
name: 'from json credential',
serviceAccountKeyJSON: '{"type":"service_account", "project_id": "my-project"}',
exp: 'my-project',
},
{
name: 'from json credential invalid',
serviceAccountKeyJSON: '{"nope": "foo@bar.com"}',
exp: undefined,
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = computeProjectID(
tc.projectID,
tc.serviceAccountEmail,
tc.serviceAccountKeyJSON,
);
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#computeServiceAccountEmail', async () => {
const cases = [
{
name: 'directly given',
serviceAccountEmail: 'foo@bar.com',
exp: 'foo@bar.com',
},
{
name: 'from json credential',
serviceAccountKeyJSON: '{"type":"service_account", "client_email": "foo@bar.com"}',
exp: 'foo@bar.com',
},
{
name: 'invalid json credential',
serviceAccountKeyJSON: '{"nope": "foo@bar.com"}',
exp: undefined,
},
{
name: 'nothing',
exp: undefined,
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = computeServiceAccountEmail(tc.serviceAccountEmail, tc.serviceAccountKeyJSON);
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#projectIDFromServiceAccountEmail', async () => {
const cases = [
{
name: 'empty',
input: '',
exp: null,
},
{
name: 'not an email',
input: 'not a service account',
exp: null,
},
{
name: 'invalid email',
input: 'foo@abc',
exp: null,
},
{
name: 'returns project',
input: 'test-sa@my-project.iam.gserviceaccount.com',
exp: 'my-project',
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = projectIDFromServiceAccountEmail(tc.input);
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#expandEndpoint', async () => {
const cases = [
{
name: 'empty',
endpoint: '',
universe: '',
exp: '',
},
{
name: 'no match',
endpoint: 'https://www.googleapis.com',
universe: 'foobar',
exp: 'https://www.googleapis.com',
},
{
name: 'removes trailing slash',
endpoint: 'https://www.googleapis.com/',
exp: 'https://www.googleapis.com',
},
{
name: 'removes trailing slashes',
endpoint: 'https://www.googleapis.com/////',
exp: 'https://www.googleapis.com',
},
{
name: 'replaces {universe}',
endpoint: 'https://www.{universe}',
universe: 'foo.bar',
exp: 'https://www.foo.bar',
},
{
name: 'replaces multiple {universe}',
endpoint: 'https://www.{universe}.{universe}',
universe: 'foo.bar',
exp: 'https://www.foo.bar.foo.bar',
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = expandEndpoint(tc.endpoint, tc.universe || '');
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#generateCredentialsFilename', async () => {
it('returns a string matching the regex', () => { it('returns a string matching the regex', () => {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const filename = generateCredentialsFilename(); const filename = generateCredentialsFilename();