This commit is contained in:
2025-09-03 08:29:15 -07:00
parent 56f55daa85
commit 2898ccdb79
164 changed files with 275 additions and 25235 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Ignore environment files
.env
# IDE files
.vscode/
.idea/
# Build artifacts
bin/
dist/
# OS files
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,80 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/joho/godotenv"
"github.com/sstent/go-garth"
)
func main() {
// Load environment variables from .env file
if err := godotenv.Load("../../../.env"); err != nil {
log.Println("Note: Using system environment variables (no .env file found)")
}
// Get credentials from environment
username := os.Getenv("GARMIN_USERNAME")
password := os.Getenv("GARMIN_PASSWORD")
if username == "" || password == "" {
log.Fatal("GARMIN_USERNAME or GARMIN_PASSWORD not set in environment")
}
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Create token storage and authenticator
storage := garth.NewMemoryStorage()
auth := garth.NewAuthenticator(garth.ClientOptions{
Storage: storage,
TokenURL: "https://connectapi.garmin.com/oauth-service/oauth/token",
Timeout: 30 * time.Second,
})
// Authenticate
token, err := auth.Login(ctx, username, password, "")
if err != nil {
log.Fatalf("Authentication failed: %v", err)
}
log.Printf("Authenticated successfully! Token expires at: %s", token.Expiry.Format(time.RFC3339))
// Create HTTP client with authentication transport
httpClient := &http.Client{
Transport: garth.NewAuthTransport(auth.(*garth.GarthAuthenticator), storage, nil),
}
// Create API client
apiClient := garth.NewAPIClient("https://connectapi.garmin.com", httpClient)
// Create activity service
activityService := garth.NewActivityService(apiClient)
// List last 20 activities
activities, err := activityService.List(ctx, garth.ActivityListOptions{
Limit: 20,
})
if err != nil {
log.Fatalf("Failed to get activities: %v", err)
}
// Print activities
fmt.Println("\nLast 20 Activities:")
fmt.Println("=======================================")
for i, activity := range activities {
fmt.Printf("%d. %s [%s] - %s\n", i+1,
activity.Name,
activity.Type,
activity.StartTime.Format("2006-01-02 15:04"))
fmt.Printf(" Distance: %.2f km, Duration: %.0f min\n\n",
activity.Distance/1000,
activity.Duration/60)
}
fmt.Println("Example completed successfully!")
}

View File

@@ -0,0 +1,8 @@
module github.com/sstent/go-garth/examples/activities
go 1.22
require (
github.com/joho/godotenv v1.5.1
github.com/sstent/go-garth v0.1.0
)

View File

@@ -0,0 +1,80 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/joho/godotenv"
"github.com/sstent/go-garth"
)
func main() {
// Load environment variables from .env file
if err := godotenv.Load("../../.env"); err != nil {
log.Println("Note: Using system environment variables (no .env file found)")
}
// Get credentials from environment
username := os.Getenv("GARMIN_USERNAME")
password := os.Getenv("GARMIN_PASSWORD")
if username == "" || password == "" {
log.Fatal("GARMIN_USERNAME or GARMIN_PASSWORD not set in environment")
}
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Create token storage and authenticator
storage := garth.NewMemoryStorage()
auth := garth.NewAuthenticator(garth.ClientOptions{
Storage: storage,
TokenURL: "https://connectapi.garmin.com/oauth-service/oauth/token",
Timeout: 30 * time.Second,
})
// Authenticate
token, err := auth.Login(ctx, username, password, "")
if err != nil {
log.Fatalf("Authentication failed: %v", err)
}
log.Printf("Authenticated successfully! Token expires at: %s", token.Expiry.Format(time.RFC3339))
// Create HTTP client with authentication transport
httpClient := &http.Client{
Transport: garth.NewAuthTransport(auth.(*garth.GarthAuthenticator), storage, nil),
}
// Create API client
apiClient := garth.NewAPIClient("https://connectapi.garmin.com", httpClient)
// Create activity service
activityService := garth.NewActivityService(apiClient)
// List last 20 activities
activities, err := activityService.List(ctx, garth.ActivityListOptions{
Limit: 20,
})
if err != nil {
log.Fatalf("Failed to get activities: %v", err)
}
// Print activities
fmt.Println("\nLast 20 Activities:")
fmt.Println("=======================================")
for i, activity := range activities {
fmt.Printf("%d. %s [%s] - %s\n", i+1,
activity.Name,
activity.Type,
activity.StartTime.Format("2006-01-02 15:04"))
fmt.Printf(" Distance: %.2f km, Duration: %.0f min\n\n",
activity.Distance/1000,
activity.Duration/60)
}
fmt.Println("Example completed successfully!")
}

View File

@@ -9,8 +9,8 @@ import (
)
func main() {
// Create a new client
client := garth.New()
// Create a new client (placeholder - actual client creation depends on package structure)
// client := garth.NewClient(nil)
// For demonstration, we'll use a mock server or skip authentication
// In real usage, you would authenticate first:
@@ -42,12 +42,31 @@ func main() {
// List workouts with options
opts := garth.WorkoutListOptions{
Limit: 10,
Offset: 0,
StartDate: time.Now().AddDate(0, -1, 0), // Last month
EndDate: time.Now(),
SortBy: "createdDate",
SortOrder: "desc",
}
fmt.Printf("Workout list options: %+v\n", opts)
// List workouts with pagination
paginatedOpts := garth.WorkoutListOptions{
Limit: 5,
Offset: 10,
SortBy: "name",
}
fmt.Printf("Paginated workout options: %+v\n", paginatedOpts)
// List workouts with type and status filters
filteredOpts := garth.WorkoutListOptions{
Type: "running",
Status: "active",
Limit: 20,
}
fmt.Printf("Filtered workout options: %+v\n", filteredOpts)
// Get workout details
workoutID := "12345"
fmt.Printf("Would fetch workout details for ID: %s\n", workoutID)

View File

@@ -1,431 +0,0 @@
# Go-Garth Implementation TODO List
## 🎯 Project Overview
Complete the Go implementation of the Garth library to match the functionality of the original Python version for Garmin Connect authentication and API access.
## 📋 Phase 1: Complete Authentication Foundation (Priority: HIGH)
### 1.1 Fix Existing Authentication Issues
**Task**: Complete MFA Implementation in `auth.go`
- **File**: `auth.go` - `handleMFA` method
- **Requirements**:
- Parse MFA challenge from response body
- Submit MFA token via POST request
- Handle MFA verification response
- Extract and return authentication ticket
- **Go Style Notes**:
- Use structured error handling: `fmt.Errorf("mfa verification failed: %w", err)`
- Validate MFA token format before sending
- Use context for request timeouts
- **Testing**: Create test cases for MFA flow with mock responses
**Task**: Implement Token Refresh Logic
- **File**: `auth.go` - `RefreshToken` method
- **Requirements**:
- Implement OAuth2 refresh token flow
- Handle refresh token expiration
- Update stored token after successful refresh
- Return new token with updated expiry
- **Go Idioms**:
```go
// Use pointer receivers for methods that modify state
func (a *GarthAuthenticator) RefreshToken(ctx context.Context, refreshToken string) (*Token, error) {
// Validate input
if refreshToken == "" {
return nil, errors.New("refresh token cannot be empty")
}
// Implementation here...
}
```
**Task**: Enhance Error Handling
- **Files**: `auth.go`, `types.go`
- **Requirements**:
- Create custom error types for different failure modes
- Add HTTP status code context to errors
- Parse Garmin-specific error responses
- **Implementation**:
```go
// types.go
type AuthError struct {
Code int `json:"code"`
Message string `json:"message"`
Type string `json:"type"`
}
func (e *AuthError) Error() string {
return fmt.Sprintf("garmin auth error %d: %s", e.Code, e.Message)
}
```
### 1.2 Improve HTTP Client Architecture
**Task**: Create Authenticated HTTP Client Middleware
- **New File**: `client.go`
- **Requirements**:
- Implement `http.RoundTripper` interface
- Automatically add authentication headers
- Handle token refresh on 401 responses
- Add request/response logging (optional)
- **Go Pattern**:
```go
type AuthTransport struct {
base http.RoundTripper
auth *GarthAuthenticator
storage TokenStorage
}
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Clone request, add auth headers, handle refresh
}
```
**Task**: Add Request Retry Logic
- **File**: `client.go`
- **Requirements**:
- Implement exponential backoff
- Retry on specific HTTP status codes (500, 502, 503)
- Maximum retry attempts configuration
- Context-aware cancellation
- **Go Style**: Use `time.After` and `select` for backoff timing
### 1.3 Expand Storage Options
**Task**: Add Memory-based Token Storage
- **New File**: `memorystorage.go`
- **Requirements**:
- Implement `TokenStorage` interface
- Thread-safe operations using `sync.RWMutex`
- Optional token encryption in memory
- **Go Concurrency**:
```go
type MemoryStorage struct {
mu sync.RWMutex
token *Token
}
```
**Task**: Environment Variable Configuration
- **File**: `garth.go`
- **Requirements**:
- Load configuration from environment variables
- Provide reasonable defaults
- Support custom configuration via struct
- **Go Standard**: Use `os.Getenv()` and provide defaults
## 📋 Phase 2: Garmin Connect API Client (Priority: HIGH)
### 2.1 Core API Client Structure
**Task**: Create Base API Client
- **New File**: `connect.go`
- **Requirements**:
- Embed authenticated HTTP client
- Base URL configuration for different Garmin services
- Common request/response handling
- Rate limiting support
- **Structure**:
```go
type ConnectClient struct {
client *http.Client
baseURL string
userAgent string
auth Authenticator
}
func NewConnectClient(auth Authenticator, opts ConnectOptions) *ConnectClient
```
**Task**: Implement Common HTTP Helpers
- **File**: `connect.go`
- **Requirements**:
- Generic GET, POST, PUT, DELETE methods
- JSON request/response marshaling
- Query parameter handling
- Error response parsing
- **Go Generics** (Go 1.18+):
```go
func (c *ConnectClient) Get[T any](ctx context.Context, endpoint string, result *T) error
```
### 2.2 User Profile and Account APIs
**Task**: User Profile Management
- **New File**: `profile.go`
- **Requirements**:
- Get user profile information
- Update profile settings
- Account preferences
- **Types**:
```go
type UserProfile struct {
UserID int64 `json:"userId"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
// Add other profile fields
}
```
### 2.3 Activity and Workout APIs
**Task**: Activity Data Retrieval
- **New File**: `activities.go`
- **Requirements**:
- List activities with pagination
- Get detailed activity data
- Activity search and filtering
- Export activity data (GPX, TCX, etc.)
- **Pagination Pattern**:
```go
type ActivityListOptions struct {
Start int `json:"start"`
Limit int `json:"limit"`
// Add filter options
}
```
**Task**: Workout Management
- **New File**: `workouts.go`
- **Requirements**:
- Create, read, update, delete workouts
- Workout scheduling
- Workout templates
- **CRUD Pattern**: Follow consistent naming (Create, Get, Update, Delete methods)
## 📋 Phase 3: Advanced Features (Priority: MEDIUM)
### 3.1 Device and Sync Management
**Task**: Device Information APIs
- **New File**: `devices.go`
- **Requirements**:
- List connected devices
- Device settings and preferences
- Sync status and history
- **Go Struct Tags**: Use proper JSON tags for API marshaling
### 3.2 Health and Metrics APIs
**Task**: Health Data Access
- **New File**: `health.go`
- **Requirements**:
- Daily summaries (steps, calories, etc.)
- Sleep data
- Heart rate data
- Weight and body composition
- **Time Handling**: Use `time.Time` for all timestamps, handle timezone conversions
### 3.3 Social and Challenges
**Task**: Social Features
- **New File**: `social.go`
- **Requirements**:
- Friends and connections
- Activity sharing
- Challenges and competitions
- **Privacy Considerations**: Add warnings about sharing personal data
## 📋 Phase 4: Developer Experience (Priority: MEDIUM)
### 4.1 Testing Infrastructure
**Task**: Unit Tests for Authentication
- **New File**: `auth_test.go`
- **Requirements**:
- Mock HTTP server for testing
- Test all authentication flows
- Error condition coverage
- Use `httptest.Server` for mocking
- **Go Testing Pattern**:
```go
func TestGarthAuthenticator_Login(t *testing.T) {
tests := []struct {
name string
// test cases
}{
// table-driven tests
}
}
```
**Task**: Integration Tests
- **New File**: `integration_test.go`
- **Requirements**:
- End-to-end API tests (optional, requires credentials)
- Build tag for integration tests: `//go:build integration`
- Environment variable configuration for test credentials
### 4.2 Documentation and Examples
**Task**: Package Documentation
- **All Files**: Add comprehensive GoDoc comments
- **Requirements**:
- Package-level documentation in `garth.go`
- Example usage in doc comments
- Follow Go documentation conventions
- **GoDoc Style**:
```go
// Package garth provides authentication and API access for Garmin Connect services.
//
// Basic usage:
//
// auth := garth.NewAuthenticator(garth.ClientOptions{...})
// token, err := auth.Login(ctx, username, password, "")
//
package garth
```
**Task**: Create Usage Examples
- **New Directory**: `examples/`
- **Requirements**:
- Basic authentication example
- Activity data retrieval example
- Complete CLI tool example
- README with setup instructions
### 4.3 Configuration and Logging
**Task**: Structured Logging Support
- **New File**: `logging.go`
- **Requirements**:
- Optional logger interface
- Debug logging for HTTP requests/responses
- Configurable log levels
- **Go Interface**:
```go
type Logger interface {
Debug(msg string, fields ...interface{})
Info(msg string, fields ...interface{})
Error(msg string, fields ...interface{})
}
```
**Task**: Configuration Management
- **File**: `config.go`
- **Requirements**:
- Centralized configuration struct
- Environment variable loading
- Configuration validation
- Default values
## 📋 Phase 5: Production Readiness (Priority: LOW)
### 5.1 Performance and Reliability
**Task**: Add Rate Limiting
- **File**: `client.go` or new `ratelimit.go`
- **Requirements**:
- Token bucket algorithm
- Configurable rate limits
- Per-endpoint rate limiting if needed
- **Go Concurrency**: Use goroutines and channels for rate limiting
**Task**: Connection Pooling and Timeouts
- **File**: `client.go`
- **Requirements**:
- Configure HTTP client with appropriate timeouts
- Connection pooling settings
- Keep-alive configuration
### 5.2 Security Enhancements
**Task**: Token Encryption at Rest
- **File**: `filestorage.go`, new `encryption.go`
- **Requirements**:
- Optional token encryption using AES
- Key derivation from user password or system keyring
- Secure key storage recommendations
### 5.3 Monitoring and Metrics
**Task**: Add Metrics Collection
- **New File**: `metrics.go`
- **Requirements**:
- Request/response metrics
- Error rate tracking
- Optional Prometheus metrics export
- **Go Pattern**: Use interfaces for pluggable metrics backends
## 🎨 Go Style and Idiom Guidelines
### Code Organization
- **Package Structure**: Keep related functionality in separate files
- **Interfaces**: Define small, focused interfaces
- **Error Handling**: Always handle errors, use `fmt.Errorf` for context
- **Context**: Pass context.Context as first parameter to all long-running functions
### Naming Conventions
- **Exported Types**: PascalCase (e.g., `GarthAuthenticator`)
- **Unexported Types**: camelCase (e.g., `fileStorage`)
- **Methods**: Use verb-noun pattern (e.g., `GetToken`, `SaveToken`)
- **Constants**: Use PascalCase for exported, camelCase for unexported
### Error Handling Patterns
```go
// Wrap errors with context
if err != nil {
return nil, fmt.Errorf("failed to authenticate user %s: %w", username, err)
}
// Use custom error types for different error conditions
type AuthenticationError struct {
Code int
Message string
Cause error
}
```
### JSON and HTTP Patterns
```go
// Use struct tags for JSON marshaling
type Activity struct {
ID int64 `json:"activityId"`
Name string `json:"activityName"`
StartTime time.Time `json:"startTimeLocal"`
}
// Handle HTTP responses consistently
func (c *ConnectClient) makeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
// Implementation with consistent error handling
}
```
### Concurrency Best Practices
- Use `sync.RWMutex` for read-heavy workloads
- Prefer channels for communication between goroutines
- Always handle context cancellation
- Use `sync.WaitGroup` for waiting on multiple goroutines
### Testing Patterns
- Use table-driven tests for multiple test cases
- Create test helpers for common setup
- Use `httptest.Server` for HTTP testing
- Mock external dependencies using interfaces
## 🚀 Implementation Priority Order
1. **Week 1**: Complete authentication foundation (Phase 1)
2. **Week 2-3**: Implement core API client and activity APIs (Phase 2.1, 2.3)
3. **Week 4**: Add user profile and device APIs (Phase 2.2, 3.1)
4. **Week 5**: Testing and documentation (Phase 4)
5. **Week 6+**: Advanced features and production readiness (Phase 3, 5)
## 📚 Additional Resources
- [Effective Go](https://golang.org/doc/effective_go.html)
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- [Original Garth Python Library](https://github.com/matin/garth) - Reference implementation
- [Garmin Connect API Documentation](https://connect.garmin.com/dev/) - If available
- [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) - Understanding the auth flow
## ✅ Definition of Done
Each task is complete when:
- [ ] Code follows Go best practices and style guidelines
- [ ] All public functions have GoDoc comments
- [ ] Unit tests achieve >80% coverage
- [ ] Integration tests pass (where applicable)
- [ ] No linting errors from `golangci-lint`
- [ ] Code is reviewed by senior developer
- [ ] Examples and documentation are updated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
login_page_1756913256.html Normal file

Binary file not shown.

View File

@@ -1,21 +0,0 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # Schema for CodeRabbit configurations
language: "en-US"
early_access: true
reviews:
request_changes_workflow: false
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
path_filters:
- "!tests/**/cassettes/**"
path_instructions:
- path: "tests/**"
instructions: |
- test functions shouldn't have a return type hint
- it's ok to use `assert` instead of `pytest.assume()`
chat:
auto_reply: true

View File

@@ -1,7 +0,0 @@
FROM mcr.microsoft.com/devcontainers/anaconda:0-3
# Copy environment.yml (if found) to a temp location so we update the environment. Also
# copy "noop.txt" so the COPY instruction does not fail if no environment.yml exists.
COPY environment.yml* .devcontainer/noop.txt /tmp/conda-tmp/
RUN if [ -f "/tmp/conda-tmp/environment.yml" ]; then umask 0002 && /opt/conda/bin/conda env update -n base -f /tmp/conda-tmp/environment.yml; fi \
&& rm -rf /tmp/conda-tmp

View File

@@ -1,10 +0,0 @@
{
"name": "Anaconda (Python 3)",
"build": {
"context": "..",
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers/features/node:1": {}
}
}

View File

@@ -1,3 +0,0 @@
This file copied into the container along with environment.yml* from the parent
folder. This file is included to prevents the Dockerfile COPY instruction from
failing if no environment.yml is found.

View File

@@ -1 +0,0 @@
ref: refs/heads/main

View File

@@ -1,12 +0,0 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/matin/garth.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
vscode-merge-base = origin/main

View File

@@ -1 +0,0 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -1,15 +0,0 @@
#!/bin/sh
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:

View File

@@ -1,24 +0,0 @@
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}

View File

@@ -1,174 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use IPC::Open2;
# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout a new update token and
# all files that have been modified since the update token. Paths must
# be relative to the root of the working tree and separated by a single NUL.
#
# To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version
if ($version ne 2) {
die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n";
}
my $git_work_tree = get_working_dir();
my $retry = 1;
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman();
sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" .
"Falling back to scanning...\n";
# In the query expression below we're asking for names of files that
# changed since $last_update_token but not from the .git folder.
#
# To accomplish this, we're using the "since" generator to use the
# recency index to select candidate nodes and "fields" to limit the
# output to file names only. Then we're using the "expression" term to
# further constrain the results.
my $last_update_line = "";
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
$last_update_line = qq[\n"since": $last_update_token,];
}
my $query = <<" END";
["query", "$git_work_tree", {$last_update_line
"fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}]
END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query;
close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/;
return $json_pkg->new->utf8->decode($response);
}
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
$retry--;
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
$last_update_token = $o->{clock};
eval { launch_watchman() };
return 0;
}
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
return 1;
}
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
}

View File

@@ -1,8 +0,0 @@
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View File

@@ -1,14 +0,0 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:

View File

@@ -1,49 +0,0 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --type=bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

View File

@@ -1,13 +0,0 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git merge" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message to
# stderr if it wants to stop the merge commit.
#
# To enable this hook, rename this file to "pre-merge-commit".
. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit"
:

View File

@@ -1,53 +0,0 @@
#!/bin/sh
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local oid> <remote ref> <remote oid>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).
remote="$1"
url="$2"
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
while read local_ref local_oid remote_ref remote_oid
do
if test "$local_oid" = "$zero"
then
# Handle delete
:
else
if test "$remote_oid" = "$zero"
then
# New branch, examine all commits
range="$local_oid"
else
# Update to existing branch, examine new commits
range="$remote_oid..$local_oid"
fi
# Check for WIP commit
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
if test -n "$commit"
then
echo >&2 "Found WIP commit in $local_ref, not pushing"
exit 1
fi
fi
done
exit 0

View File

@@ -1,169 +0,0 @@
#!/bin/sh
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.
publish=next
basebranch="$1"
if test "$#" = 2
then
topic="refs/heads/$2"
else
topic=`git symbolic-ref HEAD` ||
exit 0 ;# we do not interrupt rebasing detached HEAD
fi
case "$topic" in
refs/heads/??/*)
;;
*)
exit 0 ;# we do not interrupt others.
;;
esac
# Now we are dealing with a topic branch being rebased
# on top of master. Is it OK to rebase it?
# Does the topic really exist?
git show-ref -q "$topic" || {
echo >&2 "No such branch $topic"
exit 1
}
# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
echo >&2 "$topic is fully merged to master; better remove it."
exit 1 ;# we could allow it, but there is no point.
fi
# Is topic ever merged to next? If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
not_in_topic=`git rev-list "^$topic" master`
if test -z "$not_in_topic"
then
echo >&2 "$topic is already up to date with master"
exit 1 ;# we could allow it, but there is no point.
else
exit 0
fi
else
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
/usr/bin/perl -e '
my $topic = $ARGV[0];
my $msg = "* $topic has commits already merged to public branch:\n";
my (%not_in_next) = map {
/^([0-9a-f]+) /;
($1 => 1);
} split(/\n/, $ARGV[1]);
for my $elem (map {
/^([0-9a-f]+) (.*)$/;
[$1 => $2];
} split(/\n/, $ARGV[2])) {
if (!exists $not_in_next{$elem->[0]}) {
if ($msg) {
print STDERR $msg;
undef $msg;
}
print STDERR " $elem->[1]\n";
}
}
' "$topic" "$not_in_next" "$not_in_master"
exit 1
fi
<<\DOC_END
This sample hook safeguards topic branches that have been
published from being rewound.
The workflow assumed here is:
* Once a topic branch forks from "master", "master" is never
merged into it again (either directly or indirectly).
* Once a topic branch is fully cooked and merged into "master",
it is deleted. If you need to build on top of it to correct
earlier mistakes, a new topic branch is created by forking at
the tip of the "master". This is not strictly necessary, but
it makes it easier to keep your history simple.
* Whenever you need to test or publish your changes to topic
branches, merge them into "next" branch.
The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.
With this workflow, you would want to know:
(1) ... if a topic branch has ever been merged to "next". Young
topic branches can have stupid mistakes you would rather
clean up before publishing, and things that have not been
merged into other branches can be easily rebased without
affecting other people. But once it is published, you would
not want to rewind it.
(2) ... if a topic branch has been fully merged to "master".
Then you can delete it. More importantly, you should not
build on top of it -- other people may already want to
change things related to the topic as patches against your
"master", so if you need further changes, it is better to
fork the topic (perhaps with the same name) afresh from the
tip of "master".
Let's look at this example:
o---o---o---o---o---o---o---o---o---o "next"
/ / / /
/ a---a---b A / /
/ / / /
/ / c---c---c---c B /
/ / / \ /
/ / / b---b C \ /
/ / / / \ /
---o---o---o---o---o---o---o---o---o---o---o "master"
A, B and C are topic branches.
* A has one fix since it was merged up to "next".
* B has finished. It has been fully merged up to "master" and "next",
and is ready to be deleted.
* C has not merged to "next" at all.
We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.
To compute (1):
git rev-list ^master ^topic next
git rev-list ^master next
if these match, topic has not merged in next at all.
To compute (2):
git rev-list master..topic
if this is empty, it is fully merged to "master".
DOC_END

View File

@@ -1,24 +0,0 @@
#!/bin/sh
#
# An example hook script to make use of push options.
# The example simply echoes all push options that start with 'echoback='
# and rejects all pushes when the "reject" push option is used.
#
# To enable this hook, rename this file to "pre-receive".
if test -n "$GIT_PUSH_OPTION_COUNT"
then
i=0
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
do
eval "value=\$GIT_PUSH_OPTION_$i"
case "$value" in
echoback=*)
echo "echo from the pre-receive-hook: ${value#*=}" >&2
;;
reject)
exit 1
esac
i=$((i + 1))
done
fi

View File

@@ -1,42 +0,0 @@
#!/bin/sh
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source. The hook's purpose is to edit the commit
# message file. If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".
# This hook includes three examples. The first one removes the
# "# Please enter the commit message..." help message.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output. It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited. This is rarely a good idea.
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
# case "$COMMIT_SOURCE,$SHA1" in
# ,|template,)
# /usr/bin/perl -i.bak -pe '
# print "\n" . `git diff --cached --name-status -r`
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
# *) ;;
# esac
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
# if test -z "$COMMIT_SOURCE"
# then
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
# fi

View File

@@ -1,78 +0,0 @@
#!/bin/sh
# An example hook script to update a checked-out tree on a git push.
#
# This hook is invoked by git-receive-pack(1) when it reacts to git
# push and updates reference(s) in its repository, and when the push
# tries to update the branch that is currently checked out and the
# receive.denyCurrentBranch configuration variable is set to
# updateInstead.
#
# By default, such a push is refused if the working tree and the index
# of the remote repository has any difference from the currently
# checked out commit; when both the working tree and the index match
# the current commit, they are updated to match the newly pushed tip
# of the branch. This hook is to be used to override the default
# behaviour; however the code below reimplements the default behaviour
# as a starting point for convenient modification.
#
# The hook receives the commit with which the tip of the current
# branch is going to be updated:
commit=$1
# It can exit with a non-zero status to refuse the push (when it does
# so, it must not modify the index or the working tree).
die () {
echo >&2 "$*"
exit 1
}
# Or it can make any necessary changes to the working tree and to the
# index to bring them to the desired state when the tip of the current
# branch is updated to the new commit, and exit with a zero status.
#
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
# in order to emulate git fetch that is run in the reverse direction
# with git push, as the two-tree form of git read-tree -u -m is
# essentially the same as git switch or git checkout that switches
# branches while keeping the local changes in the working tree that do
# not interfere with the difference between the branches.
# The below is a more-or-less exact translation to shell of the C code
# for the default behaviour for git's push-to-checkout hook defined in
# the push_to_deploy() function in builtin/receive-pack.c.
#
# Note that the hook will be executed from the repository directory,
# not from the working tree, so if you want to perform operations on
# the working tree, you will have to adapt your code accordingly, e.g.
# by adding "cd .." or using relative paths.
if ! git update-index -q --ignore-submodules --refresh
then
die "Up-to-date check failed"
fi
if ! git diff-files --quiet --ignore-submodules --
then
die "Working directory has unstaged changes"
fi
# This is a rough translation of:
#
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
if git cat-file -e HEAD 2>/dev/null
then
head=HEAD
else
head=$(git hash-object -t tree --stdin </dev/null)
fi
if ! git diff-index --quiet --cached --ignore-submodules $head --
then
die "Working directory has staged changes"
fi
if ! git read-tree -u -m "$commit"
then
die "Could not update working tree to new HEAD"
fi

View File

@@ -1,77 +0,0 @@
#!/bin/sh
# An example hook script to validate a patch (and/or patch series) before
# sending it via email.
#
# The hook should exit with non-zero status after issuing an appropriate
# message if it wants to prevent the email(s) from being sent.
#
# To enable this hook, rename this file to "sendemail-validate".
#
# By default, it will only check that the patch(es) can be applied on top of
# the default upstream branch without conflicts in a secondary worktree. After
# validation (successful or not) of the last patch of a series, the worktree
# will be deleted.
#
# The following config variables can be set to change the default remote and
# remote ref that are used to apply the patches against:
#
# sendemail.validateRemote (default: origin)
# sendemail.validateRemoteRef (default: HEAD)
#
# Replace the TODO placeholders with appropriate checks according to your
# needs.
validate_cover_letter () {
file="$1"
# TODO: Replace with appropriate checks (e.g. spell checking).
true
}
validate_patch () {
file="$1"
# Ensure that the patch applies without conflicts.
git am -3 "$file" || return
# TODO: Replace with appropriate checks for this patch
# (e.g. checkpatch.pl).
true
}
validate_series () {
# TODO: Replace with appropriate checks for the whole series
# (e.g. quick build, coding style checks, etc.).
true
}
# main -------------------------------------------------------------------------
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
then
remote=$(git config --default origin --get sendemail.validateRemote) &&
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
git config --replace-all sendemail.validateWorktree "$worktree"
else
worktree=$(git config --get sendemail.validateWorktree)
fi || {
echo "sendemail-validate: error: failed to prepare worktree" >&2
exit 1
}
unset GIT_DIR GIT_WORK_TREE
cd "$worktree" &&
if grep -q "^diff --git " "$1"
then
validate_patch "$1"
else
validate_cover_letter "$1"
fi &&
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
then
git config --unset-all sendemail.validateWorktree &&
trap 'git worktree remove -ff "$worktree"' EXIT &&
validate_series
fi

View File

@@ -1,128 +0,0 @@
#!/bin/sh
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"
# --- Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# --- Config
allowunannotated=$(git config --type=bool hooks.allowunannotated)
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
echo "*** Project description file hasn't been set" >&2
exit 1
;;
esac
# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
case "$refname","$newrev_type" in
refs/tags/*,commit)
# un-annotated tag
short_refname=${refname##refs/tags/}
if [ "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
refs/tags/*,delete)
# delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,tag)
# annotated tag
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
then
echo "*** Tag '$refname' already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
fi
;;
refs/heads/*,commit)
# branch
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
# tracking branch
;;
refs/remotes/*,delete)
# delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
# Anything else (is there anything else?)
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
exit 1
;;
esac
# --- Finished
exit 0

Binary file not shown.

View File

@@ -1,6 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View File

@@ -1 +0,0 @@
0000000000000000000000000000000000000000 4b027e14574987c7ff329c5ea4980a624f954cad sstent <stuart.stent@gmail.com> 1756500815 -0700 clone: from https://github.com/matin/garth.git

View File

@@ -1 +0,0 @@
0000000000000000000000000000000000000000 4b027e14574987c7ff329c5ea4980a624f954cad sstent <stuart.stent@gmail.com> 1756500815 -0700 clone: from https://github.com/matin/garth.git

View File

@@ -1 +0,0 @@
0000000000000000000000000000000000000000 4b027e14574987c7ff329c5ea4980a624f954cad sstent <stuart.stent@gmail.com> 1756500815 -0700 clone: from https://github.com/matin/garth.git

View File

@@ -1,47 +0,0 @@
# pack-refs with: peeled fully-peeled sorted
e88832aab75f7cbdc497d40100a25f7ec9d50302 refs/remotes/origin/add-hydration-support
dab68681cede95b96b4a857041255cb2f6042205 refs/remotes/origin/add-training-status
b8989b38968d32b4a41bdc9678c4156d6703f454 refs/remotes/origin/coderabbitai/chat/20bTtA
e6e142469cb12298129e8fef5772e33a569e96ca refs/remotes/origin/dependabot/github_actions/actions/checkout-5
4546623a7ce71e5a87ef202b05c316a2d849efca refs/remotes/origin/hydration
4b027e14574987c7ff329c5ea4980a624f954cad refs/remotes/origin/main
0e739d308dc2c9a36ad9efc791787340bebb2ec4 refs/remotes/origin/minor-auth-refactor
66f488625529bc6a7bafadbb11bf064e30f6850f refs/remotes/origin/refactor/restart
19a8fc24d787a6b83b4ac87e6ab9ae8af407a1ee refs/tags/0.4.28
bed4a7cd32310c986b1c61a45dd6dfb6c5988fba refs/tags/0.4.29
53a166de4cc435b8f694d142f9af4454baada131 refs/tags/0.4.30
ca2410a9650c87514a806edbe54e89408acbbe76 refs/tags/0.4.31
c77cd9b405082b474914caadeac5cc18f0f6d70e refs/tags/0.4.32
2a0dae96d44943edf667f6173479317d2623aa17 refs/tags/0.4.33
2d1b21192270e6598ec1326ed0d6017ec23ff057 refs/tags/0.4.34
ce5874bbb6492db91a56f76b7d9ad123aa790900 refs/tags/0.4.35
5ee41c540b1a2ffd69277ffa99d4dd97a382dbc5 refs/tags/0.4.36
515933f709db3b00f5b06d5ba65b267dcda191b9 refs/tags/0.4.37
43196b588c553fcc335251d248434002caa0dab0 refs/tags/0.4.38
69a1fd4bfa2a697e15e15661eae60f6d9541daf2 refs/tags/0.4.39
fad9855b65837d7f5944ae2cd982e4af16b22ff0 refs/tags/0.4.40
5aadba6f2e01ae5eb76f2064d4d7e94f2483dbf9 refs/tags/0.4.41
6aeb0faaf0d6b473d8dc161373068d2f5413fdfe refs/tags/0.4.42
34dc0a162c19c3ec4728517239171164b8009819 refs/tags/0.4.43
3a5ebbcdd836bce4b9d5a84191819e24084ff5c7 refs/tags/0.4.44
316787d1e3ff69c09725b2eb8ded748a4422abb3 refs/tags/0.4.45
ae1b425ea0c7560155ee5e9e2e828fda7c1be43d refs/tags/0.4.46
960d8c0ac0b68672e9edc7b9738ba77d60fa806a refs/tags/0.4.47
b557b886b229ad304568988cf716c510ef7ecbd7 refs/tags/0.5.0
3004df3fb907c81153c9536c142474faf231b698 refs/tags/0.5.1
d15e2afd439268ed4c9b4db5a8d75d2afeba7a5d refs/tags/0.5.10
ad045ff989456934cb312313b01f0f1ad19af614 refs/tags/0.5.11
517eeeda1c6b6504abe7898aa0127f98bbdfc261 refs/tags/0.5.12
26b5e2eefdd26b5e5b9bb4b48260a702521cc976 refs/tags/0.5.13
922f3c305c71fb177c3bb5e3ca6697ed2e34424c refs/tags/0.5.2
a05eb5b25ba8612759e6fe3667b14e26c6af014e refs/tags/0.5.3
1a17721e24db7c2fa7f5df2326d9b9919f5de8e5 refs/tags/0.5.4
5f27385ecec57b7088f63c9a499b58da5661e904 refs/tags/0.5.5
f2592742727e3c95545dec27a7f0781dbdf5d2cd refs/tags/0.5.6
11b2ed554611bc3ce488df631d54b54afe097b6e refs/tags/0.5.7
10061601252844a18419ecfe0249aa18d3ceb1ab refs/tags/0.5.8
2c5b7d7e45bcadbae39e8a4f53169a1614523ea2 refs/tags/0.5.9
06b6bf391c1a0a9f0b412057a195415a9dc8755e refs/tags/v0.5.14
f58c7e650754e2c45f306f2f707efb389032a2e7 refs/tags/v0.5.15
c631afe82ac07302abf499e019e2ebe38c1111ac refs/tags/v0.5.16
4b027e14574987c7ff329c5ea4980a624f954cad refs/tags/v0.5.17

View File

@@ -1 +0,0 @@
4b027e14574987c7ff329c5ea4980a624f954cad

View File

@@ -1 +0,0 @@
ref: refs/remotes/origin/main

View File

@@ -1 +0,0 @@
*.ipynb linguist-documentation=true

View File

@@ -1,17 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
time: "20:00"
timezone: "America/Mexico_City"
open-pull-requests-limit: 5
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "20:00"
timezone: "America/Mexico_City"
open-pull-requests-limit: 5

View File

@@ -1,87 +0,0 @@
name: CI
on:
push:
branches:
- main
tags:
- "**"
pull_request: {}
env:
COLUMNS: 150
permissions:
contents: read
pull-requests: read
checks: write
statuses: write
jobs:
lint:
runs-on: ubuntu-latest
name: lint ${{ matrix.python-version }}
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: |
uv pip install --system -e .
uv pip install --system --group linting
- uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files --verbose
env:
SKIP: no-commit-to-branch
test:
name: test ${{ matrix.python-version }}
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
python-version: ["3.10", "3.11", "3.12", "3.13"]
env:
PYTHON: ${{ matrix.python-version }}
OS: ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: |
uv pip install --system -e .
uv pip install --system --group testing
- name: test
run: make testcov
env:
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-with-deps
- name: upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage/coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true

View File

@@ -1,30 +0,0 @@
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/garth
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@v6
- name: Build package
run: |
uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -1,53 +0,0 @@
# Virtual environments
env/
env3*/
venv/
.venv/
.envrc
.env
__pypackages__/
# IDEs and editors
.idea/
# Package distribution and build files
*.egg-info/
dist/
/build/
_build/
# Python bytecode and cache files
*.py[cod]
.cache/
/.ghtopdep_cache/
.hypothesis
.mypy_cache/
.pytest_cache/
/.ruff_cache/
# Benchmark and test files
/benchmarks/*.json
/htmlcov/
/codecov.sh
/coverage.lcov
.coverage
test.py
/coverage/
# Documentation files
/docs/changelog.md
/site/
/site.zip
# Other files and folders
.python-version
.DS_Store
.auto-format
/sandbox/
/worktrees/
.pdm-python
tmp/
.pdm.toml
# exclude saved oauth tokens
oauth*_token.json

View File

@@ -1,6 +0,0 @@
{
"MD033": {
"allowed_elements": ["img", "a", "source", "picture"]
},
"MD046": false
}

View File

@@ -1,33 +0,0 @@
exclude: '.*\.ipynb$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
args: ['--unsafe']
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
additional_dependencies:
- tomli
exclude: 'cassettes/'
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.12.1
hooks:
- id: markdownlint-cli2
- repo: local
hooks:
- id: lint
name: lint
entry: make lint
types: [python]
language: system
pass_filenames: false

View File

@@ -1,21 +0,0 @@
# MIT License
Copyright (c) 2023 Matin Tamizi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,81 +0,0 @@
# Based on Makefile for pydantic (github.com/pydantic/pydantic/blob/main/Makefile)
.DEFAULT_GOAL := all
sources = src tests
.PHONY: .uv ## Check that uv is installed
.uv:
@uv --version || echo 'Please install uv: https://docs.astral.sh/uv/getting-started/installation/'
.PHONY: .pre-commit ## Check that pre-commit is installed
.pre-commit:
@pre-commit -V || echo 'Please install pre-commit: https://pre-commit.com/'
.PHONY: install ## Install the package, dependencies, and pre-commit for local development
install: .uv .pre-commit
uv pip install -e .
uv pip install --group dev --group linting --group testing
pre-commit install --install-hooks
.PHONY: sync ## Sync dependencies and lockfiles
sync: .uv clean
uv pip install -e . --force-reinstall
uv sync
.PHONY: format ## Auto-format python source files
format: .uv
uv run ruff format $(sources)
uv run ruff check --fix $(sources)
.PHONY: lint ## Lint python source files
lint: .uv
uv run ruff format --check $(sources)
uv run ruff check $(sources)
uv run mypy $(sources)
.PHONY: codespell ## Use Codespell to do spellchecking
codespell: .pre-commit
pre-commit run codespell --all-files
.PHONY: test ## Run all tests, skipping the type-checker integration tests
test: .uv
uv run coverage run -m pytest -v --durations=10
.PHONY: testcov ## Run tests and generate a coverage report, skipping the type-checker integration tests
testcov: test
@echo "building coverage html"
@uv run coverage html
@echo "building coverage xml"
@uv run coverage xml -o coverage/coverage.xml
.PHONY: all ## Run the standard set of checks performed in CI
all: lint codespell testcov
.PHONY: clean ## Clear local caches and build artifacts
clean:
find . -type d -name __pycache__ -exec rm -r {} +
find . -type f -name '*.py[co]' -exec rm -f {} +
find . -type f -name '*~' -exec rm -f {} +
find . -type f -name '.*~' -exec rm -f {} +
rm -rf .cache
rm -rf .pytest_cache
rm -rf .ruff_cache
rm -rf htmlcov
rm -rf *.egg-info
rm -f .coverage
rm -f .coverage.*
rm -rf build
rm -rf dist
rm -rf site
rm -rf docs/_build
rm -rf docs/.changelog.md docs/.version.md docs/.tmp_schema_mappings.html
rm -rf fastapi/test.db
rm -rf coverage.xml
rm -rf __pypackages__ uv.lock
.PHONY: help ## Display this message
help:
@grep -E \
'^.PHONY: .*?## .*$$' $(MAKEFILE_LIST) | \
sort | \
awk 'BEGIN {FS = ".PHONY: |## "}; {printf "\033[36m%-19s\033[0m %s\n", $$2, $$3}'

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,89 +0,0 @@
[project]
name = "garth"
dynamic = ["version"]
description = "Garmin SSO auth + Connect client"
authors = [
{name = "Matin Tamizi", email = "mtamizi@duck.com"},
]
dependencies = [
"requests>=2.0.0,<3.0.0",
"pydantic>=1.10.12,<3.0.0",
"requests-oauthlib>=1.3.1,<3.0.0",
]
requires-python = ">=3.10"
readme = "README.md"
license = {text = "MIT"}
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Operating System :: OS Independent",
]
keywords = ["garmin", "garmin api", "garmin connect", "garmin sso"]
[project.urls]
"Homepage" = "https://github.com/matin/garth"
"Repository" = "https://github.com/matin/garth"
"Issues" = "https://github.com/matin/garth/issues"
"Changelog" = "https://github.com/matin/garth/releases"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/garth/version.py"
[tool.pytest.ini_options]
addopts = "--ignore=__pypackages__ --ignore-glob=*.yaml"
[tool.mypy]
ignore_missing_imports = true
[tool.ruff]
line-length = 79
indent-width = 4
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
[dependency-groups]
dev = [
"ipython",
"ipdb",
"ipykernel",
"pandas",
"matplotlib",
]
linting = [
"ruff",
"mypy",
"types-requests",
]
testing = [
"coverage",
"pytest",
"pytest-vcr",
]
[tool.ruff.lint.isort]
known-first-party = ["garth"]
combine-as-imports = true
lines-after-imports = 2
[project.scripts]
garth = "garth.cli:main"

View File

@@ -1,59 +0,0 @@
from .data import (
BodyBatteryData,
DailyBodyBatteryStress,
HRVData,
SleepData,
WeightData,
)
from .http import Client, client
from .stats import (
DailyHRV,
DailyHydration,
DailyIntensityMinutes,
DailySleep,
DailySteps,
DailyStress,
WeeklyIntensityMinutes,
WeeklySteps,
WeeklyStress,
)
from .users import UserProfile, UserSettings
from .version import __version__
__all__ = [
"BodyBatteryData",
"Client",
"DailyBodyBatteryStress",
"DailyHRV",
"DailyHydration",
"DailyIntensityMinutes",
"DailySleep",
"DailySteps",
"DailyStress",
"HRVData",
"SleepData",
"WeightData",
"UserProfile",
"UserSettings",
"WeeklyIntensityMinutes",
"WeeklySteps",
"WeeklyStress",
"__version__",
"client",
"configure",
"connectapi",
"download",
"login",
"resume",
"save",
"upload",
]
configure = client.configure
connectapi = client.connectapi
download = client.download
login = client.login
resume = client.load
save = client.dump
upload = client.upload

View File

@@ -1,37 +0,0 @@
import time
from datetime import datetime
from pydantic.dataclasses import dataclass
@dataclass
class OAuth1Token:
oauth_token: str
oauth_token_secret: str
mfa_token: str | None = None
mfa_expiration_timestamp: datetime | None = None
domain: str | None = None
@dataclass
class OAuth2Token:
scope: str
jti: str
token_type: str
access_token: str
refresh_token: str
expires_in: int
expires_at: int
refresh_token_expires_in: int
refresh_token_expires_at: int
@property
def expired(self):
return self.expires_at < time.time()
@property
def refresh_expired(self):
return self.refresh_token_expires_at < time.time()
def __str__(self):
return f"{self.token_type.title()} {self.access_token}"

View File

@@ -1,34 +0,0 @@
import argparse
import getpass
import garth
def main():
parser = argparse.ArgumentParser(prog="garth")
parser.add_argument(
"--domain",
"-d",
default="garmin.com",
help=(
"Domain for Garmin Connect (default: garmin.com). "
"Use garmin.cn for China."
),
)
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser(
"login", help="Authenticate with Garmin Connect and print token"
)
args = parser.parse_args()
garth.configure(domain=args.domain)
match args.command:
case "login":
email = input("Email: ")
password = getpass.getpass("Password: ")
garth.login(email, password)
token = garth.client.dumps()
print(token)
case _:
parser.print_help()

View File

@@ -1,21 +0,0 @@
__all__ = [
"BodyBatteryData",
"BodyBatteryEvent",
"BodyBatteryReading",
"DailyBodyBatteryStress",
"HRVData",
"SleepData",
"StressReading",
"WeightData",
]
from .body_battery import (
BodyBatteryData,
BodyBatteryEvent,
BodyBatteryReading,
DailyBodyBatteryStress,
StressReading,
)
from .hrv import HRVData
from .sleep import SleepData
from .weight import WeightData

View File

@@ -1,47 +0,0 @@
from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor
from datetime import date
from itertools import chain
from typing_extensions import Self
from .. import http
from ..utils import date_range, format_end_date
MAX_WORKERS = 10
class Data(ABC):
@classmethod
@abstractmethod
def get(
cls, day: date | str, *, client: http.Client | None = None
) -> Self | list[Self] | None: ...
@classmethod
def list(
cls,
end: date | str | None = None,
days: int = 1,
*,
client: http.Client | None = None,
max_workers: int = MAX_WORKERS,
) -> list[Self]:
client = client or http.client
end = format_end_date(end)
def fetch_date(date_):
if day := cls.get(date_, client=client):
return day
dates = date_range(end, days)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
data = list(executor.map(fetch_date, dates))
data = [day for day in data if day is not None]
return list(
chain.from_iterable(
day if isinstance(day, list) else [day] for day in data
)
)

View File

@@ -1,11 +0,0 @@
__all__ = [
"BodyBatteryData",
"BodyBatteryEvent",
"BodyBatteryReading",
"DailyBodyBatteryStress",
"StressReading",
]
from .daily_stress import DailyBodyBatteryStress
from .events import BodyBatteryData, BodyBatteryEvent
from .readings import BodyBatteryReading, StressReading

View File

@@ -1,90 +0,0 @@
from datetime import date, datetime
from functools import cached_property
from typing import Any
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from ... import http
from ...utils import camel_to_snake_dict, format_end_date
from .._base import Data
from .readings import (
BodyBatteryReading,
StressReading,
parse_body_battery_readings,
parse_stress_readings,
)
@dataclass
class DailyBodyBatteryStress(Data):
"""Complete daily Body Battery and stress data."""
user_profile_pk: int
calendar_date: date
start_timestamp_gmt: datetime
end_timestamp_gmt: datetime
start_timestamp_local: datetime
end_timestamp_local: datetime
max_stress_level: int
avg_stress_level: int
stress_chart_value_offset: int
stress_chart_y_axis_origin: int
stress_values_array: list[list[int]]
body_battery_values_array: list[list[Any]]
@cached_property
def body_battery_readings(self) -> list[BodyBatteryReading]:
"""Convert body battery values array to structured readings."""
return parse_body_battery_readings(self.body_battery_values_array)
@property
def stress_readings(self) -> list[StressReading]:
"""Convert stress values array to structured readings."""
return parse_stress_readings(self.stress_values_array)
@property
def current_body_battery(self) -> int | None:
"""Get the latest Body Battery level."""
readings = self.body_battery_readings
return readings[-1].level if readings else None
@property
def max_body_battery(self) -> int | None:
"""Get the maximum Body Battery level for the day."""
readings = self.body_battery_readings
return max(reading.level for reading in readings) if readings else None
@property
def min_body_battery(self) -> int | None:
"""Get the minimum Body Battery level for the day."""
readings = self.body_battery_readings
return min(reading.level for reading in readings) if readings else None
@property
def body_battery_change(self) -> int | None:
"""Calculate the Body Battery change for the day."""
readings = self.body_battery_readings
if not readings or len(readings) < 2:
return None
return readings[-1].level - readings[0].level
@classmethod
def get(
cls,
day: date | str | None = None,
*,
client: http.Client | None = None,
) -> Self | None:
"""Get complete Body Battery and stress data for a specific date."""
client = client or http.client
date_str = format_end_date(day)
path = f"/wellness-service/wellness/dailyStress/{date_str}"
response = client.connectapi(path)
if not isinstance(response, dict):
return None
snake_response = camel_to_snake_dict(response)
return cls(**snake_response)

View File

@@ -1,227 +0,0 @@
import logging
from datetime import date, datetime
from typing import Any
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from ... import http
from ...utils import format_end_date
from .._base import Data
from .readings import BodyBatteryReading, parse_body_battery_readings
MAX_WORKERS = 10
@dataclass
class BodyBatteryEvent:
"""Body Battery event data."""
event_type: str
event_start_time_gmt: datetime
timezone_offset: int
duration_in_milliseconds: int
body_battery_impact: int
feedback_type: str
short_feedback: str
@dataclass
class BodyBatteryData(Data):
"""Legacy Body Battery events data (sleep events only)."""
event: BodyBatteryEvent | None = None
activity_name: str | None = None
activity_type: str | None = None
activity_id: str | None = None
average_stress: float | None = None
stress_values_array: list[list[int]] | None = None
body_battery_values_array: list[list[Any]] | None = None
@property
def body_battery_readings(self) -> list[BodyBatteryReading]:
"""Convert body battery values array to structured readings."""
return parse_body_battery_readings(self.body_battery_values_array)
@property
def current_level(self) -> int | None:
"""Get the latest Body Battery level."""
readings = self.body_battery_readings
return readings[-1].level if readings else None
@property
def max_level(self) -> int | None:
"""Get the maximum Body Battery level for the day."""
readings = self.body_battery_readings
return max(reading.level for reading in readings) if readings else None
@property
def min_level(self) -> int | None:
"""Get the minimum Body Battery level for the day."""
readings = self.body_battery_readings
return min(reading.level for reading in readings) if readings else None
@classmethod
def get(
cls,
date_str: str | date | None = None,
*,
client: http.Client | None = None,
) -> list[Self]:
"""Get Body Battery events for a specific date."""
client = client or http.client
date_str = format_end_date(date_str)
path = f"/wellness-service/wellness/bodyBattery/events/{date_str}"
try:
response = client.connectapi(path)
except Exception as e:
logging.warning(f"Failed to fetch Body Battery events: {e}")
return []
if not isinstance(response, list):
return []
events = []
for item in response:
try:
# Parse event data with validation
event_data = item.get("event")
# Validate event_data exists before accessing properties
if event_data is None:
logging.warning(f"Missing event data in item: {item}")
event = None
else:
# Validate and parse datetime with explicit error handling
event_start_time_str = event_data.get("eventStartTimeGmt")
if not event_start_time_str:
logging.error(
f"Missing eventStartTimeGmt in event data: "
f"{event_data}"
)
raise ValueError(
"eventStartTimeGmt is required but missing"
)
try:
event_start_time_gmt = datetime.fromisoformat(
event_start_time_str.replace("Z", "+00:00")
)
except (ValueError, AttributeError) as e:
logging.error(
f"Invalid datetime format "
f"'{event_start_time_str}': {e}"
)
raise ValueError(
f"Invalid eventStartTimeGmt format: "
f"{event_start_time_str}"
) from e
# Validate numeric fields
timezone_offset = event_data.get("timezoneOffset", 0)
if not isinstance(timezone_offset, (int, float)):
logging.warning(
f"Invalid timezone_offset type: "
f"{type(timezone_offset)}, using 0"
)
timezone_offset = 0
duration_ms = event_data.get("durationInMilliseconds", 0)
if not isinstance(duration_ms, (int, float)):
logging.warning(
f"Invalid durationInMilliseconds type: "
f"{type(duration_ms)}, using 0"
)
duration_ms = 0
battery_impact = event_data.get("bodyBatteryImpact", 0)
if not isinstance(battery_impact, (int, float)):
logging.warning(
f"Invalid bodyBatteryImpact type: "
f"{type(battery_impact)}, using 0"
)
battery_impact = 0
event = BodyBatteryEvent(
event_type=event_data.get("eventType", ""),
event_start_time_gmt=event_start_time_gmt,
timezone_offset=int(timezone_offset),
duration_in_milliseconds=int(duration_ms),
body_battery_impact=int(battery_impact),
feedback_type=event_data.get("feedbackType", ""),
short_feedback=event_data.get("shortFeedback", ""),
)
# Validate data arrays
stress_values = item.get("stressValuesArray")
if stress_values is not None and not isinstance(
stress_values, list
):
logging.warning(
f"Invalid stressValuesArray type: "
f"{type(stress_values)}, using None"
)
stress_values = None
battery_values = item.get("bodyBatteryValuesArray")
if battery_values is not None and not isinstance(
battery_values, list
):
logging.warning(
f"Invalid bodyBatteryValuesArray type: "
f"{type(battery_values)}, using None"
)
battery_values = None
# Validate average_stress
avg_stress = item.get("averageStress")
if avg_stress is not None and not isinstance(
avg_stress, (int, float)
):
logging.warning(
f"Invalid averageStress type: "
f"{type(avg_stress)}, using None"
)
avg_stress = None
events.append(
cls(
event=event,
activity_name=item.get("activityName"),
activity_type=item.get("activityType"),
activity_id=item.get("activityId"),
average_stress=avg_stress,
stress_values_array=stress_values,
body_battery_values_array=battery_values,
)
)
except ValueError as e:
# Re-raise validation errors with context
logging.error(
f"Data validation error for Body Battery event item "
f"{item}: {e}"
)
continue
except Exception as e:
# Log unexpected errors with full context
logging.error(
f"Unexpected error parsing Body Battery event item "
f"{item}: {e}",
exc_info=True,
)
continue
# Log summary of data quality issues
total_items = len(response)
parsed_events = len(events)
if parsed_events < total_items:
skipped = total_items - parsed_events
logging.info(
f"Body Battery events parsing: {parsed_events}/{total_items} "
f"successful, {skipped} skipped due to data issues"
)
return events

View File

@@ -1,56 +0,0 @@
from typing import Any
from pydantic.dataclasses import dataclass
@dataclass
class BodyBatteryReading:
"""Individual Body Battery reading."""
timestamp: int
status: str
level: int
version: float
@dataclass
class StressReading:
"""Individual stress reading."""
timestamp: int
stress_level: int
def parse_body_battery_readings(
body_battery_values_array: list[list[Any]] | None,
) -> list[BodyBatteryReading]:
"""Convert body battery values array to structured readings."""
readings = []
for values in body_battery_values_array or []:
# Each reading requires 4 values: timestamp, status, level, version
if len(values) >= 4:
readings.append(
BodyBatteryReading(
timestamp=values[0],
status=values[1],
level=values[2],
version=values[3],
)
)
# Sort readings by timestamp to ensure chronological order
return sorted(readings, key=lambda reading: reading.timestamp)
def parse_stress_readings(
stress_values_array: list[list[int]] | None,
) -> list[StressReading]:
"""Convert stress values array to structured readings."""
readings = []
for values in stress_values_array or []:
# Each reading requires 2 values: timestamp, stress_level
if len(values) >= 2:
readings.append(
StressReading(timestamp=values[0], stress_level=values[1])
)
# Sort readings by timestamp to ensure chronological order
return sorted(readings, key=lambda reading: reading.timestamp)

View File

@@ -1,68 +0,0 @@
from datetime import date, datetime
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import camel_to_snake_dict
from ._base import Data
@dataclass
class Baseline:
low_upper: int
balanced_low: int
balanced_upper: int
marker_value: float
@dataclass
class HRVSummary:
calendar_date: date
weekly_avg: int
last_night_avg: int | None
last_night_5_min_high: int
baseline: Baseline
status: str
feedback_phrase: str
create_time_stamp: datetime
@dataclass
class HRVReading:
hrv_value: int
reading_time_gmt: datetime
reading_time_local: datetime
@dataclass
class HRVData(Data):
user_profile_pk: int
hrv_summary: HRVSummary
hrv_readings: list[HRVReading]
start_timestamp_gmt: datetime
end_timestamp_gmt: datetime
start_timestamp_local: datetime
end_timestamp_local: datetime
sleep_start_timestamp_gmt: datetime
sleep_end_timestamp_gmt: datetime
sleep_start_timestamp_local: datetime
sleep_end_timestamp_local: datetime
@classmethod
def get(
cls, day: date | str, *, client: http.Client | None = None
) -> Self | None:
client = client or http.client
path = f"/hrv-service/hrv/{day}"
hrv_data = client.connectapi(path)
if not hrv_data:
return None
hrv_data = camel_to_snake_dict(hrv_data)
assert isinstance(hrv_data, dict)
return cls(**hrv_data)
@classmethod
def list(cls, *args, **kwargs) -> list[Self]:
data = super().list(*args, **kwargs)
return sorted(data, key=lambda d: d.hrv_summary.calendar_date)

View File

@@ -1,123 +0,0 @@
from datetime import date, datetime
from typing import Optional, Union
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import camel_to_snake_dict, get_localized_datetime
from ._base import Data
@dataclass
class Score:
qualifier_key: str
optimal_start: Optional[float] = None
optimal_end: Optional[float] = None
value: Optional[int] = None
ideal_start_in_seconds: Optional[float] = None
ideal_end_in_seconds: Optional[float] = None
@dataclass
class SleepScores:
total_duration: Score
stress: Score
awake_count: Score
overall: Score
rem_percentage: Score
restlessness: Score
light_percentage: Score
deep_percentage: Score
@dataclass
class DailySleepDTO:
id: int
user_profile_pk: int
calendar_date: date
sleep_time_seconds: int
nap_time_seconds: int
sleep_window_confirmed: bool
sleep_window_confirmation_type: str
sleep_start_timestamp_gmt: int
sleep_end_timestamp_gmt: int
sleep_start_timestamp_local: int
sleep_end_timestamp_local: int
device_rem_capable: bool
retro: bool
unmeasurable_sleep_seconds: Optional[int] = None
deep_sleep_seconds: Optional[int] = None
light_sleep_seconds: Optional[int] = None
rem_sleep_seconds: Optional[int] = None
awake_sleep_seconds: Optional[int] = None
sleep_from_device: Optional[bool] = None
sleep_version: Optional[int] = None
awake_count: Optional[int] = None
sleep_scores: Optional[SleepScores] = None
auto_sleep_start_timestamp_gmt: Optional[int] = None
auto_sleep_end_timestamp_gmt: Optional[int] = None
sleep_quality_type_pk: Optional[int] = None
sleep_result_type_pk: Optional[int] = None
average_sp_o2_value: Optional[float] = None
lowest_sp_o2_value: Optional[int] = None
highest_sp_o2_value: Optional[int] = None
average_sp_o2_hr_sleep: Optional[float] = None
average_respiration_value: Optional[float] = None
lowest_respiration_value: Optional[float] = None
highest_respiration_value: Optional[float] = None
avg_sleep_stress: Optional[float] = None
age_group: Optional[str] = None
sleep_score_feedback: Optional[str] = None
sleep_score_insight: Optional[str] = None
@property
def sleep_start(self) -> datetime:
return get_localized_datetime(
self.sleep_start_timestamp_gmt, self.sleep_start_timestamp_local
)
@property
def sleep_end(self) -> datetime:
return get_localized_datetime(
self.sleep_end_timestamp_gmt, self.sleep_end_timestamp_local
)
@dataclass
class SleepMovement:
start_gmt: datetime
end_gmt: datetime
activity_level: float
@dataclass
class SleepData(Data):
daily_sleep_dto: DailySleepDTO
sleep_movement: Optional[list[SleepMovement]] = None
@classmethod
def get(
cls,
day: Union[date, str],
*,
buffer_minutes: int = 60,
client: Optional[http.Client] = None,
) -> Optional[Self]:
client = client or http.client
path = (
f"/wellness-service/wellness/dailySleepData/{client.username}?"
f"nonSleepBufferMinutes={buffer_minutes}&date={day}"
)
sleep_data = client.connectapi(path)
assert sleep_data
sleep_data = camel_to_snake_dict(sleep_data)
assert isinstance(sleep_data, dict)
return (
cls(**sleep_data) if sleep_data["daily_sleep_dto"]["id"] else None
)
@classmethod
def list(cls, *args, **kwargs) -> list[Self]:
data = super().list(*args, **kwargs)
return sorted(data, key=lambda x: x.daily_sleep_dto.calendar_date)

View File

@@ -1,81 +0,0 @@
from datetime import date, datetime, timedelta
from itertools import chain
from pydantic import Field, ValidationInfo, field_validator
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import (
camel_to_snake_dict,
format_end_date,
get_localized_datetime,
)
from ._base import MAX_WORKERS, Data
@dataclass
class WeightData(Data):
sample_pk: int
calendar_date: date
weight: int
source_type: str
weight_delta: float
timestamp_gmt: int
datetime_utc: datetime = Field(..., alias="timestamp_gmt")
datetime_local: datetime = Field(..., alias="date")
bmi: float | None = None
body_fat: float | None = None
body_water: float | None = None
bone_mass: int | None = None
muscle_mass: int | None = None
physique_rating: float | None = None
visceral_fat: float | None = None
metabolic_age: int | None = None
@field_validator("datetime_local", mode="before")
@classmethod
def to_localized_datetime(cls, v: int, info: ValidationInfo) -> datetime:
return get_localized_datetime(info.data["timestamp_gmt"], v)
@classmethod
def get(
cls, day: date | str, *, client: http.Client | None = None
) -> Self | None:
client = client or http.client
path = f"/weight-service/weight/dayview/{day}"
data = client.connectapi(path)
day_weight_list = data["dateWeightList"] if data else []
if not day_weight_list:
return None
# Get first (most recent) weight entry for the day
weight_data = camel_to_snake_dict(day_weight_list[0])
return cls(**weight_data)
@classmethod
def list(
cls,
end: date | str | None = None,
days: int = 1,
*,
client: http.Client | None = None,
max_workers: int = MAX_WORKERS,
) -> list[Self]:
client = client or http.client
end = format_end_date(end)
start = end - timedelta(days=days - 1)
data = client.connectapi(
f"/weight-service/weight/range/{start}/{end}?includeAll=true"
)
weight_summaries = data["dailyWeightSummaries"] if data else []
weight_metrics = chain.from_iterable(
summary["allWeightMetrics"] for summary in weight_summaries
)
weight_data_list = (
cls(**camel_to_snake_dict(weight_data))
for weight_data in weight_metrics
)
return sorted(weight_data_list, key=lambda d: d.datetime_utc)

View File

@@ -1,18 +0,0 @@
from dataclasses import dataclass
from requests import HTTPError
@dataclass
class GarthException(Exception):
"""Base exception for all garth exceptions."""
msg: str
@dataclass
class GarthHTTPError(GarthException):
error: HTTPError
def __str__(self) -> str:
return f"{self.msg}: {self.error}"

View File

@@ -1,247 +0,0 @@
import base64
import json
import os
from typing import IO, Any, Dict, Literal, Tuple
from urllib.parse import urljoin
from requests import HTTPError, Response, Session
from requests.adapters import HTTPAdapter, Retry
from . import sso
from .auth_tokens import OAuth1Token, OAuth2Token
from .exc import GarthHTTPError
from .utils import asdict
USER_AGENT = {"User-Agent": "GCM-iOS-5.7.2.1"}
class Client:
sess: Session
last_resp: Response
domain: str = "garmin.com"
oauth1_token: OAuth1Token | Literal["needs_mfa"] | None = None
oauth2_token: OAuth2Token | dict[str, Any] | None = None
timeout: int = 10
retries: int = 3
status_forcelist: Tuple[int, ...] = (408, 429, 500, 502, 503, 504)
backoff_factor: float = 0.5
pool_connections: int = 10
pool_maxsize: int = 10
_user_profile: Dict[str, Any] | None = None
def __init__(self, session: Session | None = None, **kwargs):
self.sess = session if session else Session()
self.sess.headers.update(USER_AGENT)
self.configure(
timeout=self.timeout,
retries=self.retries,
status_forcelist=self.status_forcelist,
backoff_factor=self.backoff_factor,
**kwargs,
)
def configure(
self,
/,
oauth1_token: OAuth1Token | None = None,
oauth2_token: OAuth2Token | None = None,
domain: str | None = None,
proxies: Dict[str, str] | None = None,
ssl_verify: bool | None = None,
timeout: int | None = None,
retries: int | None = None,
status_forcelist: Tuple[int, ...] | None = None,
backoff_factor: float | None = None,
pool_connections: int | None = None,
pool_maxsize: int | None = None,
):
if oauth1_token is not None:
self.oauth1_token = oauth1_token
if oauth2_token is not None:
self.oauth2_token = oauth2_token
if domain:
self.domain = domain
if proxies is not None:
self.sess.proxies.update(proxies)
if ssl_verify is not None:
self.sess.verify = ssl_verify
if timeout is not None:
self.timeout = timeout
if retries is not None:
self.retries = retries
if status_forcelist is not None:
self.status_forcelist = status_forcelist
if backoff_factor is not None:
self.backoff_factor = backoff_factor
if pool_connections is not None:
self.pool_connections = pool_connections
if pool_maxsize is not None:
self.pool_maxsize = pool_maxsize
retry = Retry(
total=self.retries,
status_forcelist=self.status_forcelist,
backoff_factor=self.backoff_factor,
)
adapter = HTTPAdapter(
max_retries=retry,
pool_connections=self.pool_connections,
pool_maxsize=self.pool_maxsize,
)
self.sess.mount("https://", adapter)
@property
def user_profile(self):
if not self._user_profile:
self._user_profile = self.connectapi(
"/userprofile-service/socialProfile"
)
assert isinstance(self._user_profile, dict), (
"No profile from connectapi"
)
return self._user_profile
@property
def profile(self):
return self.user_profile
@property
def username(self):
return self.user_profile["userName"]
def request(
self,
method: str,
subdomain: str,
path: str,
/,
api: bool = False,
referrer: str | bool = False,
headers: dict = {},
**kwargs,
) -> Response:
url = f"https://{subdomain}.{self.domain}"
url = urljoin(url, path)
if referrer is True and self.last_resp:
headers["referer"] = self.last_resp.url
if api:
assert self.oauth1_token, (
"OAuth1 token is required for API requests"
)
if (
not isinstance(self.oauth2_token, OAuth2Token)
or self.oauth2_token.expired
):
self.refresh_oauth2()
headers["Authorization"] = str(self.oauth2_token)
self.last_resp = self.sess.request(
method,
url,
headers=headers,
timeout=self.timeout,
**kwargs,
)
try:
self.last_resp.raise_for_status()
except HTTPError as e:
raise GarthHTTPError(
msg="Error in request",
error=e,
)
return self.last_resp
def get(self, *args, **kwargs) -> Response:
return self.request("GET", *args, **kwargs)
def post(self, *args, **kwargs) -> Response:
return self.request("POST", *args, **kwargs)
def delete(self, *args, **kwargs) -> Response:
return self.request("DELETE", *args, **kwargs)
def put(self, *args, **kwargs) -> Response:
return self.request("PUT", *args, **kwargs)
def login(self, *args, **kwargs):
self.oauth1_token, self.oauth2_token = sso.login(
*args, **kwargs, client=self
)
return self.oauth1_token, self.oauth2_token
def resume_login(self, *args, **kwargs):
self.oauth1_token, self.oauth2_token = sso.resume_login(
*args, **kwargs
)
return self.oauth1_token, self.oauth2_token
def refresh_oauth2(self):
assert self.oauth1_token and isinstance(
self.oauth1_token, OAuth1Token
), "OAuth1 token is required for OAuth2 refresh"
# There is a way to perform a refresh of an OAuth2 token, but it
# appears even Garmin uses this approach when the OAuth2 is expired
self.oauth2_token = sso.exchange(self.oauth1_token, self)
def connectapi(
self, path: str, method="GET", **kwargs
) -> Dict[str, Any] | None:
resp = self.request(method, "connectapi", path, api=True, **kwargs)
if resp.status_code == 204:
return None
return resp.json()
def download(self, path: str, **kwargs) -> bytes:
resp = self.get("connectapi", path, api=True, **kwargs)
return resp.content
def upload(
self, fp: IO[bytes], /, path: str = "/upload-service/upload"
) -> Dict[str, Any]:
fname = os.path.basename(fp.name)
files = {"file": (fname, fp)}
result = self.connectapi(
path,
method="POST",
files=files,
)
assert result is not None, "No result from upload"
return result
def dump(self, dir_path: str):
dir_path = os.path.expanduser(dir_path)
os.makedirs(dir_path, exist_ok=True)
with open(os.path.join(dir_path, "oauth1_token.json"), "w") as f:
if self.oauth1_token:
json.dump(asdict(self.oauth1_token), f, indent=4)
with open(os.path.join(dir_path, "oauth2_token.json"), "w") as f:
if self.oauth2_token:
json.dump(asdict(self.oauth2_token), f, indent=4)
def dumps(self) -> str:
r = []
r.append(asdict(self.oauth1_token))
r.append(asdict(self.oauth2_token))
s = json.dumps(r)
return base64.b64encode(s.encode()).decode()
def load(self, dir_path: str):
dir_path = os.path.expanduser(dir_path)
with open(os.path.join(dir_path, "oauth1_token.json")) as f:
oauth1 = OAuth1Token(**json.load(f))
with open(os.path.join(dir_path, "oauth2_token.json")) as f:
oauth2 = OAuth2Token(**json.load(f))
self.configure(
oauth1_token=oauth1, oauth2_token=oauth2, domain=oauth1.domain
)
def loads(self, s: str):
oauth1, oauth2 = json.loads(base64.b64decode(s))
self.configure(
oauth1_token=OAuth1Token(**oauth1),
oauth2_token=OAuth2Token(**oauth2),
domain=oauth1.get("domain"),
)
client = Client()

View File

@@ -1,259 +0,0 @@
import asyncio
import re
import time
from typing import Any, Callable, Dict, Literal, Tuple
from urllib.parse import parse_qs
import requests
from requests import Session
from requests_oauthlib import OAuth1Session
from . import http
from .auth_tokens import OAuth1Token, OAuth2Token
from .exc import GarthException
CSRF_RE = re.compile(r'name="_csrf"\s+value="(.+?)"')
TITLE_RE = re.compile(r"<title>(.+?)</title>")
OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json"
OAUTH_CONSUMER: Dict[str, str] = {}
USER_AGENT = {"User-Agent": "com.garmin.android.apps.connectmobile"}
class GarminOAuth1Session(OAuth1Session):
def __init__(
self,
/,
parent: Session | None = None,
**kwargs,
):
global OAUTH_CONSUMER
if not OAUTH_CONSUMER:
OAUTH_CONSUMER = requests.get(OAUTH_CONSUMER_URL).json()
super().__init__(
OAUTH_CONSUMER["consumer_key"],
OAUTH_CONSUMER["consumer_secret"],
**kwargs,
)
if parent is not None:
self.mount("https://", parent.adapters["https://"])
self.proxies = parent.proxies
self.verify = parent.verify
def login(
email: str,
password: str,
/,
client: "http.Client | None" = None,
prompt_mfa: Callable | None = lambda: input("MFA code: "),
return_on_mfa: bool = False,
) -> (
Tuple[OAuth1Token, OAuth2Token]
| Tuple[Literal["needs_mfa"], dict[str, Any]]
):
"""Login to Garmin Connect.
Args:
email: Garmin account email
password: Garmin account password
client: Optional HTTP client to use
prompt_mfa: Callable that prompts for MFA code. Returns on MFA if None.
return_on_mfa: If True, returns dict with MFA info instead of prompting
Returns:
If return_on_mfa=False (default):
Tuple[OAuth1Token, OAuth2Token]: OAuth tokens after login
If return_on_mfa=True and MFA required:
dict: Contains needs_mfa and client_state for resume_login()
"""
client = client or http.client
# Define params based on domain
SSO = f"https://sso.{client.domain}/sso"
SSO_EMBED = f"{SSO}/embed"
SSO_EMBED_PARAMS = dict(
id="gauth-widget",
embedWidget="true",
gauthHost=SSO,
)
SIGNIN_PARAMS = {
**SSO_EMBED_PARAMS,
**dict(
gauthHost=SSO_EMBED,
service=SSO_EMBED,
source=SSO_EMBED,
redirectAfterAccountLoginUrl=SSO_EMBED,
redirectAfterAccountCreationUrl=SSO_EMBED,
),
}
# Set cookies
client.get("sso", "/sso/embed", params=SSO_EMBED_PARAMS)
# Get CSRF token
client.get(
"sso",
"/sso/signin",
params=SIGNIN_PARAMS,
referrer=True,
)
csrf_token = get_csrf_token(client.last_resp.text)
# Submit login form with email and password
client.post(
"sso",
"/sso/signin",
params=SIGNIN_PARAMS,
referrer=True,
data=dict(
username=email,
password=password,
embed="true",
_csrf=csrf_token,
),
)
title = get_title(client.last_resp.text)
# Handle MFA
if "MFA" in title:
if return_on_mfa or prompt_mfa is None:
return "needs_mfa", {
"signin_params": SIGNIN_PARAMS,
"client": client,
}
handle_mfa(client, SIGNIN_PARAMS, prompt_mfa)
title = get_title(client.last_resp.text)
if title != "Success":
raise GarthException(f"Unexpected title: {title}")
return _complete_login(client)
def get_oauth1_token(ticket: str, client: "http.Client") -> OAuth1Token:
sess = GarminOAuth1Session(parent=client.sess)
base_url = f"https://connectapi.{client.domain}/oauth-service/oauth/"
login_url = f"https://sso.{client.domain}/sso/embed"
url = (
f"{base_url}preauthorized?ticket={ticket}&login-url={login_url}"
"&accepts-mfa-tokens=true"
)
resp = sess.get(
url,
headers=USER_AGENT,
timeout=client.timeout,
)
resp.raise_for_status()
parsed = parse_qs(resp.text)
token = {k: v[0] for k, v in parsed.items()}
return OAuth1Token(domain=client.domain, **token) # type: ignore
def exchange(oauth1: OAuth1Token, client: "http.Client") -> OAuth2Token:
sess = GarminOAuth1Session(
resource_owner_key=oauth1.oauth_token,
resource_owner_secret=oauth1.oauth_token_secret,
parent=client.sess,
)
data = dict(mfa_token=oauth1.mfa_token) if oauth1.mfa_token else {}
base_url = f"https://connectapi.{client.domain}/oauth-service/oauth/"
url = f"{base_url}exchange/user/2.0"
headers = {
**USER_AGENT,
**{"Content-Type": "application/x-www-form-urlencoded"},
}
resp = sess.post(
url,
headers=headers,
data=data,
timeout=client.timeout,
)
resp.raise_for_status()
token = resp.json()
return OAuth2Token(**set_expirations(token))
def handle_mfa(
client: "http.Client", signin_params: dict, prompt_mfa: Callable
) -> None:
csrf_token = get_csrf_token(client.last_resp.text)
if asyncio.iscoroutinefunction(prompt_mfa):
mfa_code = asyncio.run(prompt_mfa())
else:
mfa_code = prompt_mfa()
client.post(
"sso",
"/sso/verifyMFA/loginEnterMfaCode",
params=signin_params,
referrer=True,
data={
"mfa-code": mfa_code,
"embed": "true",
"_csrf": csrf_token,
"fromPage": "setupEnterMfaCode",
},
)
def set_expirations(token: dict) -> dict:
token["expires_at"] = int(time.time() + token["expires_in"])
token["refresh_token_expires_at"] = int(
time.time() + token["refresh_token_expires_in"]
)
return token
def get_csrf_token(html: str) -> str:
m = CSRF_RE.search(html)
if not m:
raise GarthException("Couldn't find CSRF token")
return m.group(1)
def get_title(html: str) -> str:
m = TITLE_RE.search(html)
if not m:
raise GarthException("Couldn't find title")
return m.group(1)
def resume_login(
client_state: dict, mfa_code: str
) -> Tuple[OAuth1Token, OAuth2Token]:
"""Complete login after MFA code is provided.
Args:
client_state: The client state from login() when MFA was needed
mfa_code: The MFA code provided by the user
Returns:
Tuple[OAuth1Token, OAuth2Token]: The OAuth tokens after login
"""
client = client_state["client"]
signin_params = client_state["signin_params"]
handle_mfa(client, signin_params, lambda: mfa_code)
return _complete_login(client)
def _complete_login(client: "http.Client") -> Tuple[OAuth1Token, OAuth2Token]:
"""Complete the login process after successful authentication.
Args:
client: The HTTP client
Returns:
Tuple[OAuth1Token, OAuth2Token]: The OAuth tokens
"""
# Parse ticket
m = re.search(r'embed\?ticket=([^"]+)"', client.last_resp.text)
if not m:
raise GarthException(
"Couldn't find ticket in response"
) # pragma: no cover
ticket = m.group(1)
oauth1 = get_oauth1_token(ticket, client)
oauth2 = exchange(oauth1, client)
return oauth1, oauth2

View File

@@ -1,18 +0,0 @@
__all__ = [
"DailyHRV",
"DailyHydration",
"DailyIntensityMinutes",
"DailySleep",
"DailySteps",
"DailyStress",
"WeeklyIntensityMinutes",
"WeeklyStress",
"WeeklySteps",
]
from .hrv import DailyHRV
from .hydration import DailyHydration
from .intensity_minutes import DailyIntensityMinutes, WeeklyIntensityMinutes
from .sleep import DailySleep
from .steps import DailySteps, WeeklySteps
from .stress import DailyStress, WeeklyStress

View File

@@ -1,53 +0,0 @@
from datetime import date, timedelta
from typing import ClassVar
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import camel_to_snake_dict, format_end_date
@dataclass
class Stats:
calendar_date: date
_path: ClassVar[str]
_page_size: ClassVar[int]
@classmethod
def list(
cls,
end: date | str | None = None,
period: int = 1,
*,
client: http.Client | None = None,
) -> list[Self]:
client = client or http.client
end = format_end_date(end)
period_type = "days" if "daily" in cls._path else "weeks"
if period > cls._page_size:
page = cls.list(end, cls._page_size, client=client)
if not page:
return []
page = (
cls.list(
end - timedelta(**{period_type: cls._page_size}),
period - cls._page_size,
client=client,
)
+ page
)
return page
start = end - timedelta(**{period_type: period - 1})
path = cls._path.format(start=start, end=end, period=period)
page_dirs = client.connectapi(path)
if not isinstance(page_dirs, list) or not page_dirs:
return []
page_dirs = [d for d in page_dirs if isinstance(d, dict)]
if page_dirs and "values" in page_dirs[0]:
page_dirs = [{**stat, **stat.pop("values")} for stat in page_dirs]
page_dirs = [camel_to_snake_dict(stat) for stat in page_dirs]
return [cls(**stat) for stat in page_dirs]

View File

@@ -1,66 +0,0 @@
from datetime import date, datetime, timedelta
from typing import Any, ClassVar, cast
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import camel_to_snake_dict, format_end_date
@dataclass
class HRVBaseline:
low_upper: int
balanced_low: int
balanced_upper: int
marker_value: float | None
@dataclass
class DailyHRV:
calendar_date: date
weekly_avg: int | None
last_night_avg: int | None
last_night_5_min_high: int | None
baseline: HRVBaseline | None
status: str
feedback_phrase: str
create_time_stamp: datetime
_path: ClassVar[str] = "/hrv-service/hrv/daily/{start}/{end}"
_page_size: ClassVar[int] = 28
@classmethod
def list(
cls,
end: date | str | None = None,
period: int = 28,
*,
client: http.Client | None = None,
) -> list[Self]:
client = client or http.client
end = format_end_date(end)
# Paginate if period is greater than page size
if period > cls._page_size:
page = cls.list(end, cls._page_size, client=client)
if not page:
return []
page = (
cls.list(
end - timedelta(days=cls._page_size),
period - cls._page_size,
client=client,
)
+ page
)
return page
start = end - timedelta(days=period - 1)
path = cls._path.format(start=start, end=end)
response = client.connectapi(path)
if response is None:
return []
daily_hrv = camel_to_snake_dict(response)["hrv_summaries"]
daily_hrv = cast(list[dict[str, Any]], daily_hrv)
return [cls(**hrv) for hrv in daily_hrv]

View File

@@ -1,17 +0,0 @@
from typing import ClassVar
from pydantic.dataclasses import dataclass
from ._base import Stats
BASE_PATH = "/usersummary-service/stats/hydration"
@dataclass
class DailyHydration(Stats):
value_in_ml: float
goal_in_ml: float
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
_page_size: ClassVar[int] = 28

View File

@@ -1,28 +0,0 @@
from typing import ClassVar
from pydantic.dataclasses import dataclass
from ._base import Stats
BASE_PATH = "/usersummary-service/stats/im"
@dataclass
class DailyIntensityMinutes(Stats):
weekly_goal: int
moderate_value: int | None = None
vigorous_value: int | None = None
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
_page_size: ClassVar[int] = 28
@dataclass
class WeeklyIntensityMinutes(Stats):
weekly_goal: int
moderate_value: int | None = None
vigorous_value: int | None = None
_path: ClassVar[str] = f"{BASE_PATH}/weekly/{{start}}/{{end}}"
_page_size: ClassVar[int] = 52

View File

@@ -1,15 +0,0 @@
from typing import ClassVar
from pydantic.dataclasses import dataclass
from ._base import Stats
@dataclass
class DailySleep(Stats):
value: int | None
_path: ClassVar[str] = (
"/wellness-service/stats/daily/sleep/score/{start}/{end}"
)
_page_size: ClassVar[int] = 28

View File

@@ -1,30 +0,0 @@
from typing import ClassVar
from pydantic.dataclasses import dataclass
from ._base import Stats
BASE_PATH = "/usersummary-service/stats/steps"
@dataclass
class DailySteps(Stats):
total_steps: int | None
total_distance: int | None
step_goal: int
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
_page_size: ClassVar[int] = 28
@dataclass
class WeeklySteps(Stats):
total_steps: int
average_steps: float
average_distance: float
total_distance: float
wellness_data_days_count: int
_path: ClassVar[str] = f"{BASE_PATH}/weekly/{{end}}/{{period}}"
_page_size: ClassVar[int] = 52

View File

@@ -1,28 +0,0 @@
from typing import ClassVar
from pydantic.dataclasses import dataclass
from ._base import Stats
BASE_PATH = "/usersummary-service/stats/stress"
@dataclass
class DailyStress(Stats):
overall_stress_level: int
rest_stress_duration: int | None = None
low_stress_duration: int | None = None
medium_stress_duration: int | None = None
high_stress_duration: int | None = None
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
_page_size: ClassVar[int] = 28
@dataclass
class WeeklyStress(Stats):
value: int
_path: ClassVar[str] = f"{BASE_PATH}/weekly/{{end}}/{{period}}"
_page_size: ClassVar[int] = 52

View File

@@ -1,5 +0,0 @@
from .profile import UserProfile
from .settings import UserSettings
__all__ = ["UserProfile", "UserSettings"]

View File

@@ -1,79 +0,0 @@
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import camel_to_snake_dict
@dataclass
class UserProfile:
id: int
profile_id: int
garmin_guid: str
display_name: str
full_name: str
user_name: str
profile_image_type: str | None
profile_image_url_large: str | None
profile_image_url_medium: str | None
profile_image_url_small: str | None
location: str | None
facebook_url: str | None
twitter_url: str | None
personal_website: str | None
motivation: str | None
bio: str | None
primary_activity: str | None
favorite_activity_types: list[str]
running_training_speed: float
cycling_training_speed: float
favorite_cycling_activity_types: list[str]
cycling_classification: str | None
cycling_max_avg_power: float
swimming_training_speed: float
profile_visibility: str
activity_start_visibility: str
activity_map_visibility: str
course_visibility: str
activity_heart_rate_visibility: str
activity_power_visibility: str
badge_visibility: str
show_age: bool
show_weight: bool
show_height: bool
show_weight_class: bool
show_age_range: bool
show_gender: bool
show_activity_class: bool
show_vo_2_max: bool
show_personal_records: bool
show_last_12_months: bool
show_lifetime_totals: bool
show_upcoming_events: bool
show_recent_favorites: bool
show_recent_device: bool
show_recent_gear: bool
show_badges: bool
other_activity: str | None
other_primary_activity: str | None
other_motivation: str | None
user_roles: list[str]
name_approved: bool
user_profile_full_name: str
make_golf_scorecards_private: bool
allow_golf_live_scoring: bool
allow_golf_scoring_by_connections: bool
user_level: int
user_point: int
level_update_date: str
level_is_viewed: bool
level_point_threshold: int
user_point_offset: int
user_pro: bool
@classmethod
def get(cls, /, client: http.Client | None = None) -> Self:
client = client or http.client
profile = client.connectapi("/userprofile-service/socialProfile")
assert isinstance(profile, dict)
return cls(**camel_to_snake_dict(profile))

View File

@@ -1,108 +0,0 @@
from datetime import date
from typing import Dict
from pydantic.dataclasses import dataclass
from typing_extensions import Self
from .. import http
from ..utils import camel_to_snake_dict
@dataclass
class PowerFormat:
format_id: int
format_key: str
min_fraction: int
max_fraction: int
grouping_used: bool
display_format: str | None
@dataclass
class FirstDayOfWeek:
day_id: int
day_name: str
sort_order: int
is_possible_first_day: bool
@dataclass
class WeatherLocation:
use_fixed_location: bool | None
latitude: float | None
longitude: float | None
location_name: str | None
iso_country_code: str | None
postal_code: str | None
@dataclass
class UserData:
gender: str
weight: float
height: float
time_format: str
birth_date: date
measurement_system: str
activity_level: str | None
handedness: str
power_format: PowerFormat
heart_rate_format: PowerFormat
first_day_of_week: FirstDayOfWeek
vo_2_max_running: float | None
vo_2_max_cycling: float | None
lactate_threshold_speed: float | None
lactate_threshold_heart_rate: float | None
dive_number: int | None
intensity_minutes_calc_method: str
moderate_intensity_minutes_hr_zone: int
vigorous_intensity_minutes_hr_zone: int
hydration_measurement_unit: str
hydration_containers: list[Dict[str, float | str | None]]
hydration_auto_goal_enabled: bool
firstbeat_max_stress_score: float | None
firstbeat_cycling_lt_timestamp: int | None
firstbeat_running_lt_timestamp: int | None
threshold_heart_rate_auto_detected: bool
ftp_auto_detected: bool | None
training_status_paused_date: str | None
weather_location: WeatherLocation | None
golf_distance_unit: str | None
golf_elevation_unit: str | None
golf_speed_unit: str | None
external_bottom_time: float | None
@dataclass
class UserSleep:
sleep_time: int
default_sleep_time: bool
wake_time: int
default_wake_time: bool
@dataclass
class UserSleepWindow:
sleep_window_frequency: str
start_sleep_time_seconds_from_midnight: int
end_sleep_time_seconds_from_midnight: int
@dataclass
class UserSettings:
id: int
user_data: UserData
user_sleep: UserSleep
connect_date: str | None
source_type: str | None
user_sleep_windows: list[UserSleepWindow] | None = None
@classmethod
def get(cls, /, client: http.Client | None = None) -> Self:
client = client or http.client
settings = client.connectapi(
"/userprofile-service/userprofile/user-settings"
)
assert isinstance(settings, dict)
data = camel_to_snake_dict(settings)
return cls(**data)

View File

@@ -1,73 +0,0 @@
import dataclasses
import re
from datetime import date, datetime, timedelta, timezone
from typing import Any, Dict, List, Union
CAMEL_TO_SNAKE = re.compile(
r"((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z])|(?<=[a-zA-Z])[0-9])"
)
def camel_to_snake(camel_str: str) -> str:
snake_str = CAMEL_TO_SNAKE.sub(r"_\1", camel_str)
return snake_str.lower()
def camel_to_snake_dict(camel_dict: Dict[str, Any]) -> Dict[str, Any]:
"""
Converts a dictionary's keys from camel case to snake case. This version
handles nested dictionaries and lists.
"""
snake_dict: Dict[str, Any] = {}
for k, v in camel_dict.items():
new_key = camel_to_snake(k)
if isinstance(v, dict):
snake_dict[new_key] = camel_to_snake_dict(v)
elif isinstance(v, list):
snake_dict[new_key] = [
camel_to_snake_dict(i) if isinstance(i, dict) else i for i in v
]
else:
snake_dict[new_key] = v
return snake_dict
def format_end_date(end: Union[date, str, None]) -> date:
if end is None:
end = date.today()
elif isinstance(end, str):
end = date.fromisoformat(end)
return end
def date_range(date_: Union[date, str], days: int):
date_ = date_ if isinstance(date_, date) else date.fromisoformat(date_)
for day in range(days):
yield date_ - timedelta(days=day)
def asdict(obj):
if dataclasses.is_dataclass(obj):
result = {}
for field in dataclasses.fields(obj):
value = getattr(obj, field.name)
result[field.name] = asdict(value)
return result
if isinstance(obj, List):
return [asdict(v) for v in obj]
if isinstance(obj, (datetime, date)):
return obj.isoformat()
return obj
def get_localized_datetime(
gmt_timestamp: int, local_timestamp: int
) -> datetime:
local_diff = local_timestamp - gmt_timestamp
local_offset = timezone(timedelta(milliseconds=local_diff))
gmt_time = datetime.fromtimestamp(gmt_timestamp / 1000, timezone.utc)
return gmt_time.astimezone(local_offset)

View File

@@ -1 +0,0 @@
__version__ = "0.5.17"

File diff suppressed because one or more lines are too long

View File

@@ -1,65 +0,0 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
referer:
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: GET
uri: https://connectapi.garmin.com/usersummary-service/stats/stress/daily/2023-07-21/2023-07-21
response:
body:
string: '[{"calendarDate": "2023-07-21", "values": {"highStressDuration": 3240,
"lowStressDuration": 20280, "overallStressLevel": 35, "restStressDuration":
31020, "mediumStressDuration": 11640}}]'
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 7f12d932aa00b6ee-QRO
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Fri, 04 Aug 2023 00:57:49 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=FuGLLTTuU8CV4eTRQnQ7XY0oTrHoXEaIYrPbxrkK1vRVT4yAr2Zv0YIj4D%2BZ0eQTeYgycpuCP1gSE4yk0bZE2Aj2p29AIZ2Ce%2BuOUJqB9Mp54VyHR9uEC5AAcVLUYqtzpE4YIK0Fgw%3D%3D"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Transfer-Encoding:
- chunked
alt-svc:
- h3=":443"; ma=86400
cache-control:
- no-cache, no-store, private
pragma:
- no-cache
set-cookie:
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
status:
code: 200
message: OK
version: 1

View File

@@ -1,223 +0,0 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
method: GET
uri: https://connectapi.garmin.com/activity-service/activity/12135235656
response:
body:
string: !!binary |
H4sIAAAAAAAAA6VW23LbNhD9lQ6eRZsXURL5ZstOookle2SpmbbT8ayAJYUGBDgAqETJ+N874EWk
ZbV96Btx9oLF7p5d/iRALT9we1wwkgZhEMVhFE/iyegk2G4XdyT9SaqKM5KS2SxJqD9NvCQJY288
zSIPwmnosSBE5tNxsIMpee3NV1AgSclvKgcyIpVB/aRVxgW6C8M4CSZ+OCLcLCth+XOptH0CjdKS
NANhsHe0OZZ4t3l0odhjWZsHk2hUHz7jkaTk2NxR1vabVidMnPdPnDGUJ58ajdWcWmQnyGpeFLAT
2CKvI4KH1s/Ztcnw0kpSsJgrzX8gIyNilLaPmqEmaeDXeaBozFxJq5VYV+LcWTh0Vmp+AIsuf5YX
+LuSuJXctiaV5LZ+dhyO6kNjdFOg5hSu53tOIVdkRDKgVmmS+ld+7+iC5uuIFGiBgYX2Cm4eNc+5
BEFSqyscEYYHTvGmLAWnYLmSC2ksCNF8u2yEQTh2/ZKjtP+iJyshWq1nq7nMO8g1wwelC7Augqz+
cgbTUXtonplxW/eVMYpysMjmqtIGe9cCjN2WDCzeuRymJPTDyPMTL0w2YZCOkzSOr3zXhKVQwJD9
h9qBM1RbLTr/ezBPShwFl3jqmj2Y+R60vQMLXcb2YD7pDS9wIV3ezQB/Ut/wraj143ixkJm6s6qu
dE+Tp68DmjBuSgFH2XCqsFDwH9wVvBKiBZdgufxl00nKlmwF5LjV4gF0jt2DzmRLZLwqSEr21pYm
vb420RUU8ENJ+GauqCquc9AFlx5VUiK1XqkVu26dvHDnxVxPZjEECSYeTHfojSEIvGSXJV4GFHwa
syALwGvfc1XK/H2IzwUI8X+jiPww88PA85FOvTGjibfzaeQxF9yOxuDP6FkUbcp79tM9F2zBDEn/
+LM93QxGUYsblEbp9nCapXUUDVaArBwfK+1GAvl4s14uVsSV8oCrqtg5tOvfcq4qN/mCYZt/Ufqr
qmzf59zc2LILpQMbmi7Rwl3P5gZ0liSKwiSZziazgHTK7hWuu6LJdOZ34K+oDVfS4Q2xX+vGXUiL
0rinSYv6AMIMKfBcCm57BG/5V1zC9xtjuLFLxVwumjBr0S1Yi/q4NdD34lCwxgK4HEyIWth7q4my
eXzgxg64ua5kTa8vXLKGjo2IgkDJQN+7ae5MO0GuVVWuOcNmw7Xalb40WwqwdI9sfllaojZKglgj
VbpfKVBZNQdB5yCU5gO2Z3BQmtt+jLgmAdHXtFtTZ0+6l25Bna7N6V9++HaBDRYaCjzUw3eutMbB
rnsdEVMVBehj2ybGgrZuKj0o6gZ/PxFnmyBJAz+Np/VEPCl+XG7eDk7fH6gxlyWuJEmTq8l0MiKF
OnCZ353DKKA0yN7h9JSvwC2wXaH7DNYrDQ6oIcdPa5LOpg4o4Ht9SAbSDRYlanDEI2k0btUuoVxe
QK1uevA+y5Da7mIJqNWO081F6SXZEk3T52T1+HJzv368Xcxfbu9X9x8Wm5dgRv7R6VvD1bmpS3Q9
b/AZHfeiEfkGFvW9sbyAutrB+3c8wA5dhberz6vHLysy+L1q9R4UsPo9syAI4mkYTuJJGNdZ6vrz
Acp3Natb49lqNIakk8jdjJKdgDo/jGcZapQUO9yrFY3F0pXWlS4/mYRtxYZOX1//BraydeSxCgAA
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 80e771592928359a-DFW
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json;charset=UTF-8
Date:
- Fri, 29 Sep 2023 21:50:37 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=m77%2F5qqWH%2FzNYg9h9aJB23Sa8erERimKhptV3iEpuPvFpQKcBvr8kHp%2B0tcMmTnLbEN%2FZr0zE7r9yfH0C5bHKK80P8CeBzFhzo9RkFicBPRHZMMaxBDwn7fNmDGgZpOGV9NydCV2LQ%3D%3D"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
Transfer-Encoding:
- chunked
alt-svc:
- h3=":443"; ma=86400
cache-control:
- no-cache, no-store, private
pragma:
- no-cache
set-cookie:
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
Content-Length:
- '0'
Cookie:
- _cfuvid=SANITIZED; ADRUM_BT1=SANITIZED; ADRUM_BTa=SANITIZED; SameSite=SANITIZED
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
method: DELETE
uri: https://connectapi.garmin.com/activity-service/activity/12135235656
response:
body:
string: ''
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 80e7715a5b05359a-DFW
Connection:
- keep-alive
Date:
- Fri, 29 Sep 2023 21:50:37 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=RR3a8akw5KIfJ40HMjm%2FVxtacqIHiN3EkPNr5ZFwq02kvcv2Wt8fzZL9kbXMXFTHMd3iL7ZcPj4074wQQsCMR29xUXurv6SH5Nd2hdW2qeQT%2Bl7fsosUtcPp3mglfZcBnFOy9JteAg%3D%3D"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
alt-svc:
- h3=":443"; ma=86400
cache-control:
- no-cache, no-store, private
pragma:
- no-cache
set-cookie:
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
status:
code: 204
message: No Content
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
Cookie:
- _cfuvid=SANITIZED; ADRUM_BT1=SANITIZED; ADRUM_BTa=SANITIZED; SameSite=SANITIZED
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
method: GET
uri: https://connectapi.garmin.com/activity-service/activity/12135235656
response:
body:
string: !!binary |
H4sIAAAAAAAAA6tWyk0tLk5MT1WyUvIICQlQMDEwUfDLL1Fwyy/NS1HSUUotKsovUrJS8ssvAQu5
ViSnFpRk5ucp1QIAv2CADDwAAAA=
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 80e7715cbe4b359a-DFW
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json;charset=UTF-8
Date:
- Fri, 29 Sep 2023 21:50:37 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=sBhGq8nTIKEsQsz%2FTdQHQGlCFN93mqZLF20y8BX8Vf4lPeqs0bM27QSt8IU1udH7S7x8wGmhmS3PzVMmthvCEbT8L1GrNICizdJ6H28Z%2Bd3F%2B4Em9Upz9aThxIiFzIPB8Zw6iEN%2Fqg%3D%3D"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Transfer-Encoding:
- chunked
alt-svc:
- h3=":443"; ma=86400
cache-control:
- no-cache, no-store, private
pragma:
- no-cache
set-cookie:
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
status:
code: 404
message: Not Found
version: 1

View File

@@ -1,618 +0,0 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
referer:
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: GET
uri: https://connectapi.garmin.com/download-service/files/activity/11998957007
response:
body:
string: !!binary |
UEsDBBQACAgIAJCTK1cAAAAAAAAAAAAAAAAYAAAAMTE5OTg5NTcwMDdfQUNUSVZJVFkuZml0tN0L
nFbT3zf+vdd1zVzTNE3TNNVMM9VIMhKm01Q611RTKiPFSAghhJzPVKaEJCaEkIQQBvErx5BMfqHj
zCDnSIRQJGme7/ezvmvP+nb//vfz/F/Pc3vdv5v9nn3ta332XnvttU/rSs+4enR5WRgcOWTYmBln
9Q/on0QsPjsen5mIzwxNuTHlSaY8CIOg58XXrn79g9P71dE/YfBpWl1dfEAQdAqSTFYiMOVhaGJh
EA951v/6T0EqfWpgEOSH8X/iMw0vkf8xdoE33jgoCIaGKfSnWHwmfXdSfGYQGvq7CU1yGMR4tjRa
zML8IIjTvxuE5j9jcRAMCWO0nIBKT4vieTrR32L0v8FB0CxI2O+gUoZcWtM0NFmhSeL5pKh1dUOC
oEWQhxln01pIic9sHJ+ZEZ/ZJD+RHZ/dBuWPY72kmvJGpryZuc2WNjk0VIiGockMg6wwtXkYtAhN
Thi0SjatQ5NPM/D38IqsO+Cf/7TODvzHrXisyzqBOvr/SW4pyS7G/4vFh/H/54v3P2FS/kcWPzGw
n4il/g+snC/THrGlDuJpBy5+aBA0Dxr+41XcWGiogiWFAVWLlNCkh6ZxaBJuF4rF+P+XBMHtQdI/
qKzG1cwUlPTV0/pN3T0J84bDaH0F59t5xsVnPtE/kWLKO5nyo015L96XZNdLCsMGYZAaBg3DIC0M
GrkvRUUMqCK2DIO8MGgdBm3CoG0YHBwG7cKgfRgcFgYdwuDwMOgYBkdQYxAGR4VBYRh0C4OiMOge
Bj3CoF+joH8YDAiDgWEwKAyKw2BwGBwTBiPCYGQYlIbBcWEwOgyOD4MxYTA2DE4Mg1NDMyEMTg9T
zwiDs8JgUmvKEEwOgwvC4MIwuCgMpoTBZWFweRhcF5rrQ3NDGNwYBlPDYFoYTA+Dm8KgPAxmhMEt
YXBrGNwWBreHZk5o7gjN3DC4MwzmhcHdYXBvGCwMg0fC4NEwWByaJWHwVGPzfGheCM2LYbAsDF4K
g0+oZodBA96UC5/G9rrx/2Sz/++qxLrETtoyYVAXxGLx1p2pTdlfF9L24n+COm4LMWU/gEmaoOoR
4w3FHob0Udq2B8Uz0tOSW8ZMqwNrZ50sjj7XOU4VK+D/ow/Ggol2CdS4BimJBo0apiUlp6eG8cDE
klqH/FXDud1rlxef2So+87D4zA7UHpryNqY835QfZMoPMeXtudkOYlJJgwRVvdCg4gTpYdAkDKgd
axYG2SE1X1RTDCqIOSJMpdrRlWuHKQpNzzA4Ogx6J4K+VPVWnhIEGTS5r2BwXy56j5iNcD2trvBl
uxaCziYYQ//VhCpqLC7HC9phjgmCimBAEJ9tMhKN4jMz46lZaan94zMHxGcOic/cj9a2Ibe25Tmm
vKWL0daUH2zKC8xt3U15D1Pe05QPM7eN9Y5HUbYGnE32iIzQNAlTm4ap2bwvGNoLDkkE7RPBoYng
sIRx+0Kqvy90TjZdXGxK2Cs0vcOgT0ixTb+Qjl5c+U9AtW+4dehVj5SMHnlE6SUXHV3UvUv3oq5B
8J9aGt0K1f/3VtoGU2L1FqN/c036D7P+f/zzJlWsujFbM2949LPAftJ+piF/7cCzTr/s0vwh516W
TwUM/vdFC//D97k54vzv/19Fwxy7Ns3asv0X09zzEUFwbHDoP/ZIG5+RGZ/RND6jWXxG8/iMnPjM
lqjFreMzD47PbBefQR2OmCmnRrAB6kQajsNZpryFucnWiUPMTbYVdjXAJMIw1W1+qtG5ockLTRsc
ltO4wV0Xtv9xXu/0IK0qPcg7LD0Y+CRtzwF5gavSW844Kii78orgPNP6x4weLYLfenSN3RuMji0J
qoLraY3N3x8EL9K/278cDnkj7BB0rEujqCODIC1oFMtPpMaN7XTY+oh+AjXShva1xmHQNMY7WqOT
Ljrn9P+2zRkb1KUezns9rUbepUYFQaPgGPSYZiTHZ1Df5CDe12cUxWcWx2cMy5gxPGPGOOw8Bqur
OXab/rLf817R0NvjqSAZ2ONb8E4f5OJo0QoHjOhocQgfMMyhoSngY4b5r8eMLjhs0DGDGobevHsE
/dxhg3aSEnfMGBUGx3rHjLG885iTwuAUPmwEk0I5VNDh4Soq28KZqIQ38v8ev8ygDuUOiGrbjf8H
/6O6OTTjAjped25JGyU5SEVrZKssGiFana1dZabGW3pYbr8LpHYfSx3koFEG1qmtgo3R2Bi0omhm
6o/Aje33VqGcbyxBqxz0D4Is9EMvoyVwBQipo31zaGZRy84VcXZq/7y6Rf0p3XZq7OONuGylwRv1
f0l6w/VWee9LfuN/tk/5xv9sn/L/dvH+J/5Dn/L/xeL/mz7l//3K+e/6lGEuHc6T3vTPTYI3bT1o
RfUgTM+PNQxWepBMTVXwlgdUybODt33ob/KDdzyIJycVBKsAA1rVVQ4I5wzd1SV414O7qvb1ClZ7
UDAxv3/wHmAgwcAwLUgrCaoAgwgG2Rq7xgMOErwPKCYo5oamMvi3B60XNm4YrPUgk7IkPlC95YG8
5/BZ4mRT/l93HsMzvxSfnTqYPj8Y0c2Hmii8+UhoSKu6iiFhvGVYYNYpGlgwr8isV3Q/rQKzQRGv
BLORaFlE+2P7h5lNQkPpG4diRZjN9XNVDsGqMNWKeDuaGk2U3tQK2bWMQB+ruRDoE6IVESHQp4pW
betSZLaAdgrdw4E+I3qNyBYVgT4XsusLgb4gWhkRAn0pZDcUAn0FmtDAbm4E+pqoKqIUDvSNEAVa
JIG2ClFtrLDV03xLtDai+P6gwHwHmp/Sr1XdtH7h3MSuLmabonlVe3uZ74m2RoRA2xUh0A9E/0SE
QD+C1iUs0XEvMDsUIdBPihDoZ6LmSY4Q6BeiI4n6tqqb3NcG2glKSfTNY4rvpUC/Kpqz7dFC85ui
uVV7epnfFSHQLkUItFsRAv1BdDhRn7y6kj420J+KEGiPouYc6C9FCLRXEwf6m6hLRPE/KNA+RePf
61Jo/lE0s2pnL7NfEQLVKQoCChR8aIkC5UugUKgfkWwhI0THxHzbFpqY0ACiAdQsUaA4aJ0QAiUJ
DSQaaAMlq7nihvahhJBd/IKh7QpNipAtxPSqHb1MA1DXFFtUBEolOoqIAuVToJ1BpWko1DuvLqc3
bbSgxKQpQsZGipAxHbSPl5UjG62xkP3GDM6YIUSlz5GMTYgOI6KMOZIxU2gQfXBQGG9OGZsKDaa5
Bof3n5NVaLIUTa/6uZdppggZmwsV59VlFoc/cKAW9d+YOdAGygZ1TaFVmNnfBsr5kCtT1xRahZmy
W7UUsnMhUK4QBcqUQHlCdvEI1AqEQJkUKJMCtRay5VowK15o2hBlEQ0hGkK1cFcvky80NK8uTRq+
g4RKiEp43y4xbUETGlhCoIOJWhINJxoe5vahQO2EjiE6xgY6RBHaifZEh0SEQIeCVjniQAWK4h0o
0GFC9I35w8PUJylQB0Vzq/b3MocrQqCOigIOdARRnk+l5khFuZ2oJT9KEQIVKsJu1YnoYKJheXXT
htlAnTVxoC6K4u0pUFeiwojenkQteTdFczhQEdHgpLw0SwjUXREC9TiQSk1PIdq0i4ZSINpCRxMd
Q0TbcVGJDdRLUSoH6n0gZZs+RJcTUezK4TZQXyFaPB0y422TCkw/IfpgZYkN1B+0vKElBBqgCIEG
KkKgQQdSqSlWhECDhagQk4faQEMUIdBQokk+ZZsSTRxoGNEVEcVb0xYarohOASvNMYpqJy0uNCOI
pkWEjCMVIeMoIdr5SobYhu9YRchYKkQNTNlgm/E4Rcg4mui6euKMxytCxjFEM5LGOOKMYxWhnThB
iHowZcXh+HN2FZoTieZFNJs7SGWKEOgkoseSdnK3dvIgu9HGHUil5mQh6ueUDbSBxitCoFOEqJ9T
Yvvu5lTQ8obUnyiR/sRpRAuJ7FwINEERjlanK1q1raLInEH0fESzuD9xpiIEmkj0rv1G2xsqMWcd
SKXmbCH64OT+tp04h+jDiBBoEmiMUMCBzlWEQOcR1dQTBzpfEQJNVlRSsKqTuYBoV0QzOdCFihDo
IqIOyWMa0rlKxQCcmpgpQrTuK+TQdLEiBLoEtIr7qxWDcD5iLlWELXSZkO37ItDlmjjQFUStiKRP
zsfaK0ETGth+9Cruwl6lCD2+qxUh0DV8OCGyy0KgaxUh0HWKcvtQoOuF7LIQ6AZuyYlsUdGS36gI
gaZq4kDTFGEfmq6oeNvvXcxNiuZzoHKiJhEh0AxF+2L7hpmZQl6gm9Vc2EKzFKHK3SJkTzLQ8N2q
yKRR+3UbaFX9XNlmtpCcW3HG29Vc8XRq3OcomndO1yJzh/rgvKo/epm5oJ1ynoaMdxJ9T/s21cJF
A2w3/S5FyFihCBnnEf0WETLeDVrVwBI22j1E+yNCoHuF6s/zzXwhuyPjvOM+ITo7mdY3TDuHauH9
RHXUN+lD1IdqIQV6QIjmqpBe7QJF6KY/KOS1Ew8pQsP3sJDXTiwUotKXSaBHhGgVlsiZ4SIh2tol
UgsfVYTdarEQVfKSweFdkyYWmseIdkc0h3t8jytCoCcUYQstEaLF9yy2gZ5UlNstCBJPfehfkwh4
EkvpOTi8lSDxtPy9Dn8fxNcs4v7VvtjT+ERdqzDr4DvfiS11U7sqclbEnnFT9O0NY8+6KT7axZ5z
U7x6YpVu6qmg/5eJ51WhzPNSbHtq3zEI9pgXFNHil5sXib6julbSivsWXBqzTBFW+ktC9oPxtmsK
zMtCw4iGhSvb7Oxu/kX0bfTBe6qCHmY5qHdjd30h6G9WKMJKf0URVvqrinB94TWiT4m8tux10KQM
O1cW16I3iH4gktJzLXpT6Bgi6WavFBpBNCKMFzUoMG8JjSQaGa7aTvv524oWcKB3hEYRjbK1aBXR
10THEh1L35haYt5VhECrFSHQe6A9TUqJSouDYBJuA8dxawM3j8M4z8JX/c5Lz8izkav8D9lTpTWK
EPl9oeOIjgtNMTWA/yaqjghrYS1oUsZootFhfDi1dh8oMrft7m4+JNoS0SJeCx8JHU90vF0L6xTF
g3iJWa8Ia2ED0edEY4jG2LWwUcjOhYybFPFVQLNZETJWgzLShThQjdBYorE2UC3RRiJb+pInuhWZ
jxUt5n7rJ4oQ6FPQZUI4RG1RhECfEb1IZNcqzm8/V4TW7gtFOB38EtS7sWwODvQV0YZ64kBfE22P
KD6EAn2jaPpwCrRV0UIO9C1oUoYlBPpOEQJtU4RA3ytCoO2KEOgH0MpML9CPihBoBypARBzoJ6Iv
iGxdjdORxfwsRHvGomPD0gIK9AsouxnteBW841GgnUK0x1aMsIF+FaL9uuIYCkQtyW9Cw4nk/PZ3
IWqoKobZQLtAS5pbQqDdQtRsTC6xZxZ/KEKgP7GFljSn9qZMzp72CFG3pUwuvf4FWpBt6aFJWYVm
r6L5fGbxtyIE2qcITeM/ihBov5A93mAfqiN6KyIECj6yREetnGLbToSKEMho4kAx0MQcSzjAxomW
EdHi8weH80raFZokRTjAJitCoATR6ojQY0gBoaj5ci25gaLckyhQKtEH9XNlUfvVUBEypglRUSuK
+fZChmkEmtLCEjKmE20isj14ZGysKJ5KGTOI/o6oauajnU0TRXOqfutlMkFfNhuYF3XzmipK4eY/
S6g4D/c5OGMzop8jwkZrrggNXwvQ0kZ0mkqHOJxsZBOlJjtCoBwhOn12NwBaErWJKN6S2olcIfvB
DY9R3zwPNDvVznUPB2pFdHBECNRaUTxILjFtFCFQPlGfiBDoIEXYQm2JLqknDnSwIgRqJyQZOdAh
ai4Eak80JaKM76jfeqiQ/eA8DlSgCIEOU4SGrwPRuREh0OGK0E50dIQeBwIdoeZCoCMVIdBRRGcn
q3tOhaDlDS3Fm1OgTorQheosZDs0d3GgLmouBOqqaD8H6kY0jkhurXGgIkUI1F2IPjhNAvUQogZm
mmyhnmouBDqaqFk9caBeitBO9FY053s6Ye+jiQIl+n6kOs886f6OznO/j/zOc6wf/k6d3BueqHgr
1t9N/VbRakVsgJvi1RIb6Ka45YwNclO8MmLFNDU1jaa4psYGuyl0sYe4KcNd7KFuCl3skmiKMseG
0dRWnuK4seFuipZSGTvGTa3dvqtLbET9337rFRtpp/JsOUd9xLc4aQrlPNZNoZylbgrlPM77W8PY
aDsl5Tze/Q3lHBNNcTnHuimU8wQ3hZKdWL9MKlmZm0LJTlIlG6dKdrIq2XhVslPcFEp2qirZaapk
E1TJTlclO0OV7ExbDaRkE90USnaWm0LJznZTKNk53t8axia5KZTsXDeFkp0XTXHJzndTKNlkN4WS
XVC/TCrZhW6qQ0M67bpIVWfDky2S6+rsxVqcdk1RhNOui4l+TJrb8pg8PiXhmm0uERqWxydU2M8v
FbJXn3HZ5zJF2M8vJ9oRLQst8RVCI/L49AaHlisVoU99lSLs51d/xBfTIuL9/BqhkXl8EhQv+qDA
XKsIDdd1REGyo4f4JOF6RQh0g6KUIKXE3KgIgaYS7Una1XJUHp9jIdA0oWPz+OwJgaYTZSU7whWE
m4SOy4u6oOVqLgSaAVrS3FK8b06BmSlkvxGBbiZqRFSaxx3VxRxoliIEukURznpuBZVmCfXna0+K
kHG2kC0EMt5OlBERGuc5itDNvuMjvtC0o6kE4oxzNXHGO4kOiSjel442dymaOfyxzqZCEc7s5oHa
NfUy3k3UnsiuVRxt7hEanRedCN0rdHxe3aLjbaD56A+0azqGaIwNdB9oR9OxeXUVY22g+4nOiAiB
HhA6Ia9u2gk20AJF8XF0tHlQEU5VH1K0kAM9THRCRAi0UBH6A48o4kCJRfowxZM7OAqdt+Iw9ag+
TPFkdjNqFMYsvXtlbLGbCuYOWBF7zE2hgXo8+hs3UE+4Kb5wGVtCU1NauAbqyWiKG6in3FSccsSe
dlPnhdQILdWN0FLEmdLCtiVohJ4BLci2hEboWaIBEXG5zXNCtqnCjlApZO933c/r9HlFfFUn8YJe
WzxJ3X70iLC2XtRr60X8nZvzIFgZW+amgrndVsReclNpQXxn7OVoTm74/0VTVTmu4V/uprBeV7ip
H/hq2Ct6jfDk4OSqHNtuYo28KuQ1y68RFUXrjctmXleESvwGUX8iu6z9IVXiN4XsslCJVwrZlYRK
/BbRiGiueOs1BeZtITsXVvg7QrafO59X+Co1Fyrxu0K21cexYTXRyGjx2CvfIyqL5sJeWSVkF4+m
dI0i7JXvC8lZDV9B/7fQoLzoCvpaTZzxAyG77ePNGxWYD4mGRKtwJj+K9JGQnQs76jqik5NXZnod
9/VCtlzJfCayQciuCWTc+BH3v91cyLhJyM6FjJtBhU3sysFGq1aEQDVEk4hsURGoVlG8PTWlHyta
NalrkfkEVJtu6wSa0k8VIdAWIftBtDyfEU2O5kKgz4XsXAj0BSgj3auFX2rijfaVIgT6WhMH+gar
0BECbVWEWvitkK052ELfEZ0WzYVA24TsKkSg74W8c8XtH9nzFZsRgX4Qsh9EoB8VoRbuUIRAPxGd
X08c6GdF8bYNCswvihBop5AtPS7m/kp0abK6I/8b0fvRXLgE87sQ5orzFF+RLUtvnGvT7VIfQbrd
ipDuD0W4S/Un0fM+ZZs9mjjdX4riHWhz7RWy63P4ksWdzd9qLrTS+9RcSPcP0YKIkG6/IgSqI3pW
b65gnU8IFCpC/8soQqAY0ZM6UFzIzoVASUQriGxlw+ZKFrI9UbSCCZB7GAOBUojeIbIfRKAGQl6g
VEWmgK8mKcLJcRpoappXJRsRLSeyc6FKpitCxsZEa+qJM2YoQsYmipAxk2gDkV05uGHSVMjLmKUI
GZsRbYoIGZuDljbyArVQhEDZRF9FhFqYcyBlJ1quUwdznlzaiDroi0bag3nuOnUwz8Xf6cB7w+Hz
3o7lualg7hErYq3cFLowrd1U/GjqwrRxU1nn7e4Sy48+FwQ9Yge5KZz9ta3/G3UCDvbmLI21c1Po
BBxCUxnprnPV3k3h5tmhbgqdq4Joikt2mJtCyTrQVG26K9nhbgol6+imULIj3NQPMep0HKlWnOHJ
1IRr29HpOArkDh3odBQS/UlHE3uw4nVoOgl5zwV1JtrvU6npIuQ1PF1BrpuDTd5NETZ5EVE8UZXj
NTzdiRrWE9fhHkKuWaUTv56g4uZeb+VoosyI7uI63EvIHcmD/qY3UeeU4ubRQaHE9BGK1kSp6Stk
l4VA/YhGRnMhUH8h6VhxoAFC3jXFgUL26ioCDQKVZknXpMNxBaaYqA+Rd+AbLGRjI9AQIXuehEBD
hexJEQKVgFZmRlRqhhF1iQiBhgvZU6c435k7Rs2FjCOE6GzK3u7KMCNBe5rYDyLjKKIj6okzHkvU
KiLcASsVsstCxuMUIeNo0KQMWwhkPF4RMo4R8s5kxypCxhOI1iYmZXhnsieC1mZIuThQmZB3tn4S
0UdE9mIAAo0TktNpvgN2spD94Alr6Ex2PB95aLfyAp2iCIFO5QMIkT2LQ6DThOjss3KsDTRBEQKd
DsI5KhECnUG0LCJsoTMPpGwzURMHOovo+Yj4XM6cDVrS3FLVuRToHEXzqkwPM0kRDg3n8qE0IpwE
nCdEZ7KV9kzWnA+qyrGEQJOJFkeEDvIFQtGayDAXKkKgizRxoCmg/DwJNJq20MVCdkWjyl1CdE5E
8znQpYoQ6DIhu9GwhS4HVbaKqNRcQTSMyH4Qga4UsnMh0FWKsIWuJurvU7a5BtS/jRAHulZRfBQF
uk5R8jbq8V9PdEhECzjQDUK2kiPQjdykJb7Ps4QtNFURAk1ThEDTFSHQTby7E0U7TIYpB1XlePvQ
DE0caKaQaxSoyt1MdGhEq+PvdjKzhOwHF3GgW0BTWrhGgQLdStQpIjxZcJuQ3d1xgWg2t4XRXMh4
uyJ0UOaAVMN3h5AtF85q5ipCxjvxjRFxxruE3EajjBWgedIClNxKu9U8RQ9xxrsVIeM9QrZ+4cby
vURH2rkWSS2cL2Qvb3GgxH3q+B/wpPs7Ok73r1Mdp/vxd+44Pb3xrdgDbsrM7bMitsBNoSPzIE1N
ynAdmYfcFDoyD0dT3JFZ6KZ4FcQecVMfnksdmUU0tbAxlml6xB51U+jILHZTuM7yWP2c1MV63E2h
i/XEOr+LtWSd38V60k114O7QU7o79BRWWEa6Pf6iO/S0UHQ6v9wsJWqTuEyI14x5Rsjrrzyr5kIN
eE7I9VfWFJhK9UG0Q88rQp/7BfVBHCleJMqOCH3uZUJef+UlRdhtXwa5E2JU6X8J2aKiSi9XhEAr
UIgMP9ArQhKofasC86qQF+g1Idu/w5nf64pQpd9QhHboTaJERAi0UsgL9JYitENvqw/irOgdor+S
HSHQKk0c6F2ilIQ71cBZ0WpFyYfs6mLeU4SnWaqImkSEQGuE7Jkfrjy8Dyps4p3m/VsRMq4V8jJ+
IGSXhY32ITJGxBk/Aq3NsISM6/DBiDjjeiG5XdKZMm4Qsqey5SW7upuNROkRPcQZN4EWNrb3IJBx
M9FBESFjtSIEqlGEQLVCttOEQB8THRwROmCfCHm3Ej4V8g4eWxShR/mZkG1+Mxo+3tl8Durd2B0N
qRZ+QdSUyDu8fylk58Ju9ZWQ16P8Wsg7Gn4Dct+IQFvX8VlGVFQO9C1RXjQXAn2niQNtU4RA34O2
plmaOXx3F7OdKIvIFhW71Q+KEOhHRQi0QxEC/QQqaeQF+lkRdqtfhLwttFPI66/8ShSLPohAv4Ey
0uXQxx2w34W8DtguRbhDt5voF9pFvR7lH0J2Ltyh+5NoX7JbPALtUYRAfwl5gfYq4huw5m/1QQTa
B6pN9wL9owhbaL8iBKpDUWvTXX+FAgXrLXkdsJBod7I7icE+ZIS8KhdThM5JnOjviFDlkoS8Kpe8
nk+l5zXxOmAJIbs/IlCKkHcS00B9MP2p5oUmlSggotIvGs3D61SahkI0V8VxuAqbSFuv+hg86f6O
Pkaj9aqP0Wi99CN6HH73O7F0N4X7Uo3dFHoAGW7qPL5/0kR9jWkiBbGHJxy7MxXh2N2UaEfyPLlI
wd+YyNLFzcKX2L+juM10cXlyfKa7MdTcTQVze66ItYj+xl2U7Ohv3EXJcVPoorSkqZWZrouS66b4
qn8sz02h89QqmsL1KTfFvYdYG5mK83/wNd9JfM2X614s383YwdC6Okivq4OwFlZmeo8AtFWEdXUw
0afRPQrca2oHWpbl3fk5hOhrovpbbaa9kO1Goc05VMi7OFSgPohd9DCiz5N3NJVnl7hGdxCKLpxk
mMOFvL5JR01co48Q8i4OHbmeLzc6wi56lCI0ooVC3k2ETkSfRXPhAerOQt5xuwtoWZY9ZNqrXUTf
Etk9DW1ONyF3W4yvdimyV7uEvMccehBtTs5uZueKDz+9wPQU8tqco7EdHaHN6SXkHeZ6C9l2Ffet
+qznPkBp1olEJ8rVLqEyojK52iU0jmicDdRfzYWjwgChk4hOkqtdoFlN7Qft1S6ipIjiF1KbUyx0
MtHJ4bqGRUVmMJGJCIGGKEKgoaDsZpbQiJYosle7FOGEaLje+3nS/R17/zF67z9mvdwMx94/Yn10
E33witjI9fW3zStjo9b7N9GPdVM4CSldX39LPTt2HE0ty3J7+Gg3Fb+QTo+Od1PYqce4KVznHeum
0DieQFOlPMXhYye6KbQ9ZW4Kbc9J3t8axsa5KZTsZJq6LirZ+GiKS3aKm0LJTnVTKNlpbgolm0BT
PbK5oY5T43O6bnx48o/kHtl2j0bjc4aQd835TLQOPeQ+PxqfiUJux6SW5iyin6P7/GhpzhbyWppz
1Fyox5PwjY7Q0pyrCJ2B84j+iZaFeny+kNfSTBaS+1+tqaW5QMi7lXIhDs0Lsr2m8yJQkOMFmiLk
3Uq5WM2FQJcoQmfgUqJfibzTusuIWiS80meYyw+kbHOFJg50paJ427DAXKVoyJPzOpmrsTMh9qJh
NtA1ihDoWkV4wPo6Icq4SJrO62XlUNu2SJ4CuEEyWkKgG9XKwWndVEUINE3I20LThSQQn9bdBJqY
422h8vV8n2lijnfiPUPIndZRoJlEO6O5EOhmIe9YMAvk6mrurxToFiFvC926nnuDPaSamHZ8qUpI
isoZZwt5FxduJ9qWvKmFdzNkjiLcNL9D0cAOu7ubuUS/RzSPM96pCBnvArly4SJnBVHjhEelZp4i
bLS7FSHjPev5fpIjHO/uBbljJwLNF/LOU+9bz9cqdzS1h0BstPuFvI32gJAXaAG3ron6xxoo0IOK
EOghIW+3elgRAi3EYavWf6zhEZD3DEOGWUTUMboOgkCPCnm1cLEQtQCL5EWMxxQtKFnVyTwOcq+L
38Un3k8oQqAlivDO+pNCxXnRi4FPETWPCO3E06AxQgi0VMiOuZHNgZ5RZK9nCXmDdTynKJ5OgSpB
O1PpgzmDwvElWYXm+fXcC3d0K79v+4Iiez1LyA6KgS207EAqNS8R7UmeLWSvZymy17Mc5TLhzZLl
ai57PQu7lZvLXs8C5aUNzuVBRPDE+KuK5i3ZVWheU4RAr2NPy0sbksvjBdjrWYrs9SxQSSP6xskS
aKUQzTVZHg16az3fvy9pNDS3bpo8GvS2EM21SDrB79TP5TrBq0C16SW5UUv+rhAVlW8o8sgJq4mO
JbIfxD70npD9IF7xrhIalht1gtcQnZncu7H9IA5N7ysyPArDvxXZ61lCVHr3+NMHiuz1LKIJRFJU
XM8SovXl3vpepwnXsxShFm4QGpTLT32t3vp7F7NR0RzOuIloTET2epYi1MLqA6nU1BCdSDQwl0eI
stezQEsbWbLXsxThea5PFNnrWUJ28fZ6liIE+gybo0Ro9aG7upjPFc3mQF8ostezhOgbZQw085Wa
C0+Sfr1edYJ5cnlD+3d0gr9ZrzrB3+Dvdbnhhvy5K2Nb3dQefuHhWzeFruZ3bgpn7NvU1xie7JE8
pqGtY+gIfq8IHcHt3NxGxN9ofhCydcw+mqUIe8gOUO/GsjvwYyM/6Zw86Woqcv6sc/6Mv1Pp0dn/
xU3hGdCdbgpd41/dFE7Lf3NTSP27Ts2TbahYw3OjRy52KULq3URJUeHR/f0D5M5xseP+KWSPO+iC
7RFyr0sF/RN/6dQ86T6C1Ht16r34uzvF+dtNBXM7rYjti/7GFzj+if7GJxn73RRS1+nUPJkcnS4j
dbCByevXLjehIqQ2G/hJftd1w74W28DDA7gOHjZ/XBH67kkgdZUgmahB/Wk872sJIa/vniLkdTIa
bOD3IxyhZ5gKWtjYfrBHR74UJmT7MDiqpQm5i+HJO02jDfwkfz1RM5ouZC9zo4lpTNSNtr/3XEaG
kL0yidhNhOyZPd7GyyQaHRFiNxXy7ohngfLS7N31+DjqWzUjuiCi4qcf62yaK0LfqgXRrIiQMVso
ui1fYnKI2jfYmuY9s9BSyN0Rp65IriIEyiMaHhE6i62EvAuyrUEljbx7AG2IzmsQXZouojYzX2hk
Lt/nGD2JjtwHCdGOVjHc9q3aCtGONllurR0sRA1DyVDbt2pHdE5ECHSIEB3CespRrX39XD2lYh6q
CBWzgOjsBkuFEOgwIVpWR3lfsoMidBYPV1ReQoE6CtFBs4QHZ9nXyxyhCIGOrP9gmTzbdBTRWT6V
mkJFCNRJEboinYnKGmRw96FCGtouQrQK3ahUXevnWiS9325EJ0SErkiRouICCtQdhLU6bQhVOQrU
QxGOaj0V4ah2tCIE6kU0KqLcbkmB6Q2ibnMuv8yHQH0U4T5aX0UI1I9oaD1xoP6K0FkcQFTSIC/N
dkVKvl/c2QwUsj2d+/kwPUgRAhUrwvW4wYoQaIgi7ENDiQZGhCpXookDDVOEQMOJBtUvPpuf8gIt
bwiK8xRfUj6FLykj8AiSYvd3WyVHKtqypGuhGSVEG7XjUDv0zLFEQ4joEN5ROpOloK1ptCdimDdq
BY8TOoboGBt4tCIEPl4RAo9RhBfHxoJ6N7aEwCdo4kAnEh0dUbwzBSoTGkEnBSPCMdvjheYkRfN4
C44DTcqwhEAnE3WJCL3j8YoQ6BQh+saecp3jVEUIdBpoTxNLaAUnEB0eEQKdrokDnSFE31gxwgY6
U4hawUUjw5mHvdvJTASVZtm57uJAZxHlEdm5EOhsRdhC5xC1ITqWarwcpyYJlVLzKbelzgUVN7eE
QOcpwgnZ+UK0rBK5tTuZqC3RKAokt6UuIMokokL0HGmb9QtB2c0sLVgzr9BcJESBOo6wjcYURQh0
cf0H80dSU0yBLlGEQJeCxmdSIXJG2UCXKUKgyxVhWJcrFCHQlZo40FWgebw50iTQ1USxiBasoSp3
jSL0664VokBpEug6oj9THO3gKnc9aC1XpoTsQzcI0Z6WJldpblSEQFMVYR+aRvSjXZYbDnI6CDuM
Gw7yJkXxIylQOdE3EQ1fU1xoZihCB2qmIgS6mejTlEnyjThOzTqQSs0tRF+mYLeaLLdZblUUFPJ1
NEVo6WeDdjS1hMuHtytCxjmaOOMdQrYngY02V9Mtu7qbO0Goq+4F07uIPowIGSsUYTCGeYqQ8W5F
yHgPKMixhED3ClFR+QoZB5pP9HZECHSfJg50P9Er9R/kduIB0K6WlrK2Ly40CxQh0INCVIgSCfSQ
oiDYN8w8LGR3ZARaqAi71SNC1AJ0lGdwFynK40CPKkKgxZo40GOg13ItYdCWx4mejmjL9qxC8wTR
qUR2f8QjIEsUIdCTitCSP6UIgZ4mGhwRAi1VhEDPbOAHuh215kDPKkKg50D5eV47UcmH+ohQ5Z4X
knaCD00vKMLdyxeJjokIgZaByoX2cqCXFCHQy4pQ5f61gZ8Xd4Qqt1xI2mgOtIJoQMqIVhFlm1c0
caBXFfE7DOY1omMjKt4+sdC8LkQbreex9qmjN+rnKpEt9KaiOD8CspJoLBEVtUwCvSVE1bdM2om3
kdERAr2jCL2lVYoQ6F1NHGi1IuxD7ynKbLC70FQRdaCtTeWaNjJcyIemNURZtgKUSaD3hezBMOjN
19EU4ZHVtUJ0FC2Rp0I+AE3MsYRa+CHRr4mJOXYVohZ+pAgbbZ0QFaKjbLT1QlTUjvJo+waifxJT
WlhCxo1C9sA6/knarTYJ2W7U/TzI+WaieIojZKwWoiNMznB7h7aG6K/EsixLCFSrCBvtY0UI9Imi
lhzoU6KfI0KgLaDxmUIc6DNFGLz4c0WlvFt9QfRdRPdwB+lLRQj0FWhhY0toJ75WhEDfbOBXARY2
pr5vvtxT2KoIgb5VhB7fdyBcgc2Xuz7bhOypHQJ9T3SUnatMBi/ersikdisyPxD1iwhd2B9BGUII
tEMRAv20gZ9Qd4RAPytClftFyJ5DY7faSTQ6sTXNErbQr0J0YlAm572/aeJAv/O+HRFOE3cpSuGB
fXcTXRDRPXyS8YciBPpTyJ4AJnM7sYfoPCJ70oZAfynCFtoLmsrncdPknsLfitCF3YfFT00blIth
mzjQP5o40H6hgbk8iCtGcqojOiuxvKGl4ieo8xBs9OkuHq0qJLokIgQyQvbSLBqFmBCVq0LuKcQV
YQslKUKgZKJTI8K1o4QiBErRxIEaEJ2b2JlK66tCzntTFdnBsBRVnzOms0kjupTv52BZc3nooEZC
dt0jYzrRDRGhFjZWhIwZipCxiSJkzNTEGZsK2dsyyJiliTM2c4Txn3Gq21zRrTev6mRaKJrNgbJB
Y2TgaATKUYRALbE5xvjDS+cSTUlg/GcXKE+oOC/aaK1AuCXmNlprRQjURmhQXt0iqYX5inBP4SCi
SREVv3dZV9MWhNt+i/hWHQU6GNvREQK1U4Qzw0MOIArUXsgbT+xQIe/xrgKicRHhnZ7DhOS9OA7U
Qci7mXo40fjogxitqqOmRNcic4SiWRzoSKKzI0Kgo4TsN2ILFRJdExECdQJhRXOgRyhQZyE7ugAa
vi5EM6O5cKztKuTdHe4m5I0nVkR0RUKNJ9ZdyHuWvoeQXRb2oZ5Ec4i8oReOFnIPzdFJRi/QzlRv
WILeilDl+gh5W6gvUbmt5BhsjQL1I7qnvvQcqD/IrVUEGiBk1wRGKh6oCIEGCdlhHObw728UCw3I
cz/kYwZjfS1v2J+ov91CQ4TsXAg0VKgfkfz+RolQb6LeNtAwRWjJh4PGNCyi6lsUBuuCSnOMUNe8
urKuNuMIotuJuuTV9exiM44kuomoc15dx85h/P2gwIxSNL79mYXmWNC+FEtXVH3Sy5QqQsbjFCHj
aKKrI0LG44U65dXldLIZx4DWJSwh41iiG4kKiQptw3eC0FFER9lAJ2riQGWo0Y7ib1Cgk0Bbkixl
JFUUmnGKzq5a28ucrAiBxhNdS3QkrcIjbaBTHOUyIdCpRLcmsoTQTpymCIEmKMJudbrQUXQwlEBn
1M9VeaQNdCboNdM5t24RbaFXKNBEoUKaq9DWwrOE7LImcqCzheyyEOgcoiujudDwTQJVBY4o0LmK
EOg8RQh0PtFVESHQ5AMp21yALTTREQe6UMiWPv52vMBcpOjtiXcVmSmKzuBAF2PxE4NORJ1soEtA
dXWWEOhSomlE9oMIdJmQnQuBLhfyAl2hiQNdKYRVGOcpvqA8AReUOd1V/t9tuqvVUuKvULprhI4g
OiKcXty5yFyrPoh014EmBt7muh7HvIn126bE3HAglZobFWGHmipkl4V00xTh2tH0jdyxc4RANwnZ
oiJQOQ71bi4EmqFodbvfu5iZiiZwoJsVIdAsRQh0y0bulrrtjEC3Ctm5zHJqzG5ThIyzFSHj7Ypw
UXOOImS8QxNnnKsIGe8kOjki7GN3KZpQ9WEvUyFk1z0yziM6Wme8W8jLeI8iVMl7FSHQ/I38RNTE
gPb9ys72ouZ9ihDofqLe0X6BQA8oir9PgRYI2Q8i0IOKEOghou4RIdDDoI6hJQRaKNSFqIsN9IhQ
N6JuNtAioSKiIhvoUaHuRN1tLVwMmhv2oE51DxvoMU0c6HFF8a+pFXxC0erHKzqZJYrOqFrXyzxJ
1D5RZiwh0FNCVIiy7jbQ0wdSqVmqCFXuGUUI9CwoP2YJgZ4Tom8skUCVmjjQ80I9c+t69gzj31Gg
FxRlbutSaF4ElceOJjqajlPVvcwyoV60rF420EuKDAd6magZUW8i+eGrfwn1yY1+rWw5aES8Ly2+
rw20QhGq3CuKEOhVIVpWR/m1stcU4dfKXgdlJVmav+3gQvPGRn7Dz9GUqi97mTcVIdBKRdhCbx1I
pebtjTyEx5YkypjfO8z9iqrcO4oQaNVGflg3Ig70riIEWq2JA72nKP4bBariI0BE2IfWCNG6L+sV
XlL1dS/zvhBttMlHh8Euar/+LUSlL5OMazfy87tYVkdaPN8C/kCIlpXfy2b8UFHuGsr4kSL02ddt
5Ke1HeFYvB7UP5kW31MybhCiuXr2shk3Ctn6hVq4ieh7IlsL5yZ3LTSbFV3DGatBi5OpRveU3apm
I79u5SiZA9UKUQvQs8gG+liIdpiS7rad+IQozc5VUmQDfQrKSrKE08QtihDoMyFqc3p2s4E+VxT/
hAJ9sZHfa8MHy4rC8iHvdDJfgq7iuUq6hTdUfd7LfCVkF49AX2/kYV3cXLjg940QzTVZAm0VokDT
JNC3oMo4fbCim62F3xGFicq4bQsRaNtGfoPUEQJ9r4kDbReixVd0tw3fD6ARcVrRi3rYWvijkG1X
r6na0svsqJ+rUrbQT0SNornQn/1ZyC4LgX4Roq1d0dMG2qkIgX5VhIbvN7UsBPq9fq7KnjbQLkWo
crsVVa2mLfSHEFXMRUeHV3GgPxUh0B7s7o4Q6C/Q99yITu5pA+1VhEB/K0KgfUK0rLKjbaB/1FwI
tF/NhUB1iuI/UKBgk0/YQqGiKziQ2RQtnlYhAsXq53KB4kRbI0KgJEUIlKwIgRKK8FR0yiZ+M8yt
LwRqoIkDpQrZzRF8zz+tWD9XpWRMU1T+3qOdTSOi1yO6jDOmK0LGxkLUFlb2sRkzNvG4YG4uZGyi
CBkziT6MCBmbKsJl5yxFyNhME2dsrgiBWoAqhbDRsoWoxazkxp0C5ShCoJbqgwiUq+ZCoDxQVhId
RSv72sNvKyG7JhCotRA123Ruj1rYRsgLlC/k7VYHEb0WUfxr6vG1JXqVyLYAye27FJmDFU3hQO2E
bKOAQIdgCznC4bc96Kp6KjWHKsIWKlCEQIcpwqGpw4GUbQ4HbXHEgToqin9OW+gITTdf2tUcSVQZ
EQIdpQiBChUhUCchuyYQqLMibKEuihCoq5Bd0dhC3YS83apIEwfqrij+MwXqQXSHPW4v6kX70PGd
TU8hOxcCHV0/l6tyvRQhUO8DqdT0UYQt1FfI1i8E6qcI+1B/RQg0gOjCeuJAA4Xs4uO/UZUbpAj7
UDHoKvnghRxosCIEGqIIgYYSlRLZHQaBSoS8QMNAbk9DoOGKzA6+GEY0kEi2I2ccIWTnQsaRmjjj
KEXxnynjsUJ2f1z9OJ39lhINjwgZj1OEjKMVIePxQtLUcsYxipBxrCJkPEETBzpRyNutyjRxoJMU
xb+jQOOI2kXLwkY7WcgLNF4RAp1C1Nlv5UrMqULRdiw1pwn1y7UXNSnQhE084m9lvH8uXw1FoNOF
7FzYrc4QGpDLF0gR6Ez1QQSaKJvDfvAhfkL5LEXx/ZTxbKLjI5r+3u/dzTmgLVK/LuRO4CSieyKy
438JeRnPI1qfvEWabWQ8X5Ed/0sRMl6gCG3hhURVPmWbizRh/C8hu1ZRCy8GLU625Vo1pLyLuUQR
Al2qyI7/RbQ2IgS6/EAqNVcosuN/KUKgq4RsuXCyeLWQXYV2/C8hWvcV/WT8L9C6xEDqWck9xesU
jS9Y3Nlcr+hCPlm8QZEd/0sRzn6ngrqmDKJzK7mbMw3HR0d2/C9FCHQT0QsRoc9UrsiO/6UJ438p
wu2pm4meiuiEkomFZpaiG/hewS2K7PhfRC8m73Olx08rauKMsw+kUnM70RsRIeMcRTi3ugO1cH5K
cS6/QIc9ba4iO/6XJoz/pQj3TSvQEZmfYm/oz+aM80CzUy3hjtXdm/ileEd2/C8h+/A5HoC+VxEC
zd/Ep6eOEOg+0M5U+/A5At2vCG3hA4oQaIGQ94j6g4pwk/Qh0JiGlhbw7zA9vIlfvh7Dj0nnD7Y3
SRcqQqBHFOFK9KIDiAI9KkSLzxwS5ym+oDw+Pb2l7W8s3sSv68rfbZV8TIiiZMoP8D1O9FNESPeE
Jk63RIiWlSbpnlS0oCReaJ4SokKmDbZ3TJ8Wou2cJj8mulTRVu7iPkNUm4xb+Qmpf88K0Z6YkNfK
nhOiNjwhP7RZqQj72POb+FzCEQK9IGSXhUAv1s+VMyCMJ1OgZYoyCyjQS0T/jmgm/7T6y4oQ6F+b
eByQkkaWsLmWH0AUaIUiBHpFEQK9SlQdEc7tXwNtTbOEQK8T/VhPHOgNIVqFZYNsoDfr55pMGXkg
95Woko7KOdBbihDobUV45+MdITpYVvS3gVYR7YoIgd4Vsq0zdqjVoJJGlvDOx3uKEKhKEwdaQ/Qd
ES1+kfzC8PuKgoCfDFP0/pAxnc1aRbM44weKkPHD+nVfKT86/JEiZFwHwpqolI22nuiLiJBxg5B9
rxCt4MZNPEIRNocbfX+TJs64GYS3bvEbvUkFpnoTj/vkaPqaeUWmRsi+DDqHA9UqQqCPFeGtiU+I
6pJ3CiHQp0Le66dbNvFdAEcI9JmQ9wLh5wdStvlik33H2nvF9ktFeO/+KyH7ymJVVdci87WiuzjQ
N4oQaKsiXDL7Vi0egb7bxGOBOULDt00RdqvvhbwXdbYfSNnmB00c6EdFeAJrB1G7iLYsoSr3k6K5
HOhnRQj0i5B9tAYPLO1UhEC/KsIW+o0oJyIE+l3IHpRQ5XYpQqDd9aXvKYH+UIRAfyrasiSr0OzZ
xBf8xvAqzC+h4xQF+ksRAu0lOiwiBPpbEQLtU4RA/2zicSQdIdB+RXgQoU4RAgWbFXGgUBHG5zCb
eeCFnamWSg7bXWhiihAoTlQQEQIlCVGVmzbMPo+erAiBEpt5dFlHqHIpQsOpxRxu96EGitCfTVUU
YGB7osKIkDENtLTRMbR4ea+gEdHAiPBeQbqi8ipqJxorwm6VoQgZmxD1SuBVA14W/wZSppB9ExAZ
mwq5jLTRsoRofU2TjdZMiGrOZNmtmoPmNbHv8CNQCyFqhibL4zDZm3lHdoQfJ88RooavrDjMm9S1
0LQUohazhF9T39HL5BIVJSZlUFPbU/rseYr2ci1sJURtdE9pyVtv5kFiHWGjtRGio0JPOXPMB8Uz
6DjUs5/tIB2kCIHaauJABwvRqUrHvvaHu9tt5t3K0Ra+Y3UIqHdjS+VVW3uZ9ooQ6FCi/IhwDbBA
EQIdJkSndjlymtgBdFm6JQQ6fDPfNouIA3VUhEBHaOJAR27msYQvS/cCHQXKEJq57dFCU6hoKt/N
6bSZh5Z0hECdFWGUwi6oExnptArL5JfIu4KmpllCoG6KsFsVKcIVpu5E3SJCoB6aOFBP0KoGVIjJ
EuhoRevav9vJ9FJUzqeJvbE/rmpAK6dC7rf1IRpLROeqk3vbQ1Nf0L4USwjUTxEC9Rei894KOe8d
oAgt+UBFCDQIND/Fu0pbLGSvQ+FEfrCQPYeeM/OxzmYI2pz5KfZMeyqfyA8V8q6+lGzmp533yeIR
aJgiBBouJNc5v+SLYYqQcQRW4b6U7tTZkgu3I0FdU4ooUJHtBI5ShIzHauKMpUSHR4RbcMcpwhWm
0Zv5aYfJia5UiK7hdZzxeCF79wsZx6i5sFuNFbIPGiDjCURdog8i0Img/snelegyIRsb7cRJihBo
HFGHBO6IunsFJwvZSxrYaONB7oIMAp0iZC/bXMe18FQh73LSadjd3VwINEHIu855+mZ+Aj4ryevC
nqEIgc4UivqrGWYi9u2sJG/AkbOEbIcSgc4Wkg4SH37PEaIDRVlJ2HoN9ScmCdHhpGQY1cLveplz
N/MgWo4Q6DxFGMj7fNAWXlaJ9CcmK0JLfoEiBLpQyL6kjobvIqKMiBBoymYeZzklIcSBLlaEQJeg
BUhJ2MVnPbmr0FyqaHrVD73MZUL2YIhAl4P2pdgH/7GFrlCEQFcKUWz3k9dXKUKgqxWhnbgGNDvV
EgJdq4kDXSckfXIOdL2ijFnU8N2wmbt3jmby5aQbQRkyOA4CTVWEU91pm3mQo3qiQNMVocrdJOS9
ml2+2Q4dZedCSz5DEQLN1MSBblYUb00nGbMUvb3t9y7mls384yeObuW3Im5VhBF6blOEAQJmC0Un
GSXm9gOp1MwRsmcnyHiHkO1ZIeNcRWgn7hTyXqi6SxNnrADlpXkvVM1TNG7Nqk7mbkUY9uYeIfsi
NALdqwiB5m/mXzda2og+2FPexr5PEWrh/UL0wY4S6IH6ufJHhM050AJFCPQgaGFjIQ70kCIEeljR
Am4nFgqNzK3LHGkH2XhEEQItIro7eXymJQR69EAqNYsVYQs9pgiBHhcalVuXNspecHkCNKupJQRa
ookDPakI7/A9tZl/udoRWvKnFd1fldzDLAUtaW7fxcRu9YyiVL4Y8SzRWREh0HOKci+mQJWK0EF6
nuji5AXZpVQnSm078YIiBHqR6Np64kDLFMWHUKCXhI6jQ/lxNtDLoLktaa7K0nARB/qXEM3lfnZi
OdEVEaHhWyFEa8INhf6KotxzKdCrIHyj+42J14TsN2ILvU50UUQI9IYmDvSmEK0c/lVV3kIrZVn2
G80ttA+9pWhhVbyHeZtoOpH3pvI7ihBolSIEepdoXjJeEXRvKq9WhEDvES1LxluDlSNsb6hKyM6F
QGvUXAj0PghjFfAH+c3ufwvZ0ZIwfv1aoseiuVal3F1kPlD0EGf8UBEyfkT0r2SMjlA5Ms5TfEH5
5PT0XNvSr/P/bgOvF7KrGI3Ghs38y3l7mnhbcKPQ8XRQOt629JtAvRtbQuDNRO/yYD10ZBxjA1cL
jSWSH9+pUZTyAx26ajfzRTpHD3GV/BgUz7CEdJ8I0eIXjbFb8FNFCLSFaB2RLRe24GeKEOjzzfw4
zKSM0bnymzAZ5gshmsuNzPOlmguBvlJz4bdqvlZUPnx3d/MNaK3QAg60VYiK6oYt/lYoKkSJ+e5A
KjXb1LIQ6Hs1FwJtV4Tu3w8HUrb5URMH2rGZL047QqCfhOw3otH4eTNfpl+bYXdOBPpFyM6FQDsV
IdCvB1Kp+U0RAv0uZFcOAu3azHftHOFy0m5FCPSHJg70J6iwiSUE2iNkv3H6ml3dzV+K7q9K6WH2
En1AZNcEAv0t5AXaB9rTxAv0j5D9IALtV3MhUJ0iNBpBtf9BBAqruTlYmeltIVPNd+0cYXT+mKKq
J7sVmbgiBEpShEDJ1bzz7WjqVbmEo7yoyqUoyh1LgRpU829BOkKgVCHvl4kagtLkN4fQ0qdV8+0W
R8jYSBNnTFcUH0gZGxNtJ7JVbuCTXYtMBmhijh0WCxmbVPMpgSNkzBSKxh8rMU0PpFKTJSS/4tGJ
MjYjapBwv4OOjM2FbIuJhq+Fo7yopc9WcyFQjqJ4EQVqCXot17b0VTdToFwh24bP50B51Ty0pyME
aqUIgVpX8xOkr+VSx65SBhJpA8rPs3MhUL4iBDpIEdqJtkJ2WQh0cDXf1sjPs0VFoHZCdq74kRTo
EEUDn6Tdqn0138NwhECHKkKgAkUIdFg1j5iLAQLcFuogZOfK7UaBDq/msxdHCNRREarcEYoQ6EjQ
rpZeoKOIcom8QIWKRn1PW6iTIgTqLGSXhUBdqvm3ZBwhUFchex6HQN0UYQsVKUKg7gd+MMP0qOZ7
dx1b0tlLT7mO3lMTBzpayLsx0EvR+3xJtreiedyT6FPNTygvyLaEQH2r+VaEIwTqdyCVmv5CMsZa
J+o8DFCEQANBU1oIcaBBQnJBmQMVa+JAg4XsS+p4jGJINd/9mtJiENGgMP1mOpEfCipuPpBoYDiX
zzhKqvkXjYqb9yeSu4XDFOGO6HAhe8sy2McXwzRxxhHV/LB+aVZfor52o41UhIyjFKHPfqwiZCwV
8h6bOw770J4mcjlpb7zAjFb09tmPdjbHV/Nz8o7m8EAiYxQh41hF2GgngCZleJeTTlSEQGWKEOgk
RWgnxh1I2eZk0FpHHGi8IgQ6RcherVp1dkWROVXNhUCnKUKgCaC8NC/Q6ULe41ZnEP2WnJcmj5Rx
LTxTEQJNVIQtdBZoVQNLCHR2NQ/Kv6qB90jZOUL23nA8ldqJSdV8N9rRqlm7uphzhdyAvBToPNCE
Bt74tedX85MTExrYK3IINPlAKjUXKMJo6xcK2et2CHRRNf/ohSNsoSkHUra5WMi7+nKJWhbeNb9U
fSN6fJeBVKDLhbx71lcoQqArq7m37VGpuUoROkhXK0KgaxRhC12rCIGuq+YniiLiQNcj4yopPQLd
AMKDLDQXAt0oZOeazYGmqrkQaJpQdDGsxEwX8oYuvKma+6tuWRwoUV6thqXlyTH+oMMzqtWwtDPw
dx6Mt+1xK2Mzo6mKnBWxm91UwL9oMataBh3GL1rcUu0PQXyr+lLDkyuTl0vhMSztbULRha/lZraa
i7/f3K5oYj515eYIeYMs3qGWxas+MVennovS2Y8g9Z069Z2u9BiM9y43FcxttSJWoZLN08l48oXk
5f7gyncLRZ2I5eYeooeiAmLA3XsV4UcK5wvZZaFu3KdoDh9D7ie6Mbn+eYagv3lAEerGgmp+Nnpq
mndd8UFFqOwPET1I5I3w/LCQjD7JlX2hIqzxR0BbHXFlX0R0H5FU9paNCsyjQnYuw6O3LVY0mwM9
RvRERBhK+XG99XjSFRJb7wm99Z7A393WW+KmgrlUZ590U9h6T+mt9xS+2q0S+0OUIAwR5IZLXqoI
W+8ZogeSFzb2+lXPVvNlD49KzXNC3hqvBNX6V6ufr+ZHJN1I8XzhM/GCzs+T7u/I/6LO/yL+7vIv
c1PI/1K1P0j2yzr/y/jq3o292vsvIW+Q7OVEj9ePSM/5Vwh5VeIVoueSx2d6DfqrilDHXxOyq2QP
Pwf9ejWfDY7P9Kr9G0K2EFxLEm/qVcKThU3s37FKVupVshJ/d6vkLTeFVfK2m+K6FnvHTWEFrdIr
iCef55/rqq8g7wpFDfFys7rans/Wr6DEe7q4POn+juJW6eJW4e+uuGuq8WOtUtz3oyn+vaN/uykU
d60u7loUZFKGV9wPFKG4H6KBym5mNx6250dC3tFl3YFUatYLeXcmNlTzZTo3l70mB1qWZefCBZJN
iuw1OSHvEa9qopciwk+J1RC9FRHqRi2oXVP7jWhBPlaEJvETHKDbNfWO/58eSKVmiyJ7TQ60MtM2
Y/aaHNGbRHYuPAr6hZD30P+XRB8T1Xc5E1/pSvAVNrLtIKISfK0rwdeuEozJm/NW7JuoSvChd6ub
4hdYYt+6qbSbH+0c+85NBcH+XrFtbgoV5HtdQXjy31FNRQXZLuRVkB+IaiLi0iR+1FF+xJfYvyPK
Dh2FJ9dmuPr8k5tCff7ZTaGAv+gC/oKvxgCpbgD7nYpQwF95EVEBscP9pgv424EF/F0X8He1w+2K
1iAXcLebQvvwB03tidqHP3Vx/0RB9jTxGtA9Ql778JciFHevLu5efInXPvyti8uTVTl1uXH+D752
fiZfO0fZ98mfbNn/iaaoysf2uyn7WwG67HXYm6tyvFUd1FjyDn6hIvtbATV8Mc2bq9TEhLwdKU70
HpHXMiSBZjW1H8SVxmQhuf7BO1Kihi8+zmrqHUhShLyWoYGQd9RIJdqSXNjEznU2dxYbCnmNRZpQ
fXfDNKrh8y1HaCzShbz+U+MafogkL80W1f5WgJD3PGgTRbgwl0k0nMi799xUyGv9smp4JCRH8bbr
C0wzRcjYXMgL1ELI6xBmK0KgHLUsBGpJdHk0FwLlCtmMHCiRV6NqKk+6paCmtqpRNbUV/u52rNZu
CpWzjZviZxZj+W4KlfMg9TWGJydFOe1PZzrKi3asg0HuHjt2rHa6uDzpfh0KxT1EF/eQGvmdchS3
vZtCcQ91U7zJYgU0dVm6+92Nw3RxD0NB3K+Qo7gdhLzf3Ti8hsdXc4TidtTF7YgvsX9HcY/QxeXJ
3o1dcY90UyjuUW4KP5VS6KZQ3E66uJ1QkN6NvR8m7QxSP+LahWhkRChuV11cnlQ/4tpNF7dbjfyS
LIpbVBP95iwVtztNLWnufmK1h5tCcXvq4vIk34v2inu0IhS3l5D3O6q9a/g+rSPsQH2E7DpG17Kv
DtUXRfFC9dOh+rmiIlR/N4VQA9wUDh0DVahBOtQgKa4tCEIV/9dQg2v4jVNHCDUEtKlFNFeJGUp0
JZE36lyJkB1Pzr6EKuT9yNrwGr7B7paF8+tjFOHyzQgh7zdeRtbwKadbvH0JVVG89eACc6yQt/5L
1Vw4oz1OfaN9CVV9I96hOF6RfQmVaG5E9iVUIbus1jsbNzQnCNnYuBB/Yg2f5Gxq4f0yXpmQdFHx
EmoN/+zrphZyB/zIJgVmHGhijj23ty+h1vAvQk6UAcMxiPx4Ie/OwilC9vYGAp1K9FdE9iVURfYl
VEUIdLoiBDpDLd6+hKrKZV9CBU1N8wZYP6uG77E4Wl21uLM5W9FCvh51Tg0/8uYIgSYpQqBzFSHQ
eTX8SKUjBDpfEQJNJhpMZB+UQKALFCHQhZo40EWK4kcnFZgpNTy4kiNsoYuF7C2cJRzoEkUIdCla
u8Im0Y2eEnOZkPdgyeVqLgS6gmhoRHhS5kpFeDjrKtDKTPvwAgJdjU7Aykw7FwJdI2TvLcb7UqBr
1Vy4iHId0aiIHudA14N2NLVFRaAbFOHJ2xsVIdBURQg0rYafvHWEdmI6CL/gwM+acKCbFCFQeQ0/
9Ipfg3APlswQkkBFDQrMTCH7QWyhm0FuHP7FHGiWIgS6RREC3UrUKqJk/CKBkJdxdg0/7e+ep0HG
24W8O5BzFKHndof6IDLOVXMh4501fA/P7e7xzpTxLiEvY4WaC7vVvBr+UU3VTtwtZBePjPfU8AO0
VTneLdV7hbx2Yr6aC7XwPiHvHvH9Ql53+wF841z/HvECRditHqzh5zrdA1VD1uzqYh5StIgDPawI
gRYSDUhMaSG7KL8U8ogiBFpEdHxECPSoIgRarAjtxGMgV1cR6HFVoxHoCSF5Xot3qyVqrukc6Emi
E6O5UAufAk3K8NqJp4VsjcYWWirkBXpGEQI9qwhV7jnQUvntLASqFPIeQHtezYVAL6BRWNpIAvFu
9SIaGDcXqtwyRdhCLwl5Ve5lNGl4ytJVuX8JeS358hoeQ9DNhUArhLwH0F5RhCr3qvogAr0mJPfB
OdDrNTxEuaP4kRToDUUI9KaQe9qMAq0U8m56v6Xmwstjbwt5p6zvgEoaeadzqxShyr2rCFtoNdEQ
Iu8u/nuaOFAVKnmJFALPWawRsiunvGB3F/M+tqMj04OfuxPydqu1ipDxA1BGututkkvMhyiEI2T8
SEgavgGUcR02rSNstPWK0OPbAMIDtG6jbcRGw9OyrhZuUoT+xGZFqxp0KzLVitDw1RBdnMDTsq5x
ryWaGxE22seKEOgTRbil+qkiBNoCcr8agyuQnylCoM+Jrq8nDvSFIgT6UhFq4VdC7pkzCvR1DZ83
u7kQ6BtFeCpwaw2PFovfvHE9vm8V4ab3d6Di5t6haZuQd0n1e0UItF3Iu6jwA9Fl0Vx4iPNH0IJs
W6MRaIci7FY/CXlV7mc1F961+kXIOzTtJDqNDgFeB+lXIa8l/43ofCKbEbdUfxfyttAuNRcC7UbN
cY8JxftSO/GHIvuDnUK2XNiH9gh57cRfai4cmvbW8Ek7ftbFVbm/FSHQvhoepdZ9IwL9I2Tnwg8x
7BfyDk11oPI8r8oFtZZcO0GBwloeJ708z+vCGiH7QfT4YkLe81hxRWjJk2p53ZfneVsoWchryROK
UOVS1AexhRqouRAoVc2F/ldDolPq5+KMaYrQQWqkqHxN1yKTDnJrFRutsSJkzKjlA1i+n7GJkLfR
MhUhY1OQ+5klZMyq5VMR9ywcMjZThIzNNXGgForQuGcTtUx0FHr7vN3dTY4iNHwta/mNZUcIlKsI
gfIUIVArkPtZKgRqrQiB2ijC0Sq/ll/AdIRAB2niQG1BbndHw3ewkGRMpaNVO0Vo+A5RH0Sg9orQ
8B0K2mQ7bnGeigYUQboC/+823WGKkK5DLT8htqmFdHo43eFC3j7WEdQj2zurOkIRun9HKlp162Od
zVGK7ud0hULe5uqkF88vcneu5WdNesgLAgjUpZYflXKEQF2F7AkgGo1uai5sriLQlBZe96+7Jg7U
o5Z/Gt1RfAgF6km0l8guHo3G0ULuhYqgh+kF2tTCe/K2t5ArBPUk+gjZZSFQXxTCEQL1U4RA/aWo
8oQ+BxpQy9ccHCHQQCEv0CBFeMy2WFExP70+GIR3UmjdI9AQRQg0VBF2qBIhbwsNE/J2qOFEn1Eg
7yWIYxShFo4Qsk0e3r4fqQgZRwl5z3UeS/RlsjtYxjtQxlIhO9cqbjSOU4Rj8WhFyHi8IvSWxihC
xrFC3gX+ExQh0IlC0Q94Z5iyWh7C0M2FQCdp4kDjavmSmftG/MrTyYrKGlCjMV4RGo1ThGhNVMhJ
yKlESyJCo3GaIgSaoAiBTheiLTRNttAZtTyEoSPsVmeCesgHEWgi0dv1xIHOUoTBEM5WVLqdThPP
EbK/64l2YhIIb9RUyHHqXEUIdJ4iBDq/lh8mwK91TZOTkMlC9hvtiGy1fHetuHlU+gxz4YGUbS4S
8qrcFEX42aqLa/m+u+sRTi/Z1d1couge/vG0SxXZEdlQete7RKDLhbxHBK6o5dG3HNkR2UBfNvOe
qLtKyN5kx43Aq4W8YW2uERqQW7dIxiK6Vsgbuuc60LIsS+bmiiJzvaK5/EtpNyiyI7IpQqCptTzK
6LIsWwgEmgZamWkfvMR7SNMV2RHZFOG8t1yRHZFNE0ZkI1qavDbDEgLdrGjWzVTlZgnZB0Ln8NuY
tyiyI7IpwnPDt4Ew+lFlP3tRc/aBVGpur+WHkhzZEdmEvIx3EN0fzYXD71zQ8oZexjs1YUQ2RchY
QTQjouIC6v7NU4SMd4PcQ692RLZafn5sVYNoJKUSc6+QnQuB5tfyU0ZuLjsimyI7IlstD947P8US
fgbzAUV2RLZavuQfEUZkU4QRsR5SNCvxRxfzsJB9GBe/BrdQzWVHZKvll1Ad2RHZhLxa+KiaC4EW
E30SzWUHYVOEV2gfV2QHYavlB1UwQF6ljJm3RBFGi3pSyO61A5/4vbt5Ss11F+9WT9fawfa839he
WhuNv+d+5ekZRQj0LGiflMsOwqbIDsKGBsYRqtzzQnat2kHYhLxAL9byUzzuueF4Jp1xLBPynrx9
ScguHoFeVh+0g7CpuRBoea19/Nd78naF1C8v0CtC3vBXrwrZxWMLvYb90c1lB2EDuULYQdhq+RGn
KBA/XfmmkBdopaL5HOitWn68xz1nbQdhA61q4A741IV9h+ibiBBolZDXeXhXyLs+tloRekPv1fJv
IOClcNcbqiI6vZ4wCJuQnF7wlYn3hfyXUIkej+ZCxrWgqTLXAs74gSI7CBvRoxGhE/iRkHcqvE6R
HYSNW5OIUAs3KMK5/UYh7zr6Jk0YhE0RenzVigZ2/KO7qVGEjVYr5B1+P1aEQJ8Q3RYRAn0qJE+c
nsSDsClCoM9q+TeXHOHw+7kiOwibJgzCpijelgdhU/T2NmonvlZ0Dwf6RsgfhK2Wr485wm71LWir
H+i7Wv5Vv63+MyrbFKEWfi/kXaXdrsgOwibknff+qObCu1Y7avlHpNziUeV+ErIVAFXuZ0V2EDaQ
u+yME6mdtXxxxxEC/arIDsIm5FW532v50lT9sngQNpAawWK3kLdb/SHkAlHD96eaCz9UuqeWfyHQ
EX5K9i8QBu9ygfYqsoOwEZ0REQLtU2QHYRPyzpr2oxCOcKmlTpEdhO3j0/tNrScMwqYI7YRRtHpm
lyITI7ojsbCxexuOAsWFvEYhiehf0VyocslC3hZKqLnsIGxEH9mfgHeBGgjJRsMgbETZDebJ7WJc
zG+oyA7CpgmDsIFmyb0onM6nE6UTeafzjYXsSex87qZnEKUSeafzTRQhYyZoR1PvdL4pURqRfRc5
d3NSYLKE7GvArYP4TtNMzYU9rbnQWKKxNnYLorBBdrMyojJ571ToBKIT5L1TIfvB+DiqmC3VXMiY
S7Q7JbuZ/UZcMssD9ci2c9n3ThXh2YPWQicSnSjvnQqdRHRSmLueMuYrwrMHB4E6trSlx6X1tkQ/
E9m57HunmvDeKdHXROOIxoXx8/i9U0Xv/0hnv+2JtqaU5Z5MdLK9cHuoIvveqaK9sb3DzGGK7Hun
RN9GhD3tcFB5nv1G+96pIuxpRyiy750KeYGOUhQ/m987JdqSMqKVpbXcq+2kCDdJOwvZVWjfOxWy
mwNHq65EtdFc9r1TUFZrOxcCFQnZuex7p4pwbtWDaD2RF6inkNRCvHcK2tJaAp1KgXoJ2W9EletN
tCaaC1Wuj5AXqK8iBOpH9H7K4ja2Ftr3ToW8QAPUXNiHBgp5gQYRvRSRfe8UlJLvBRosZBePfWiI
kLcPDSV6hMjuabiOXqLIvndK9Dh9o+yPfPNjuJBdVryMr38JebvVCKJ7ormQcaSQt9FGgdzWRr/w
WKIHoppj3zsV8jIeJyTlOoE6uqOFbKOAjMcTLUzZ1RKNVZynogFFEHgMyZP0d9tW2pdQhbzLgico
si+hEj1N5A2ZUAZakO0NanESWsEF/oXbcULuujC/hKrmsi+hEv2Z4gjXOU9RlNrh8q7mVKKk6IO4
BniaIvsSqpBtsBHodCFvlI4zQNc1865En6nIvoRK9HfKdc3sIcK+hKrIvoSqCS+hEv2V4h5rwqFr
kpAtV3EH6m+cS7Q3mgvXAM8T8gKdrwiHrsmgeU28C7cXCHk3GC9Eudxc9iVURQg0RZF9CRWkhh25
RMgLdKki+xIq0W8pbtgRXAO8XJF9CRUUz/COxVcS/ZQS94cduUrIq3JXq7nsS6g4AsWlEHik7FrQ
vCbeFrpOyAt0vZorPooajRuIvo/mWtWQmvUbhdzAMPwSKtF22hzewDDTFOEW8HSiX4ns/mpfQhWS
zgUHKldzoRWcAdrR1DYteKtmppB0GzjQzWouBJqFomKuydRMjQsLzC1En0Y0632qcreCiptbCsbz
k2FCtKyyE207MVvNhYy3E62LCLvVHEXIeAdoSgtLyDhXETbanUTvRISNdpciZKzQxBnngRZkexnv
VrTpvG6F5h6iGREt5iET7lWEQPMVIdB9RLelzG3pBbpfyI5rhEAPEM21c02TQAuEqMFeVGa7fw8K
eYPfPERUEbXhCPSwIoy3tFDRkKff7WQeUbSIAy0SokIskkCPgq5qbQm71WIh2rSL5Dj1mCIEepzo
FiIqfYUcp54QooPStJPsoBZLQCn51LmYJp2LJ4mmEVHPa/LJNtBTiuKXUKCnhcYTjQ/nNHy80CxV
tLgqtYd5BrQu/xSiU2ygZ4mujAiBnlOEQJVCpxKdagM9T3RNRNitXlCE/uyLQqfRWj3NBlpWP1fl
qTbQS4riN1Cgl4muiz5YPuKP7uZfipZWpfUwy1G/HCHQCkU4FX6F6G2iCUQTbKBXhexcCPSamguB
Xldz4ebHG0TP6UBvCtkPItBKdI2iQFdQS/4WaEJbWquVp9iW/G1Fz3KgdxQh0Cr0ASa0pe1YOd7u
Q+8KeR301YoQ6D1FqHJVRA8S2X4QOg9rhLz+7PuKzES+GAZa1dbrs69VhD77B1j3jkzq7u7mQ0VP
ccaPhLwu7jqiq+kbvS7uekXIuEHI6+JuJOoXETJuEnJnaJRxM2jfQdKSc8Zqom71xIFqFMXHUaBa
okERYaN9LGTbiSUc6BPQ/IO808RPifoQ2bmwW21RhECfKUKgz4W8Ub++AE3Ot4SN9iXRYUTe0eor
Ia/h+5qoczRXfDQdfr8R8oYx2wpa3MYu/nEO9K2QN/bhd0QdiLwO0jYhrz/xvZDXn9gO6t/G68L+
oAgdpB+FvP7EDqJcIq8L+5MidGF/Bl3V2tJqHnbkF6IcIvcsLQXaqQiBfhXyHhX5jSiPyHv25Xch
7+mQXYoQaLf6IPoTfyhCoD81caA9QnJfnh/m+UvNlXxL1yKzlyg1+kYE+lsRAu0jyojWBK5M/APq
6J9k7CdqSuQ9TFGnCA1f8IklGdGPA4WKEMiAivnRhgrZQjEhO7YmhtuMK1rH9+WTFGEfSlbLQqAE
qF1TS9hCKUIUaLIEaqAIl8xShWjl9BwVBl35YtgnvL7a8RORPeWBpTRQaZYl3A9ppAgZ0zVxxsZE
YUR4YClD0YLt7QpNE0UYnTJTETI2JdqXcLSXM2YJjcity5HLgs0UYaM1V4RALYh2RoTdKluIFp8j
gXI0caCWihAoF4SXG3Lk5kce0ff22dAyfvbA9DCtZC5LCNRaUTLfU2wjRJtjmjwcnA/C41TT5ETq
IEUI1JaoOiI0fAcrQqB2mjjQIUSf/6/a7j26iur6A/jMySQ3CQExPBMCBkF5RbxgCITwkpcXpBIe
YhDEFwJiVFREVHygNymy0CIookWKERVRW2ytolXr28DPV621mgfVqhWsWmvRUqvmt/d375nMzvqt
/vpHy1ou1/3kPuZ7Z+7MmTOzz0ngjLWO9xMU6GhD+VOpxdeP6LOI7uA11N8QAg1Qom21eoZcJB0I
wuZbrZ2ag4g+JKJdWrXu+EoMIdAxSvQbqtIhQwcbQqBjQY90VuJASSXaR5fz+I0UaAjRe/Kskln+
vvu7JN1QQ1t5XLbjlGghirWXthS0qovQtzzq1zBDCFSmRBkLdD8x3BACjVCi7yu/UrpayoleT+CH
nK+nuiOVaA3l6xqqMBSMoUCjDN0xgAKNVpKRjbdyE3YM0bsJjCmbr7+hsYZwXX4cUWOcKt3xhhBo
vBJtmFW6J5+AQCFhxzdRKRzGmAJNImppJQ40WYlil+g4wyco0fdVMsOfOuWrpEuBMIIwEQJNIfpL
RAg01ZA3jjvDlOjtqysl4zSipjhVuh+ApvUUwhBMJzWYAlN+OK2n7HdRYDq9wRSYTsffW3r4N/Wd
+UxGZfgod/3oxzNmhI8wBNNMerSyV1hCPct8jJuFBVnZKzY2wWyl2FgPJxO9HD2LP9HNMfS9zwWT
oGk9Yxepq5Ri413NJXqKKDbey6lKsRL2eeZZG1AwSfRworhInoUS9tMM4cte0JYq3elKsUmnziDa
EpHM2qkknyizdhLdF72XFEwqxe5wW6gUXsDt0j9xjl2X/DD8O9blIrsu+eHBwrAcfnH4yFvf+/GM
JeEj/loyzg0fYV0uteuSH65PHCyMDexyHsgriK3LaqI7IkLB9vl2cflh+Hcs7gV2cS/A38PFvTB8
hMVdFj7CAl5kF/Ai89FYwIuVYgNhXNLA1+S8+GASy5XkWRgl8VKiDfSbjQ1osEIptjVcZp4lPWlE
tyWWdYuV9FyuFLvf8QqirYkdeiObDOdmSHrSLKEnTUkbimMO7++uJtqcCO81nTaQ2lXXgPrKZbqA
H3HXdFWH9j0k3bX697BdT+lWEz0RkYzt1pYq3fVKdIDYOFMaWWkQOvI26uGtxhC6nGqJ3olIutWI
9rQSutUMBVO5Ww2E7lGiSj5eryV6KSIvxd1qhtZzm2SdIWS80b6QM97UlirdjwxJtxp+4iGhZXyz
IazBDYakW41oW2LUYbGMtyjJgNs4hN9qKG8nrcFNSjJ492YOdJsh6VYDpdoLoal/uyHpViNaGpF0
qxlCoC0NfOtAqj0tRN0saQbf2SB3EwhJt5oldKs1yH0CQsFM7lZTklPJvXvuGeruMrSNA9WBlneI
de7erRReT+BuNaXY9YR7lGLnLvc28OX+kKRbrUHuABBCq3GHIelWU4qdXe4kOiMitBofMJS6n84u
HwQFWpa5lQM9ZEi61ZRiZ5c/a+BpjIJ4peYupdiN4A+DkofHKit+bghtkl8Ykm41S+hWI5rT+kKe
neJRUN9Och9w7YFbytxjhrZwoN2GpFvNEAI9YUi61UA4BdmoJypPGkKgpxp4nJqQEOhpQ9KtZgnd
aqDuXWI3ST/bwGUnIU3k8ZafM4QTlecbeBqjkKRbzRB+Qy8akm41olERSbeaUqwCq94QOj73NEiJ
euwOk70NUtsev8esgesTQ5JuNaXYzWmvEj0b1QujIfOaUlheSxlfBz2TH7vp5A2lsJAqO+V+Q/Qo
UayP401D0q2m1HrTSeItezTnh0m9KQVH89/Zo/nvGuKD4L0dPnLrJz2e8fuG+LB379ijOT98LBGO
foSj+btKsaN5A9H9EeFo3miJm45NSrFja3MD33YXEX/j+0DRUIG9qOn4B6VY0/G9Bm6PhbSFD6fv
K4X3LdGh5o9Ksd/9B0Q77Tf+oVLsvqWPlGK3+fyJaAdRdBtZR/exUmyr2o8DZbgQCHRAKbxvqWd/
94mh55bQz+TPRH+Iile3cqBPDWET+gwUTo+BTehzom8SZsaMvxD1zg4Jgb4whEB/NYQ985dEZdnh
hoRAf1OK7cgOmmfhN/GVoXru9/taSb4JBPo7Ub9sc2fZISX5VtHV/A+l2LDk3yjJtJAI9E+iI7Mx
FGWVDir2LQjDgxLhd/+dksxqikDfExW3EgdqMYSJwr1GITqnKZ/kb96/Pel8oqMiws2MzhACZSjJ
pOPY5AJDCJRpCIGyiNplFykhUEJJ5ltGB0W20nhaVL3pOUfpeCK9LT1XaRy9UCcxbmcIt97nEXkR
1f/whSGuvaGb6g9WuA6GkPEwQ8jYEYQpq8N5jQ8nOpAICVMJ5SvJLdvI2ElJ7/XmjJ2VYpPCdSFq
JoqdYnZV0uFpeSz5bkq0AdTpIEPdG7lTAYPr1qUkUIEhBCok+ohItkIcfXooxU4qigxhpfXURY3d
B93LEA6nRxhCoGIleq86PcXsrUTLtTElgY5UokCrT/ALlxxMuj5KMlT9Bg7U1xACHaUkGyZacEe3
fqvhVtjPEM4j+uuXE9sKByjRGiqeIDc9D2zkTr6QEGiQJQ5UYgiD4x9D9JvW9+rfOekGg2bzNpE/
3l/LM8oeawiBkoYQaIgSbV/5uskNJXohehbW0HGGEKiU6LVW4kDDlGLzn5e1PqtEJ3QfbgiBRjTy
MR3DaBdM8Ocv6Zt05YZqeUbZkUR7ExiMu0BrCSoMoSNzVBuiQKP1E4UQaIwhBBqrJJ+IQOOIfhs9
C4GOt8SBxivRZlI8SXZ8Ewxt+nhi0k00hBnqJzVyiyQkBJpsCIFOaEM8GJkhGYyM6MGIZDAyQ15H
7nJToqXfOFH6NqeBdivJYGSNfEodEQYjM4RpgKcbSu2gHV+lIWScAcIOeaOutJlE9xDRcm2cpIOR
KdGPr04LQGYbksHIlGSXhv3EHFA4ljYCnWJI+taIbkyEQ15L35ohTE14KhY1JFzpnQcKbxLHhO7z
lcK+NR6MTEl2aWhPLGjk3qTwvnHpW1OKNZDOAL3SMdZAOlMpNuLLWUrU/qrTm57PVopdH1jY+qzV
OsvdOSDcGk20NeerpFtkaD1PHrnYkAxGZijwqD1xrhJ9YkqvUi0lqpHBtVKVsuM7j+g6GZWrXE9d
q5XonLd8pr+KR6g+3xBW2gVK0s0v9Zcg3EBdoqfny5SkTx99KhcZKhp4VNJdrEQLUTxDbia7RIkW
tUCvgSwH5XF/d8F0/xDqL5ViPewrDCHjZUq00vJ1pa1s5HE7QkJ/w+WNfPqE0to8rdq5whLqLw3h
5G+Vofn3B0l3FTejItrMga4GYXiqPG0EXmMIO/drDUn9pSFshdc18jhzIUn9JQiz4eTpbJhpos4R
Sf2lJdRfGkKgHxqav4cCrSH6OguDTiSmUSOQAt1gSOovleiFCVr6Eu4ba+TJnCLi3eO6NsT1l4ak
/rKRRwsLSeovDaHXa30jj4gWktRfWkL9JQiBEppxo6HnDtCpyC2GbuGMtxIdE5HUXxrCfJK3GZL6
S0NSf9nIY4qFJPWXStK+x67jx0pSui31l40yGBm9sFoD3WkIgbYSDY1o4h5qM/1ESd5rC9dLbGvk
yYJDkvpLEMqt6/T0vE4pVhNyt1Ls3Gq7eRYC3WMIJ4v3NvIE5RjcPpxI9z4liS31l4bQCLyf6DA5
IaZGYMcdFGinEh0oqidToEMV7gHQko6ggB9xZ/OCDu0LtRiTJI/+LpMX4UTrIUNSjGlIijGVZIoj
KcYEPdienrV6kpw5PqwUOxb/vJGr/yJCMaYhHIsfMdSB0/0SlFLazLPqPqokZ0JSjNn6rNREqUna
3fqslJbLPm4Ie8EnQB/mCSHQrwxJMaYStfXK9UTrKUsoxiT6KCukIJcC/RqEcxyirtzEfUaJWpcl
x/vr+Fj8rCEpxjSEs6rnDUkxJqh/rhACvWgIgV5SkoXAHuJlJfq+SnQN1SvJWYIUYyrRZlQ+WSZy
3kv0OZHM4+X15Y4yJZlEe+Ke5aXuFUNoxr9qSIoxifbLe6VSsqd/3ZAUYxqSYkxDUoxpCcWYIBSv
prSH6S2iT4hkxmwpxjSE39jbhmqXbB/qfm9oDQd6R4m+nI16ovWuIay0BkNSjGkIgZoMSTGmIdy3
tE9JGpxSjGkJxZiNPC5+SGjGv29oNjdx/wj6IlcITdwPDEkxpiEcpz5qQ1yMaQhb4ccgtHrDadX2
G8Kx+IAhKca0hGJMJfruS3QNfQpa3kFowZrSpPtMidZ2cYrW0JcV7nNDUoxpCIG+aENcjAlCR2Sx
Tvj+pSEpxlSiLTp/ijT/DhqSYkwlakHna/3v16BPOwmh7//vStLy2nKAGheHlGQi5w184P2HErX1
8vRixjeGcMfIP0FoEeZpE/dbJXkvBPrOkBRjKtHRLKGjubYYkmLMJiaMQpXQPkBfid4roW12Z2jf
lMykyzCEmakDpVigzCaetKm4SKgjXxDMUqJWb57eTZFQouZynl4QzFaidna+FkfkKJ1MdLK0JHKb
uBR+F983nj/H97iYqZ0hKcZUOoXeXu9AbW8oWEAZOxgqXEoZDwM18T3oeXP8e+s7j3AdDUkxpqEv
eaXlK9Gi5ukdqJ2UZtNyzZaV1rmJ59fB/aD5egdqF0PoYepqCIG6WeJA3Ym2t76QrxEWgHA3K1Ft
zj1DXaEhBOphCIGKiLZFhKu4PdtSpesFeqM4DEQr7QhDCFTcxPNXv8E3y27UldZbKXbR80hLHKgP
qLS3XvTkq7h9DaWW0pnjUYYQ6GilWLlsP0MI1J/ofPrE2EXPAYYQaGATj00SEgINUoqNflViCIGO
AWWHxIEGN/EI5SGh5ujYJp5gK6TJi0vLXNLQdg40xBACDQWFN/Ei0HFKsSKq0iaeYj4kBBpmCIHK
DKHFNxwU3uCKQCOUYjVH5eZZuEd4pKEX1tAZR4UhBBoF2tUzVkQ1mmgtUXTJNuXGKMVuqR1rCL+h
ceaFCHQ8aJo+C9cKxivFKrQnWOJAE5ViA/5NMoRu58mg/UXhMNYU6ARDCJRq4mnh97cOV5hyU5Ri
ZfVTiZZFz3J8/+yJoHR8UMNpSrEOmR8Ywko7ieg0othKm64Uu+hZCQoHSMQojTOaeLKEkJBxpiFk
nEV0dUTIONsQMp4MMsNYzzGEQKcYQqAqpVgP01yl2Eo7FTQiXmw5D4s6ons4gzjtJ+YryT0OTWtf
HOJOU5LdEAItMIRAp4Pe41r1qjkS6Iy2VOnOJFpCRAeKci2OOIvo4ogQ6GxDOO9YCEKZW7kWR5xj
iQMtUqJPLNCqsMVESyNqWNol6ZYY2lHffYQ7F7RGCYGWGkKg89pSpasmmhsRAp1vCOe9F4D6KmHH
d6EhBFpmiQNdpCQZcay92NArXEN/CWh+fhW1yav8n9YXjnDLlaRsC4EuNYRe2hVKUkODQJcRlct7
paok0ErQjq5CCHQ50VQieq+SudLiu0KJ3qtES3SutMSBVhkKllKgq4imUNtEqOsDw5LuakOPcqBr
DCHQtYZwkrGaaFJECHQdqLpYCIGuN4RNLm0IZ4Y1hhColmhcK3GgH4LW9YkFWmNo4gO3lrkbsKgh
PcKB1irNo9/QPN9bxp1hRCuJ5hPNl4zrDGEoohsNIeNNSqcRnSYZf4S9XEhYaeuVFhAtkP3EzUQb
svr3OZ3aAFootsESZ9yoJLVjwXWU8RZDk7ny7VZDT3LGTYYQ6DZDqHzbbAiBbjeEQHcYwkr7MeiF
I4UQaItSrPLtTkscaKtSWPlGgX6iJDVtc26gFt82Jalp213fdYS7i+jaiBCoTklqxw5xh8vdhhBo
u5IUiuHwew/oWy3IQqB7lbQ8igPd15a6ux1K0pmPQPcr0YlUuY5juBO0uTedgZVM8Qv30JnhA4a2
83nHg4YQ6CFDHl8r+KkSvX1xOHUn0XJquAkh0C5DMnWnEr1X8RT5Wf1cSc4fEegXSnISi0CPgFb2
orPfEr334JdKdHJdMsmfs58CPaoknTcYQe4xQwi02xD2E4+DqnrIeyHQE008t1BVD3kWNrlfGUKg
J3EAi4gDPaUk74VAT4Pe6qbEgX5tKCikQM8Qnc0z/SDjpjV9k+5ZQ7gu/5whBHoeNLGrEAK9QLQg
IgR6UYk+sUo7LV8yhEAvt75wo05nWw/qzuNR1mmgPZY40F4l6Z/wunP/lyFkfMXQqWu2J92roDWd
hNZxxtcMIePrhpDxDUPI+BtD2ArfJKqg42Osw+W3htCD9JYhZPydUmylvW0IW+HvQWuEAn7EHcoz
+O7lohStwXf077SBFE/013K6d5Xk6j7SNRB1jehrTteoJJffka5J6Xg6FdZOzWainhHJ7JuG0Fr6
gyGke4/o2FbidO+DDh1On5invbR/xDEvpO79g6T7wFCaLzB+qCTvhUAfGXLcWvoTKMkvLNBAHxvC
Jrmf6PSIcJw6gJUaEnYanyjF7qz4MyjoqB23HOhTQ1hdnynR6kpN8t+6j3Yan5tnoUv2L0r09iW6
hr5AQ2XUYXJTQza3lv5qCIG+NIRAfzOEIZQOGsJv7CtQxw70ias10NeWONDfiUZGhAsDh5Ro6et0
wvd/EJVmhde01/OdFd+APsyLzY/+T6ITo1mkMZ7Nt0qx4Wm/M4RA3xPNiQhrqAUUDXPHgbzmM8cu
zDLj4/mWOJBTCi/SU6AMQwgUgMK5DdfXf1LhMpVi42hmKUlGXLpJmGchUHYzL2r4LATKMYTfUK4h
15k7w4gWRYStME8pdiNCe0ucsYNSbFL7w5p5hx2+FzJ2NLSOMx7ezGcvRfGVlm8IGTuBdscnte/c
zKd24W1gyNjFEDJ2beYRa8MXorXUzZCMTGYJI5M189iqrbeUUaBCJZ2QdT+1lnoQvZgI6SYOVGRI
RiYzhK2wF9EjEcnIZIYQqFgpNkpjb0MyMhnRAwmzhvpYwshkSroQhTwymXnWC/0ODndHE90VEdZQ
P0MyMpkhrKEBhmRkMqXYGhpkniUjkzVzeUS40rCGjlGKBRqsFPtZHaukw8DyJpc074VNboiSbJhr
OdBQQzIymSEceEvNQsjIZIZwaCozJCOTEV0ZEU4TRxiSkcmUYnVlI4lWtN7FyGuowtC0PcPK3Cgl
WY9rONDoZp56xmxyYwwh0NhmnuvVbHLjQLOjQDwymSEEGm8Im9wEQzIyWTMPRTo7/huaBPoiVwPx
oWmyUmwNnYAtOqRaDpQyJCOTNXPBaUi4dDPVEu+/TmzzLB6ZTCmW8QfNXA0VkoxMpqS/NIxMZp4l
I5Mpxe7AmmEIg5/ONO/VtDNziJulJGsb19tmmxfKYGTNfA/5F7nh7VaUcY5S7I7uU5TCgQ9oK6xS
ilV8zQXtbkenUFUny/W2U5VOoSOydifNU6IztBIdam2+oeAsWmmnGWpaOyzpFoA+zBNCQfPpSvT2
BafoYGSGcIn3TCW9AoPByIg2Ecl1GgQ6W2kWvVCv5ixUkhpnbIXnKMnNTzIYmSUMRmYIxUNLQJit
raDSbzo3M+nONVTHAx8sNSSDkSlp/S+3+KrbUqU7HzQ/XwiBLiC6PiIZjMwQznuXKdEnVoWDkVnC
YGQgzJ5YpdVQlxBVRZTKOXmoW25oGwe61JAMRkY0l0jGaPier0VdBkK1dziSw0pDUkJpCA2kKwwh
0JVK9ImrNdCqZp59DFX7q/VKx1VKtPmu1i7Zq4mmJzCMXfUsKTK8xhDG2ri2mSurtnSfTRQORgaq
6iGEDr/rlGQQK6maNCSDkRmSwciaeZq3kNDiqwWFA2JJ1aQSfeJqvbi2hujEiBDoBiVa+tWz/GkH
DibdWiWJLVWToHSREAZSWmdIBiNTkq8Qt7vcZEiqJonKI5KqSRCq0Dfq1ZybiaYQya4Dh98NhqRq
EhkjQtWkIdzFeCsIl2WZBtBWuMnQTi6rv00pdnlqc7NMzCSEo9XtSrHLU3eA1heGl6foZ/VjJbl0
I4ORGUIT9k6l2IWBrUQTWglVk0Rj7PRN2wzhaHWXoe2846szJFWTSuFkg1w1CTKzot2jFFYmU6B7
lWKVyfcpxSqTd5hnSdUkaFWXWANpp1KsPfGAodTivkPcg818K+UqnYhjG/ePPaQkLSupmgQ9ky+z
bmBP/jOlcbRqdYaFXUpj6dcxVgI9bEiqJpXG0G5ojA5GBgo6CknVpNJootFaNak0is5rR/nB515/
96hSBVGFf8vk2UPdY5b4Dr/doKI8IamabOaZforyRhKNlEBPKJUTlWvVpNIIohES6Eml4T1ayodr
1STojBwhBHpaqYx27mVaNWkJVZNKw4iG+cHbFOhZQxg5/TlQaTa9MFXmX13/pwr3vCGpmlSShbia
71B+UYmWvmSEtCdeakOU8WVQdUIIGesNIeMeolciQofLXkPee94u9z+GEPsV0PYsWq6C4RL7VUPB
Por9mqGmRZ2S7nUlylhQ5l9cv6/CvWEIsX8DWpkphDtg3jSEjL81hIxvGULG3zXz7QghocvsbVBn
JQT6vRKtocQwCfSOIazHdw1tmZyRdA2GquvfqXCNoF1BKVGpBGoyhCNys9JxPVrccRJonyEE+oMh
BHoPNC0kDvQ+0bfBtGAo0VAJ9EeirMyIONAHhoK9FOhDQ72OpkAfER2eGb79ovq3KtyfDCHQx4bQ
ZtrflirdAUMI9IkhBPqzIbRqPzWEQJ8RdWglDvQ5UZeIgtco0F9A4fc1nwN9QZSMCIH+agiBvjSE
QH9rS5XuINHwzM6ZsUBfGUKgrw314kB/53ZORAh0yBIH+gdoZWYs0DeGEOifRNdGhEDfEj0XEQJ9
ZwiBvgeNy4oFajGEQN6+M8fujQiBfEM4+3WGECiD6M3M7UIBP+IO5VP4DmWkC/Tvse0v0xDSZREd
imghp0sYQrpsom5ZISFdTluqdLlEw1qpnnZm7QwhcJ4hBG6/j7tRsxNC2DF2CKkw+o0dZokzdiT6
IDskZDw8pNaM+USdcsJnIWOnkFozdiaqIBpCNER2Gl1CKmRCxq6GEKgb0Vk51UoI1N0Q1mAB0YqI
EKjQEgfqYSh4iQIVGSriQD2JLiGKBeplCIGOMISVVkx0cZwqXW9DCHSkIQTqQ3RhRFhDfQ0h0FGW
ONDRoDcSxxVGv7F+REsjwhrqbwiBBhCdFxECDTSEQINApdkRVboSokURIdAxhhBosCHsBY8lWhwR
AiWVYoGGEC2JCJvcUEMLONBxhhCo1LwXAg0zhEBl5oUINNwQAo0g2hIRApUbwiY30hACVRDd2JqR
A40yhDU02hDW0BiiayM6iwONNYRA4wwh0PFtqdKNJ7rSrqEJhhBoIihc21hDk4guiwiBJlviQCcY
QqCUIQSaAqpOxAJNNeS9QvuvEw0h4zT7LM74A6KVcap0JxlCxumGkLGSaFlEyDjDEDLOtMQZZ4Gy
4xlnYzMJCVvhyYaQcY4hBDrFEAJVEZ0Rp0o31xACnUp0dkQINE+plKhUAs03hECnWeJAC4jmRhS8
SYFON7SJm39nEM2PaFH92xXuTEMYpuIsQwh0ttIwomESaKEhnNufYwiBFhnCecliQwi0hGhOK3Gg
c5WGF7bkaQN9qaG9Z1MD/TylskJuGy/i9mx167OoGY9A5xPNkmcVl0mgC9pSpbuQ6CRZiAINtMwQ
Al2kRF9OQal0yFxsCIEuIUrJqi3XTW65Ej0rpWvoUkNrJx+XdCtATZlCSzjQZUq0EOXDZJNbSdQ/
IhS7XW4Iga4whEBXEg3K6ayEHqZVhtBldhXR4Dh1d1db4kDXGMIZx7VER+XsCoTyXnp+iFttaBkH
uk6JvvuNegp1vRKttI3DpcssrTSisKVOTxNriHrl7M8oJyr3e6ymQLWGEOiHRH2JRhKNlEBrlCqI
KiTQDZY40FpQOmNUYcuuUb53kKepxEKkM0YTjfaDbyjjOqUxRGP8t/psH+puNLSSM96kNJYC6SyY
P9JA4wpbVo/zs/kOrPVKxxe2VGtB1c2GsJ/YYAhb4UZD6DK7BdQ5UwgZbyXqRkSfWK1TXm4yhHE3
bjO0afHCpNusREtfNVYC3Q5qyqSMqTES6A5DuAT3YyX6vlKjJdAWQwh0pyEE2krUPnovnCb+xBAC
bbPEge4iyoreK/ia1lCdofqPz066u0HjsoRWcKDthhDoHkPoMrvXEALdt4/nRQkJP6sdoO1ZsUD3
W+JAOw0h0ANKFGi1BnpwH88EFBI2uYdAiE300seXlLqfGsIa+pkSrbQ63eR2xSmTH/E5jhd4LS1Z
/Kj8oitfamnzz/s3/vHzfK8xr3+uvkJe2dKSGb5LVvhh/4m394P/+NvHX+Gy/ytvv9CTV2Tk/he+
nPfy7pKl9oK8tm+PYmFsq9g6cS8MOm1Tnve+1+H7oNYLarOC2gyXDlw6kZXOzkrnuHSuS7dz6TyX
bu/SLiM303fZv6TlXDhkvPfUqzp8V5uljZban+J573jtvwtqXVFtZnFtTse059K+Sx/m0llZLtf3
2vkuz3ftfdfB93L4fQ/9dMy/uYb+/3/v4DWrbth098NP+097O1oy+RNYkxl+S8tUz1vnVfDCBTUZ
QU1mUJMV1CSCmi5BTfegpjCo6RHUFAU1PYOa4qCmT1DbN6g9Kqg5OqjpF9T0D2qSQW1ZUCPfUAdE
6ujSg9z1Je76Y9z1w1263KVHurTnO993AX1tvt/O9/J8j9Ie7vv5vuvEt7a4rr7r5rsC3/Xy/SN8
v7fvjvS9Ab430HeDfTo3pZMMbi1Sk2sYT9LncjnEY873EhN874l+vnfej7bh6+m7op03q7kH3P4r
Pun9YePx9YV/C/9/8lrZ5KZ7930RPjvfD7/sCZ7v5Q6i9Vjo5Xv9tuAnnclblZd7Ij3PW44vr5a+
vIC/udrsoDYnqM0Nauk76xXUHIFvTr6wAUEtfWEnBbUXFCeWBbXLA3dp4FYE7rLArQzSlwfpVUHt
VUHt5qB2R1Dzy2DNr4I132PLk6+2s0t3cemuLt3NpQtcutCle7v0kS7dx6X7uvRRLj3QpUtc+hiX
Ptalp7t0pUvPdelTXXqeSy916fNcutqlL3LptEvXuHStS9/m0ne69FaXvt+ld7r0wy79c5f+hUs/
4tJPufTTLv0CrzrP971M38vi0V1opTlZad1918N3Rb43yPcG+16574/0/Rk8aY87zXcLfHeO7xb5
brHvlvjuXN+d77uLff8S37/CuSudu8F3a53b5Pu7fe9J33vGd8/67jnfPe+7l31X77s9vtvru3a8
nvUndg3/dwGtQ/7ZhbuE0P+P//CPnzv3wsVn/jt7pX/1L9xeWh/f5tm/+//yfeynyHMHJ7bktLT0
dy0tjwTrcpsyz/dy/NxBqTPo77zT6tGz9TXXIBBtbYV4o4EtLfNa/uO7ivy2u4ppnnekl/MddoqZ
QS3ts+gXl+F7AW8NLkG7rXAvyD8h+sUU0h7l6Kb/BVBLBwipDEp8X3oAAJRYAQBQSwECFAAUAAgI
CACQkytXqQxKfF96AACUWAEAGAAAAAAAAAAAAAAAAAAAAAAAMTE5OTg5NTcwMDdfQUNUSVZJVFku
Zml0UEsFBgAAAAABAAEARgAAAKV6AAAAAA==
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 8051f899de562845-DFW
Connection:
- keep-alive
Content-Type:
- application/x-zip-compressed
Date:
- Mon, 11 Sep 2023 18:28:32 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=UasM5X17vbczyPuHS8ZkKgf9dhIaPVvfztkmUlVZCFpeDvl304Gx8EwjapAM4eMIjt70PTgSNnNAMmXtzkVKh0BVUVAUf9X3p6ro5v%2FIN2mLHmxnv3AU27akiMmY8QOJmwHsSrIsqQ%3D%3D"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Transfer-Encoding:
- chunked
alt-svc:
- h3=":443"; ma=86400
cache-control:
- no-cache, no-store, private
content-disposition:
- attachment; filename="11998957007.zip"
pragma:
- no-cache
set-cookie:
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
Secure
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
status:
code: 200
message: OK
version: 1

View File

@@ -1,105 +0,0 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.31.0
method: GET
uri: https://thegarth.s3.amazonaws.com/oauth_consumer.json
response:
body:
string: '{"consumer_key": "SANITIZED", "consumer_secret": "SANITIZED"}'
headers:
Accept-Ranges:
- bytes
Content-Length:
- '124'
Content-Type:
- application/json
Date:
- Fri, 04 Aug 2023 03:43:24 GMT
ETag:
- '"20240b1013cb35419bb5b2cff1407a4e"'
Last-Modified:
- Thu, 03 Aug 2023 00:16:11 GMT
Server:
- AmazonS3
x-amz-id-2:
- V8hHVVhXCEX7RD7Vzw8IsKS//xFr7co0468z4G834xsWIJ46GpXAwZKETm68Odczy470cauMZXo=
x-amz-request-id:
- Z03APPY9GXZFWZ69
x-amz-server-side-encryption:
- AES256
status:
code: 200
message: OK
- request:
body: ''
headers:
Accept:
- !!binary |
Ki8q
Accept-Encoding:
- !!binary |
Z3ppcCwgZGVmbGF0ZQ==
Authorization:
- Bearer SANITIZED
Connection:
- !!binary |
a2VlcC1hbGl2ZQ==
Content-Length:
- !!binary |
MA==
Content-Type:
- !!binary |
YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVk
User-Agent:
- !!binary |
Y29tLmdhcm1pbi5hbmRyb2lkLmFwcHMuY29ubmVjdG1vYmlsZQ==
method: POST
uri: https://connectapi.garmin.com/oauth-service/oauth/exchange/user/2.0
response:
body:
string: '{"scope": "COMMUNITY_COURSE_READ GARMINPAY_WRITE GOLF_API_READ ATP_READ
GHS_SAMD GHS_UPLOAD INSIGHTS_READ COMMUNITY_COURSE_WRITE CONNECT_WRITE GCOFFER_WRITE
GARMINPAY_READ DT_CLIENT_ANALYTICS_WRITE GOLF_API_WRITE INSIGHTS_WRITE PRODUCT_SEARCH_READ
GCOFFER_READ CONNECT_READ ATP_WRITE", "jti": "SANITIZED", "access_token":
"SANITIZED", "token_type": "Bearer", "refresh_token": "SANITIZED", "expires_in":
107182, "refresh_token_expires_in": 2591999}'
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 7f13cbbc2a754790-DFW
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Fri, 04 Aug 2023 03:43:23 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=T5EHGPEATgD5SbyAMCZh1mKSEJkUest3sa7l%2FTpQ6dZl3uv3K%2BW7Ng20XTseNh3KPdqYzHdkCCB5d4npBML1ZgAAmVUYdkrYiM2uJhmn7WfvSdrIyme0uCf9p5t7RY6%2BRUxNYfhL8Q%3D%3D"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
Transfer-Encoding:
- chunked
alt-svc:
- h3=":443"; ma=86400
cache-control:
- no-cache, no-store, private
pragma:
- no-cache
status:
code: 200
message: OK
version: 1

File diff suppressed because it is too large Load Diff

View File

@@ -1,601 +0,0 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
method: GET
uri: https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
response:
body:
string: "<html>\n\t<head>\n\t <title>GAuth Embedded Version</title>\n\t <meta
http-equiv=\"X-UA-Compatible\" content=\"IE=edge;\" />\n\t <style type=\"text/css\">\n\t
\ \t#gauth-widget {border: none !important;}\n\t </style>\n\t</head>\n\t<body>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/jquery/3.1.1/jquery.min.js?20210319\"></script>\n\n<div>\n\t<pre>\n\t<span>ERROR:
clientId parameter must be specified!!!</span>\n\n\t<span >Usage: https://sso.garmin.com/sso/embed?clientId=&lt;clientId&gt;&amp;locale=&lt;locale&gt;...</span>\n\n\tRequest
parameter configuration options:\n\n\tNAME REQ VALUES
\ DESCRIPTION\n\t------------------
\ --- -------------------------------------------------------
\ ---------------------------------------------------------------------------------------------------\n\tclientId
\ Yes \"MY_GARMIN\"/\"BUY_GARMIN\"/\"FLY_GARMIN\"/ Client
identifier for your web application\n\t \"RMA\"/\"GarminConnect\"/\"OpenCaching\"/etc\n\tlocale
\ Yes \"en\", \"bg\", \"cs\", \"da\", \"de\", \"es\",
\"el\", \"fr\", \"hr\", User's current locale, to display the GAuth login
widget internationalized properly.\n\t \"in\",
\"it\", \"iw\", \"hu\", \"ms\", \"nb\", \"nl\", \"no\", \"pl\", (All the
currently supported locales are listed in the Values section.)\n\t \"pt\",
\"pt_BR\", \"ru\", \"sk\", \"sl\", \"fi\", \"sv\", \"tr\",\n\t \"uk\",
\"th\", \"ja\", \"ko\", \"zh_TW\", \"zh\", \"vi_VN\"\n\tcssUrl No
\ Absolute URL to custom CSS file. Use custom CSS
styling for the GAuth login widget.\n\treauth No
\ true/false (Default value is false) Specify true if
you want to ensure that the GAuth login widget shows up,\n\t even
if the SSO infrastructure remembers the user and would immediately log them
in.\n\t This
is useful if you know a user is logged on, but want a different user to be
allowed to logon.\n\tinitialFocus No true/false (Default
value is true) If you don't want the GAuth login widget
to autofocus in it's \"Email or Username\" field upon initial loading,\n\t
\ then
specify this option and set it to false.\n\trememberMeShown No
\ true/false (Default value is false) Whether the \"Remember
Me\" check box is shown in the GAuth login widget.\n\trememberMeChecked No
\ true/false (Default value is false) Whether the \"Remember
Me\" check box feature is checked by default.\n\tcreateAccountShown No
\ true/false (Default value is true) Whether the \"Don't
have an account? Create One\" link is shown in the GAuth login widget.\n\tsocialEnabled
\ No true/false (Default value is false) If
set to false, do not show any social sign in elements or allow social sign
ins.\n\tlockToEmailAddress No Email address to pre-load and
lock. If specified, the specified email address will
be pre-loaded in the main \"Email\" field in the SSO login form,\n\t as
well as in in the \"Email Address\" field in the \"Forgot Password?\" password
reset form,\n\t and
both fields will be disabled so they can't be changed.\n\t (If
for some reason you want to force re-authentications for a known customer
account, you can make use of this option.)\n\topenCreateAccount No
\ true/false (Default value is false) If set to true,
immediately display the the account creation screen.\n\tdisplayNameShown No
\ true/false (Default value is false) If set to true,
show the \"Display Name\" field on the account creation screen, to allow the
user\n\t to
set their central MyGarmin display name upon account creation.\n\tglobalOptInShown
\ No true/false (Default value is false) Whether
the \"Global Opt-In\" check box is shown on the create account & create social
account screens.\n\t If
set to true these screens will show a \"Sign Up For Email\" check box with
accompanying text\n\t \"I
would also like to receive email about promotions and new products.\"\n\t
\ If
checked, the Customer 2.0 account that is created will have it's global opt-in
flag set to true,\n\t and
Garmin email communications will be allowed.\n\tglobalOptInChecked No
\ true/false (Default value is false) Whether the \"Global
Opt-In\" check box is checked by default.\n\tconsumeServiceTicket No
\ true/false (Default value is true) IF you don't specify
a redirectAfterAccountLoginUrl AND you set this to false, the GAuth login
widget\n\t will
NOT consume the service ticket assigned and will not seamlessly log you into
your webapp.\n\t It
will send a SUCCESS JavaScript event with the service ticket and service url
you can take\n\t and
explicitly validate against the SSO infrastructure yourself.\n\t (By
using casClient's SingleSignOnUtils.authenticateServiceTicket() utility method,\n\t
\ or
calling web service customerWebServices_v1.2 AccountManagementService.authenticateServiceTicket().)\n\tmobile
\ No true/false (Default value is false) Setting
to true will cause mobile friendly views to be shown instead of the tradition
screens.\n\ttermsOfUseUrl No Absolute URL to your custom
terms of use URL. If not specified, defaults to http://www.garmin.com/terms\n\tprivacyStatementUrl
\ No Absolute URL to your custom privacy statement URL. If
not specified, defaults to http://www.garmin.com/privacy\n\tproductSupportUrl
\ No Absolute URL to your custom product support URL. If
not specified, defaults to http://www.garmin.com/us/support/contact\n\tgenerateExtraServiceTicket
\ No true/false (Default value is false) If set
to true, generate an extra unconsumed service ticket.\n\t\t (The
service ticket validation response will include the extra service ticket.)\n\tgenerateTwoExtraServiceTickets
\ No true/false (Default value is false) If set to true,
generate two extra unconsumed service tickets.\n\t\t\t\t\t\t\t\t\t \t\t\t
\ (The service ticket validation response will include the extra service
tickets.)\n\tgenerateNoServiceTicket No true/false (Default value
is false) If you don't want SSO to generate a service
ticket at all when logging in to the GAuth login widget.\n (Useful
when allowing logins to static sites that are not SSO enabled and can't consume
the service ticket.)\n\tconnectLegalTerms No true/false (Default
value is false) Whether to show the connectLegalTerms
on the create account page\n\tshowTermsOfUse No true/false
(Default value is false) Whether to show the showTermsOfUse
on the create account page\n\tshowPrivacyPolicy No true/false
(Default value is false) Whether to show the showPrivacyPolicy
on the create account page\n\tshowConnectLegalAge No true/false
(Default value is false) Whether to show the showConnectLegalAge
on the create account page\n\tlocationPromptShown No true/false
(Default value is false) If set to true, ask the customer
during account creation to verify their country of residence.\n\tshowPassword
\ No true/false (Default value is true) If
set to false, mobile version for createAccount and login screens would hide
the password\n\tuseCustomHeader No true/false (Default value
is false) If set to true, the \"Sign in\" text will be
replaced by custom text. Contact CDS team to set the i18n text for your client
id.\n\tmfaRequired No true/false (Default value is false)
\ Require multi factor authentication for all authenticating
users.\n\tperformMFACheck No true/false (Default value is
false) If set to true, ask the logged in user to pass
a multi factor authentication check. (Only valid for an already logged in
user.)\n\trememberMyBrowserShown No true/false (Default value is
false) Whether the \"Remember My Browser\" check box
is shown in the GAuth login widget MFA verification screen.\n\trememberMyBrowserChecked
\ No true/false (Default value is false) Whether
the \"Remember My Browser\" check box feature is checked by default.\n\tconsentTypeIds\t\t\t\t\tNo\tconsent_types
ids\t\t \t\t\t\t\t\t\t\t multiple consent types ids can be passed as consentTypeIds=type1&consentTypeIds=type2\n\t</pre>\n</div>\n\n\n\t<script>(function(){var
js = \"window['__CF$cv$params']={r:'7f1ab8a0eff61559'};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/invisible.js',document.getElementsByTagName('head')[0].appendChild(_cpo);\";var
_0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position
= 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility
= 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi =
_0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj =
_0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if
(document.readyState !== 'loading') {handler();} else if (window.addEventListener)
{document.addEventListener('DOMContentLoaded', handler);} else {var prev =
document.onreadystatechange || function () {};document.onreadystatechange
= function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange
= prev;handler();}};}})();</script></body>\n</html>\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 7f1ab8a0eff61559-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Fri, 04 Aug 2023 23:53:41 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=j5pKqMzrTlGTnexO0FnuZsm0YObQFg1OH0auGBikdNQ44TMOIITdLtHmkIg36gVUZ65RQe4mMPXUL0SfZUdBcVPtg%2F3Dr3d4GgcIueMqtynkohsWR86sKXRVMZroPe%2Fp"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
- __cf_bm=SANITIZED; path=SANITIZED; expires=SANITIZED; domain=SANITIZED; HttpOnly;
Secure; SameSite=SANITIZED
- __cflb=SANITIZED; SameSite=SANITIZED; Secure; path=SANITIZED; expires=SANITIZED;
HttpOnly
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
Transfer-Encoding:
- chunked
X-Application-Context:
- casServer:cloud,prod,prod-US_1102:6
X-B3-Traceid:
- 3d744417e02674885d3c4741abc1daad
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- d62a3e74-b259-4a55-437a-2635831aa9f2
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Cookie:
- __cf_bm=SANITIZED; _cfuvid=SANITIZED; __cflb=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
referer:
- https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
method: GET
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
response:
body:
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n <meta
http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n <meta
name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"X-UA-Compatible\"
content=\"IE=edge;\" />\n <title>GARMIN Authentication Application</title>\n
\ <link href=\"/sso/css/GAuth.css?20210406\" rel=\"stylesheet\" type=\"text/css\"
media=\"all\" />\n\n\t <link rel=\"stylesheet\" href=\"\"/>\n\n <script
type=\"text/javascript\" src=\"/sso/js/jquery/3.1.1/jquery.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/popupWindow.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20210406\"></script>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/gigyaUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/login.js?20211102\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/reCaptchaUtil.js?20230706\"></script>\n\n
\ <script>\n var recaptchaSiteKey = null;\n var
reCaptchaURL = \"\\\\\\/sso\\\\\\/reCaptcha?id=gauth-widget\\u0026embedWidget=true\\u0026gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\";\n
\ var isRecaptchaEnabled = null;\n var recaptchaToken
= null; \n </script>\n <script type=\"text/javascript\">\n
\ var parent_url = \"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n
\ var status \t\t\t= \"\";\n\t\t\tvar result = \"\";\n\t\t\tvar
clientId\t\t= '';\n\t\t\tvar embedWidget \t= true;\n\t\t\tvar isUsernameDefined
= (false == true) || (false == true);\n\n // Gigya callback to
SocialSignInController for brand new social network users redirects to this
page\n // to popup Create or Link Social Account page, but has
a possibly mangled source parameter\n // where \"?\" is set as
\"<QM>\", so translate it back to \"?\" here.\n parent_url = parent_url.replace('<QM>',
'?');\n var parent_scheme = parent_url.substring(0, parent_url.indexOf(\"://\"));\n
\ var parent_hostname = parent_url.substring(parent_scheme.length
+ 3, parent_url.length);\n if (parent_hostname.indexOf(\"/\") !=
-1) {\n parent_hostname = parent_hostname.substring(0, parent_hostname.indexOf(\"/\"));\n
\ }\n var parentHost \t = parent_scheme + \"://\"
+ parent_hostname;\n\t\t\tvar createAccountConfigURL = '\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed';\n
\ var socialConfigURL = 'https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed';\n
\ var gigyaURL = \"https://cdns.gigya.com/js/gigya.js?apiKey=2_R3ZGY8Bqlwwk3_63knoD9wA_m-Y19mAgW61bF_s5k9gymYnMEAtMrJiF5MjF-U7B\";\n\n
\ if (createAccountConfigURL.indexOf('%253A%252F%252F') != -1) {\n
\ \tcreateAccountConfigURL = decodeURIComponent(createAccountConfigURL);\n
\ }\n consoleInfo('signin.html embedWidget: true, createAccountConfigURL:
\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed,
socialEnabled: true, gigyaSupported: true, socialConfigURL(): https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed');\n\n
\ if (socialConfigURL.indexOf('%3A%2F%2F') != -1) {\n \tsocialConfigURL
= decodeURIComponent(socialConfigURL);\n }\n\n if( status
!= null && status != ''){\n \tsend({'status':status});\n }\n\n
\ jQuery(document).ready( function(){\n\n\n consoleInfo(\"signin.html:
setting field validation rules...\");\n\n jQuery(\"#username\").rules(\"add\",{\n
\ required: true,\n messages: {\n required:
\ \"Email is required.\"\n }});\n\n jQuery(\"#password\").rules(\"add\",
{\n required: true,\n messages: {\n
\ required: \"Password is required.\"\n }\n
\ });\n\n consoleInfo(\"signin.html: done setting
field validation rules...\");\n\n });\n\n XD.receiveMessage(function(m){\n
\ consoleInfo(\"signin.html: \" + m.data + \" received on \"
+ window.location.host);\n if (m && m.data) {\n var
md = m.data;\n if (typeof(md) === 'string') {\n md
= JSON.parse(m.data);\n }\n if (md.setUsername)
{\n consoleInfo(\"signin.html: Setting username \\\"\"
+ md.username + \"\\\"...\");\n jQuery(\"#signInWithDiffLink\").click();
// Ensure the normal login form is shown.\n jQuery(\"#username\").val(md.username);\n
\ jQuery(\"#password\").focus();\n }\n
\ }\n }, parentHost);\n </script>\n </head>\n
\ <body>\n\n <!-- begin GAuth component -->\n <div id=\"GAuth-component\">\n
\ <!-- begin login component-->\n <div id=\"login-component\"
class=\"blueForm-basic\">\n <input type=\"hidden\" id=\"queryString\"
value=\"id=gauth-widget&amp;embedWidget=true&amp;gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
/>\n\t \t <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n
\ <!-- begin login form -->\n <div id=\"login-state-default\">\n
\ <h2>Sign In</h2>\n\n <form method=\"post\"
id=\"login-form\">\n\n <div class=\"form-alert\">\n\t\t\t\t\t\t\t\n
\ \n \n \n
\ \n \n \n\n
\ <div id=\"username-error\" style=\"display:none;\"></div>\n
\ <div id=\"password-error\" style=\"display:none;\"></div>\n
\ </div>\n <div class=\"textfield\">\n\t\t\t\t\t\t\t<label
for=\"username\">Email</label>\n \t\t<!-- If the
lockToEmailAddress parameter is specified then we want to mark the field as
readonly,\n \t\tpreload the email address, and disable
the other input so that null isn't sent to the server. We'll\n \t\talso
style the field to have a darker grey background and disable the mouse pointer\n
\ \t\t -->\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t<!--
If the lockToEmailAddress parameter is NOT specified then keep the existing
functionality and disable the readonly input field\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t
\ <input class=\"login_email\" name=\"username\" id=\"username\" value=\"\"
type=\"email\" spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\"/>\n\n
\ </div>\n\n <div class=\"textfield\">\n
\ <label for=\"password\">Password</label>\n <a
id=\"loginforgotpassword\" class=\"login-forgot-password\" style=\"cursor:pointer\">(Forgot?)</a>\n
\ <input type=\"password\" name=\"password\" id=\"password\"
spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" />\n <strong
id=\"capslock-warning\" class=\"information\" title=\"Caps lock is on.\" style=\"display:
none;\">Caps lock is on.</strong>\n\t\t\t\t\t </div>\n <input
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
type=\"hidden\" name=\"_csrf\" value=\"DAA89CB8362ABB6DB2548101BE44A857AFCE1CC8C7B0825A54361D5D1FB60E47649DA070B388D04CB797C2AD4C9B9FAF9E3C\"
/>\n <button type=\"submit\" id=\"login-btn-signin\"
class=\"btn1\" accesskey=\"l\">Sign In</button>\n \n\n\n
\ <!-- The existence of the \"rememberme\" parameter
at all will remember the user! -->\n \n\n </form>\n
\ </div>\n <!-- end login form -->\n\n <!--
begin Create Account message -->\n\t <div id=\"login-create-account\">\n\t
\ \n\t </div>\n\t <!-- end Create Account
message -->\n\n\t <!-- begin Social Sign In component -->\n\t <div
id=\"SSI-component\">\n \n\n\t\t\t\t\t\n\t </div>\n\t
\ <!-- end Social Sign In component -->\n <div class=\"clearfix\"></div>
<!-- Ensure that GAuth-component div's height is computed correctly. -->\n
\ </div>\n <!-- end login component-->\n\n\t\t</div>\n\t\t<!--
end GAuth component -->\n\n <script type=\"text/javascript\">\n jQuery(document).ready(function(){\n
\ \tresizePageOnLoad(jQuery(\"#GAuth-component\").height());\n\n\t\t
\ if(isUsernameDefined == true){\n\t\t // If the user's login
just failed, redisplay the email/username specified, and focus them in the
password field.\n\t\t jQuery(\"#password\").focus();\n\t\t }
else if(false == true && result != \"PASSWORD_RESET_RESULT\"){\n //
Otherwise focus them in the username field of the login dialog.\n jQuery(\"#username\").focus();\n
\ }\n\n // Scroll to top of iframe to fix problem
where Firefox 3.0-3.6 browsers initially show top of iframe cutoff.\n location.href=\"#\";\n\n
\ if(!embedWidget){\n \tjQuery('.createAccountLink').click(function(){\n\t
\ send({'openLiteBox':'createAccountLink', 'popupUrl': createAccountConfigURL,
'popupTitle':'Create An Account', 'clientId':clientId});\n\t });\n
\ }\n });\n </script>\n <script>(function(){var
js = \"window['__CF$cv$params']={r:'7f1ab8a1e90a155f'};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/invisible.js',document.getElementsByTagName('head')[0].appendChild(_cpo);\";var
_0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position
= 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility
= 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi =
_0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj =
_0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if
(document.readyState !== 'loading') {handler();} else if (window.addEventListener)
{document.addEventListener('DOMContentLoaded', handler);} else {var prev =
document.onreadystatechange || function () {};document.onreadystatechange
= function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange
= prev;handler();}};}})();</script></body>\n</html>\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- 7f1ab8a1e90a155f-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Fri, 04 Aug 2023 23:53:41 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=2m3IsPHrodwZcDphNIdQkuKtFDRIq67h9%2BNyhtturCTJsq8UH%2BqzYY1lhYjgkKLu0YrwD8sYfVBP03Dj8Lf4R0Ghzc0o647YHYroy2Tkp2YQLDOtMwR56XKEVYEl0yhg"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
- SESSION=SANITIZED; Path=SANITIZED; Secure; HttpOnly
- __VCAP_ID__=SANITIZED; Path=SANITIZED; HttpOnly; Secure
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
X-Application-Context:
- casServer:cloud,prod,prod-US_1102:5
X-B3-Traceid:
- 5ebef167e205ed0449ddea900e2d06fc
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- 8bb46b1b-d486-4df3-5d3e-2767045abcdd
status:
code: 200
message: OK
- request:
body: username=SANITIZED&password=SANITIZED&embed=true&_csrf=DAA89CB8362ABB6DB2548101BE44A857AFCE1CC8C7B0825A54361D5D1FB60E47649DA070B388D04CB797C2AD4C9B9FAF9E3C
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '171'
Content-Type:
- application/x-www-form-urlencoded
Cookie:
- SESSION=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED; __VCAP_ID__=SANITIZED;
__cflb=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED
User-Agent:
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
(KHTML, like Gecko) Mobile/15E148
referer:
- https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: POST
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
response:
body:
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n <meta
http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n <meta
name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"X-UA-Compatible\"
content=\"IE=edge;\" />\n <title>GARMIN Authentication Application</title>\n
\ <link href=\"/sso/css/GAuth.css?20210406\" rel=\"stylesheet\" type=\"text/css\"
media=\"all\" />\n\n\t <link rel=\"stylesheet\" href=\"\"/>\n\n <script
type=\"text/javascript\" src=\"/sso/js/jquery/3.1.1/jquery.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/popupWindow.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20210406\"></script>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/gigyaUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/login.js?20211102\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/reCaptchaUtil.js?20230706\"></script>\n\n
\ <script>\n var recaptchaSiteKey = null;\n var
reCaptchaURL = \"\\\\\\/sso\\\\\\/reCaptcha?id=gauth-widget\\u0026embedWidget=true\\u0026gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\";\n
\ var isRecaptchaEnabled = null;\n var recaptchaToken
= null; \n </script>\n <script type=\"text/javascript\">\n
\ var parent_url = \"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n
\ var status \t\t\t= \"FAIL\";\n\t\t\tvar result = \"error\";\n\t\t\tvar
clientId\t\t= '';\n\t\t\tvar embedWidget \t= true;\n\t\t\tvar isUsernameDefined
= (true == true) || (true == true);\n\n // Gigya callback to SocialSignInController
for brand new social network users redirects to this page\n //
to popup Create or Link Social Account page, but has a possibly mangled source
parameter\n // where \"?\" is set as \"<QM>\", so translate it
back to \"?\" here.\n parent_url = parent_url.replace('<QM>', '?');\n
\ var parent_scheme = parent_url.substring(0, parent_url.indexOf(\"://\"));\n
\ var parent_hostname = parent_url.substring(parent_scheme.length
+ 3, parent_url.length);\n if (parent_hostname.indexOf(\"/\") !=
-1) {\n parent_hostname = parent_hostname.substring(0, parent_hostname.indexOf(\"/\"));\n
\ }\n var parentHost \t = parent_scheme + \"://\"
+ parent_hostname;\n\t\t\tvar createAccountConfigURL = '\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed';\n
\ var socialConfigURL = 'https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed';\n
\ var gigyaURL = \"https://cdns.gigya.com/js/gigya.js?apiKey=2_R3ZGY8Bqlwwk3_63knoD9wA_m-Y19mAgW61bF_s5k9gymYnMEAtMrJiF5MjF-U7B\";\n\n
\ if (createAccountConfigURL.indexOf('%253A%252F%252F') != -1) {\n
\ \tcreateAccountConfigURL = decodeURIComponent(createAccountConfigURL);\n
\ }\n consoleInfo('signin.html embedWidget: true, createAccountConfigURL:
\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed,
socialEnabled: true, gigyaSupported: true, socialConfigURL(): https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed');\n\n
\ if (socialConfigURL.indexOf('%3A%2F%2F') != -1) {\n \tsocialConfigURL
= decodeURIComponent(socialConfigURL);\n }\n\n if( status
!= null && status != ''){\n \tsend({'status':status});\n }\n\n
\ jQuery(document).ready( function(){\n\n\n consoleInfo(\"signin.html:
setting field validation rules...\");\n\n jQuery(\"#username\").rules(\"add\",{\n
\ required: true,\n messages: {\n required:
\ \"Email is required.\"\n }});\n\n jQuery(\"#password\").rules(\"add\",
{\n required: true,\n messages: {\n
\ required: \"Password is required.\"\n }\n
\ });\n\n consoleInfo(\"signin.html: done setting
field validation rules...\");\n\n });\n\n XD.receiveMessage(function(m){\n
\ consoleInfo(\"signin.html: \" + m.data + \" received on \"
+ window.location.host);\n if (m && m.data) {\n var
md = m.data;\n if (typeof(md) === 'string') {\n md
= JSON.parse(m.data);\n }\n if (md.setUsername)
{\n consoleInfo(\"signin.html: Setting username \\\"\"
+ md.username + \"\\\"...\");\n jQuery(\"#signInWithDiffLink\").click();
// Ensure the normal login form is shown.\n jQuery(\"#username\").val(md.username);\n
\ jQuery(\"#password\").focus();\n }\n
\ }\n }, parentHost);\n </script>\n </head>\n
\ <body>\n\n <!-- begin GAuth component -->\n <div id=\"GAuth-component\">\n
\ <!-- begin login component-->\n <div id=\"login-component\"
class=\"blueForm-basic\">\n <input type=\"hidden\" id=\"queryString\"
value=\"id=gauth-widget&amp;embedWidget=true&amp;gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
/>\n\t \t <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n
\ <!-- begin login form -->\n <div id=\"login-state-default\">\n
\ <h2>Sign In</h2>\n\n <form method=\"post\"
id=\"login-form\">\n\n <div class=\"form-alert\">\n\t\t\t\t\t\t\t\n
\ \n \n \n
\ \n \n <div
id=\"status\" class=\"error\">Invalid sign in. (Passwords are case sensitive.)</div>\n\n
\ <div id=\"username-error\" style=\"display:none;\"></div>\n
\ <div id=\"password-error\" style=\"display:none;\"></div>\n
\ </div>\n <div class=\"textfield\">\n\t\t\t\t\t\t\t<label
for=\"username\">Email</label>\n \t\t<!-- If the
lockToEmailAddress parameter is specified then we want to mark the field as
readonly,\n \t\tpreload the email address, and disable
the other input so that null isn't sent to the server. We'll\n \t\talso
style the field to have a darker grey background and disable the mouse pointer\n
\ \t\t -->\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t<!--
If the lockToEmailAddress parameter is NOT specified then keep the existing
functionality and disable the readonly input field\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t
\ <input class=\"login_email\" name=\"username\" id=\"username\" value=\"user@example.com\"
type=\"email\" spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\"/>\n\n
\ </div>\n\n <div class=\"textfield\">\n
\ <label for=\"password\">Password</label>\n <a
id=\"loginforgotpassword\" class=\"login-forgot-password\" style=\"cursor:pointer\">(Forgot?)</a>\n
\ <input type=\"password\" name=\"password\" id=\"password\"
spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" />\n <strong
id=\"capslock-warning\" class=\"information\" title=\"Caps lock is on.\" style=\"display:
none;\">Caps lock is on.</strong>\n\t\t\t\t\t </div>\n <input
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
type=\"hidden\" name=\"_csrf\" value=\"25BD4FF6F23ED011DADAD97BD7125D89DF74ACC8A85485B451F997AB2E42D9216133505121272347D78B445FB19881C9968B\"
/>\n <button type=\"submit\" id=\"login-btn-signin\"
class=\"btn1\" accesskey=\"l\">Sign In</button>\n \n\n\n
\ <!-- The existence of the \"rememberme\" parameter
at all will remember the user! -->\n \n\n </form>\n
\ </div>\n <!-- end login form -->\n\n <!--
begin Create Account message -->\n\t <div id=\"login-create-account\">\n\t
\ \n\t </div>\n\t <!-- end Create Account
message -->\n\n\t <!-- begin Social Sign In component -->\n\t <div
id=\"SSI-component\">\n \n\n\t\t\t\t\t\n\t </div>\n\t
\ <!-- end Social Sign In component -->\n <div class=\"clearfix\"></div>
<!-- Ensure that GAuth-component div's height is computed correctly. -->\n
\ </div>\n <!-- end login component-->\n\n\t\t</div>\n\t\t<!--
end GAuth component -->\n\n <script type=\"text/javascript\">\n jQuery(document).ready(function(){\n
\ \tresizePageOnLoad(jQuery(\"#GAuth-component\").height());\n\n\t\t
\ if(isUsernameDefined == true){\n\t\t // If the user's login
just failed, redisplay the email/username specified, and focus them in the
password field.\n\t\t jQuery(\"#password\").focus();\n\t\t }
else if(false == true && result != \"PASSWORD_RESET_RESULT\"){\n //
Otherwise focus them in the username field of the login dialog.\n jQuery(\"#username\").focus();\n
\ }\n\n // Scroll to top of iframe to fix problem
where Firefox 3.0-3.6 browsers initially show top of iframe cutoff.\n location.href=\"#\";\n\n
\ if(!embedWidget){\n \tjQuery('.createAccountLink').click(function(){\n\t
\ send({'openLiteBox':'createAccountLink', 'popupUrl': createAccountConfigURL,
'popupTitle':'Create An Account', 'clientId':clientId});\n\t });\n
\ }\n });\n </script>\n <script>(function(){var
js = \"window['__CF$cv$params']={r:'7f1ab8a41e554752'};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/invisible.js',document.getElementsByTagName('head')[0].appendChild(_cpo);\";var
_0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position
= 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility
= 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi =
_0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj =
_0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if
(document.readyState !== 'loading') {handler();} else if (window.addEventListener)
{document.addEventListener('DOMContentLoaded', handler);} else {var prev =
document.onreadystatechange || function () {};document.onreadystatechange
= function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange
= prev;handler();}};}})();</script></body>\n</html>\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- 7f1ab8a41e554752-DFW
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Fri, 04 Aug 2023 23:53:42 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NmXSOa2OY4MXHw09DrfMkMUFE5FijBSW8oF9uituKDizIcYfhS1rFKYV0Q3ACOQVYT6Q8Iwzj6PiIL%2BBY6E4f%2BFqsB2a20zfVAiW65WDXm6hGdPkJozBoAfyFzQZzAOc"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
- __cfruid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
X-Application-Context:
- casServer:cloud,prod,prod-US_1102:5
X-B3-Traceid:
- 139f06d066cf2a2d4b988ae89e0203d7
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- b2849c1b-b909-403f-57b6-e13f20fc9546
status:
code: 401
message: Unauthorized
version: 1

View File

@@ -1,749 +0,0 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
User-Agent:
- GCM-iOS-5.7.2.1
referer:
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: GET
uri: https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
response:
body:
string: "<html>\n\t<head>\n\t <title>GAuth Embedded Version</title>\n\t <meta
http-equiv=\"X-UA-Compatible\" content=\"IE=edge;\" />\n\t <style type=\"text/css\">\n\t
\ \t#gauth-widget {border: none !important;}\n\t </style>\n\t</head>\n\t<body>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n\n<div>\n\t<pre>\n\t<span>ERROR:
clientId parameter must be specified!!!</span>\n\n\t<span >Usage: https://sso.garmin.com/sso/embed?clientId=&lt;clientId&gt;&amp;locale=&lt;locale&gt;...</span>\n\n\tRequest
parameter configuration options:\n\n\tNAME REQ VALUES
\ DESCRIPTION\n\t------------------
\ --- -------------------------------------------------------
\ ---------------------------------------------------------------------------------------------------\n\tclientId
\ Yes \"MY_GARMIN\"/\"BUY_GARMIN\"/\"FLY_GARMIN\"/ Client
identifier for your web application\n\t \"RMA\"/\"GarminConnect\"/\"OpenCaching\"/etc\n\tlocale
\ Yes \"en\", \"bg\", \"cs\", \"da\", \"de\", \"es\",
\"el\", \"fr\", \"hr\", User's current locale, to display the GAuth login
widget internationalized properly.\n\t \"in\",
\"it\", \"iw\", \"hu\", \"ms\", \"nb\", \"nl\", \"no\", \"pl\", (All the
currently supported locales are listed in the Values section.)\n\t \"pt\",
\"pt_BR\", \"ru\", \"sk\", \"sl\", \"fi\", \"sv\", \"tr\",\n\t \"uk\",
\"th\", \"ja\", \"ko\", \"zh_TW\", \"zh\", \"vi_VN\"\n\tcssUrl No
\ Absolute URL to custom CSS file. Use custom CSS
styling for the GAuth login widget.\n\treauth No
\ true/false (Default value is false) Specify true if
you want to ensure that the GAuth login widget shows up,\n\t even
if the SSO infrastructure remembers the user and would immediately log them
in.\n\t This
is useful if you know a user is logged on, but want a different user to be
allowed to logon.\n\tinitialFocus No true/false (Default
value is true) If you don't want the GAuth login widget
to autofocus in it's \"Email or Username\" field upon initial loading,\n\t
\ then
specify this option and set it to false.\n\trememberMeShown No
\ true/false (Default value is false) Whether the \"Remember
Me\" check box is shown in the GAuth login widget.\n\trememberMeChecked No
\ true/false (Default value is false) Whether the \"Remember
Me\" check box feature is checked by default.\n\tcreateAccountShown No
\ true/false (Default value is true) Whether the \"Don't
have an account? Create One\" link is shown in the GAuth login widget.\n\tsocialEnabled
\ No true/false (Default value is false) If
set to false, do not show any social sign in elements or allow social sign
ins.\n\tlockToEmailAddress No Email address to pre-load and
lock. If specified, the specified email address will
be pre-loaded in the main \"Email\" field in the SSO login form,\n\t as
well as in in the \"Email Address\" field in the \"Forgot Password?\" password
reset form,\n\t and
both fields will be disabled so they can't be changed.\n\t (If
for some reason you want to force re-authentications for a known customer
account, you can make use of this option.)\n\topenCreateAccount No
\ true/false (Default value is false) If set to true,
immediately display the the account creation screen.\n\tdisplayNameShown No
\ true/false (Default value is false) If set to true,
show the \"Display Name\" field on the account creation screen, to allow the
user\n\t to
set their central MyGarmin display name upon account creation.\n\tglobalOptInShown
\ No true/false (Default value is false) Whether
the \"Global Opt-In\" check box is shown on the create account & create social
account screens.\n\t If
set to true these screens will show a \"Sign Up For Email\" check box with
accompanying text\n\t \"I
would also like to receive email about promotions and new products.\"\n\t
\ If
checked, the Customer 2.0 account that is created will have it's global opt-in
flag set to true,\n\t and
Garmin email communications will be allowed.\n\tglobalOptInChecked No
\ true/false (Default value is false) Whether the \"Global
Opt-In\" check box is checked by default.\n\tconsumeServiceTicket No
\ true/false (Default value is true) IF you don't specify
a redirectAfterAccountLoginUrl AND you set this to false, the GAuth login
widget\n\t will
NOT consume the service ticket assigned and will not seamlessly log you into
your webapp.\n\t It
will send a SUCCESS JavaScript event with the service ticket and service url
you can take\n\t and
explicitly validate against the SSO infrastructure yourself.\n\t (By
using casClient's SingleSignOnUtils.authenticateServiceTicket() utility method,\n\t
\ or
calling web service customerWebServices_v1.2 AccountManagementService.authenticateServiceTicket().)\n\tmobile
\ No true/false (Default value is false) Setting
to true will cause mobile friendly views to be shown instead of the tradition
screens.\n\ttermsOfUseUrl No Absolute URL to your custom
terms of use URL. If not specified, defaults to http://www.garmin.com/terms\n\tprivacyStatementUrl
\ No Absolute URL to your custom privacy statement URL. If
not specified, defaults to http://www.garmin.com/privacy\n\tproductSupportUrl
\ No Absolute URL to your custom product support URL. If
not specified, defaults to http://www.garmin.com/us/support/contact\n\tgenerateExtraServiceTicket
\ No true/false (Default value is false) If set
to true, generate an extra unconsumed service ticket.\n\t\t (The
service ticket validation response will include the extra service ticket.)\n\tgenerateTwoExtraServiceTickets
\ No true/false (Default value is false) If set to true,
generate two extra unconsumed service tickets.\n\t\t\t\t\t\t\t\t\t \t\t\t
\ (The service ticket validation response will include the extra service
tickets.)\n\tgenerateNoServiceTicket No true/false (Default value
is false) If you don't want SSO to generate a service
ticket at all when logging in to the GAuth login widget.\n (Useful
when allowing logins to static sites that are not SSO enabled and can't consume
the service ticket.)\n\tconnectLegalTerms No true/false (Default
value is false) Whether to show the connectLegalTerms
on the create account page\n\tshowTermsOfUse No true/false
(Default value is false) Whether to show the showTermsOfUse
on the create account page\n\tshowPrivacyPolicy No true/false
(Default value is false) Whether to show the showPrivacyPolicy
on the create account page\n\tshowConnectLegalAge No true/false
(Default value is false) Whether to show the showConnectLegalAge
on the create account page\n\tlocationPromptShown No true/false
(Default value is false) If set to true, ask the customer
during account creation to verify their country of residence.\n\tshowPassword
\ No true/false (Default value is true) If
set to false, mobile version for createAccount and login screens would hide
the password\n\tuseCustomHeader No true/false (Default value
is false) If set to true, the \"Sign in\" text will be
replaced by custom text. Contact CDS team to set the i18n text for your client
id.\n\tmfaRequired No true/false (Default value is false)
\ Require multi factor authentication for all authenticating
users.\n\tperformMFACheck No true/false (Default value is
false) If set to true, ask the logged in user to pass
a multi factor authentication check. (Only valid for an already logged in
user.)\n\trememberMyBrowserShown No true/false (Default value is
false) Whether the \"Remember My Browser\" check box
is shown in the GAuth login widget MFA verification screen.\n\trememberMyBrowserChecked
\ No true/false (Default value is false) Whether
the \"Remember My Browser\" check box feature is checked by default.\n\tconsentTypeIds\t\t\t\t\tNo\tconsent_types
ids\t\t \t\t\t\t\t\t\t\t multiple consent types ids can be passed as consentTypeIds=type1&consentTypeIds=type2\n\t</pre>\n</div>\n\n\n\t<script>(function(){function
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83cf2bfb5e42',t:'MTc0ODkyNTY1Mi4wMDAwMDA='};var
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-RAY:
- 949c83cf2bfb5e42-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Tue, 03 Jun 2025 04:40:52 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=cwd6V1kar7GXC7ImUBfvrwg3vgZw4sMdraKN0bkZjRt%2Bsu4gSDU%2Bv0N%2BSUhVzY7ZkTgMTuIkEmTRl7ywQ5Z%2FAD3BUh03xdXX%2B2qCgU0plnOrl93fBAlMcDC9U%2FMRzoHW"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
- __cf_bm=SANITIZED; path=SANITIZED; expires=SANITIZED; domain=SANITIZED; HttpOnly;
Secure; SameSite=SANITIZED
- __cflb=SANITIZED; SameSite=SANITIZED; Secure; path=SANITIZED; expires=SANITIZED;
HttpOnly
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
Transfer-Encoding:
- chunked
X-Application-Context:
- casServer:cloud,prod,prod-US_Olathe:3
X-B3-Traceid:
- 85cea212845648ad7fbb7b5ad97acb70
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- 85cea212-8456-48ad-7fbb-7b5ad97acb70
cf-cache-status:
- DYNAMIC
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
__cflb=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED
User-Agent:
- GCM-iOS-5.7.2.1
referer:
- https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
method: GET
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
response:
body:
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n <meta
http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n <meta
name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"X-UA-Compatible\"
content=\"IE=edge;\" />\n <title>GARMIN Authentication Application</title>\n
\ <link href=\"/sso/css/GAuth.css?20210406\" rel=\"stylesheet\" type=\"text/css\"
media=\"all\" />\n\n\t <link rel=\"stylesheet\" href=\"\"/>\n\n <script
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/popupWindow.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20231020\"></script>\n\t\t<script
type=\"text/javascript\" src=\"/sso/js/gigyaUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/login.js?20211102\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/reCaptchaUtil.js?20230706\"></script>\n\n
\ <script>\n var recaptchaSiteKey = null;\n var
reCaptchaURL = \"\\\\\\/sso\\\\\\/reCaptcha?id=gauth-widget\\u0026embedWidget=true\\u0026gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\";\n
\ var isRecaptchaEnabled = null;\n var recaptchaToken
= null; \n </script>\n <script type=\"text/javascript\">\n
\ var parent_url = \"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n
\ var status \t\t\t= \"\";\n\t\t\tvar result = \"\";\n\t\t\tvar
clientId\t\t= '';\n\t\t\tvar embedWidget \t= true;\n\t\t\tvar isUsernameDefined
= (false == true) || (false == true);\n\n // Gigya callback to
SocialSignInController for brand new social network users redirects to this
page\n // to popup Create or Link Social Account page, but has
a possibly mangled source parameter\n // where \"?\" is set as
\"<QM>\", so translate it back to \"?\" here.\n parent_url = parent_url.replace('<QM>',
'?');\n var parent_scheme = parent_url.substring(0, parent_url.indexOf(\"://\"));\n
\ var parent_hostname = parent_url.substring(parent_scheme.length
+ 3, parent_url.length);\n if (parent_hostname.indexOf(\"/\") !=
-1) {\n parent_hostname = parent_hostname.substring(0, parent_hostname.indexOf(\"/\"));\n
\ }\n var parentHost \t = parent_scheme + \"://\"
+ parent_hostname;\n\t\t\tvar createAccountConfigURL = '\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed';\n
\ var socialConfigURL = 'https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed';\n
\ var gigyaURL = \"https://cdns.gigya.com/js/gigya.js?apiKey=2_R3ZGY8Bqlwwk3_63knoD9wA_m-Y19mAgW61bF_s5k9gymYnMEAtMrJiF5MjF-U7B\";\n\n
\ if (createAccountConfigURL.indexOf('%253A%252F%252F') != -1) {\n
\ \tcreateAccountConfigURL = decodeURIComponent(createAccountConfigURL);\n
\ }\n consoleInfo('signin.html embedWidget: true, createAccountConfigURL:
\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed,
socialEnabled: true, gigyaSupported: true, socialConfigURL(): https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed');\n\n
\ if (socialConfigURL.indexOf('%3A%2F%2F') != -1) {\n \tsocialConfigURL
= decodeURIComponent(socialConfigURL);\n }\n\n if( status
!= null && status != ''){\n \tsend({'status':status});\n }\n\n
\ jQuery(document).ready( function(){\n\n\n consoleInfo(\"signin.html:
setting field validation rules...\");\n\n jQuery(\"#username\").rules(\"add\",{\n
\ required: true,\n messages: {\n required:
\ \"Email is required.\"\n }});\n\n jQuery(\"#password\").rules(\"add\",
{\n required: true,\n messages: {\n
\ required: \"Password is required.\"\n }\n
\ });\n\n consoleInfo(\"signin.html: done setting
field validation rules...\");\n\n });\n\n XD.receiveMessage(function(m){\n
\ consoleInfo(\"signin.html: \" + m.data + \" received on \"
+ window.location.host);\n if (m && m.data) {\n var
md = m.data;\n if (typeof(md) === 'string') {\n md
= JSON.parse(m.data);\n }\n if (md.setUsername)
{\n consoleInfo(\"signin.html: Setting username \\\"\"
+ md.username + \"\\\"...\");\n jQuery(\"#signInWithDiffLink\").click();
// Ensure the normal login form is shown.\n jQuery(\"#username\").val(md.username);\n
\ jQuery(\"#password\").focus();\n }\n
\ }\n }, parentHost);\n </script>\n </head>\n
\ <body>\n\n <!-- begin GAuth component -->\n <div id=\"GAuth-component\">\n
\ <!-- begin login component-->\n <div id=\"login-component\"
class=\"blueForm-basic\">\n <input type=\"hidden\" id=\"queryString\"
value=\"id=gauth-widget&amp;embedWidget=true&amp;gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
/>\n\t \t <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n
\ <!-- begin login form -->\n <div id=\"login-state-default\">\n
\ <h2>Sign In</h2>\n\n <form method=\"post\"
id=\"login-form\">\n\n <div class=\"form-alert\">\n\t\t\t\t\t\t\t\n
\ \n \n \n
\ \n \n \n\n
\ <div id=\"username-error\" style=\"display:none;\"></div>\n
\ <div id=\"password-error\" style=\"display:none;\"></div>\n
\ </div>\n <div class=\"textfield\">\n\t\t\t\t\t\t\t<label
for=\"username\">Email</label>\n \t\t<!-- If the
lockToEmailAddress parameter is specified then we want to mark the field as
readonly,\n \t\tpreload the email address, and disable
the other input so that null isn't sent to the server. We'll\n \t\talso
style the field to have a darker grey background and disable the mouse pointer\n
\ \t\t -->\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t<!--
If the lockToEmailAddress parameter is NOT specified then keep the existing
functionality and disable the readonly input field\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t
\ <input class=\"login_email\" name=\"username\" id=\"username\" value=\"\"
type=\"email\" spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\"/>\n\n
\ </div>\n\n <div class=\"textfield\">\n
\ <label for=\"password\">Password</label>\n <a
id=\"loginforgotpassword\" class=\"login-forgot-password\" style=\"cursor:pointer\">(Forgot?)</a>\n
\ <input type=\"password\" name=\"password\" id=\"password\"
spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" />\n <strong
id=\"capslock-warning\" class=\"information\" title=\"Caps lock is on.\" style=\"display:
none;\">Caps lock is on.</strong>\n\t\t\t\t\t </div>\n <input
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
type=\"hidden\" name=\"_csrf\" value=\"90280BE13709DE2C0CF38CAB2A77E3FC82F62894F2396D07630AD246706B197735797D02C4592A6D5AB3B8BF1F3B80460522\"
/>\n <button type=\"submit\" id=\"login-btn-signin\"
class=\"btn1\" accesskey=\"l\">Sign In</button>\n \n\n\n
\ <!-- The existence of the \"rememberme\" parameter
at all will remember the user! -->\n \n\n </form>\n
\ </div>\n <!-- end login form -->\n\n <!--
begin Create Account message -->\n\t <div id=\"login-create-account\">\n\t
\ \n\t </div>\n\t <!-- end Create Account
message -->\n\n\t <!-- begin Social Sign In component -->\n\t <div
id=\"SSI-component\">\n \n\n\t\t\t\t\t\n\t </div>\n\t
\ <!-- end Social Sign In component -->\n <div class=\"clearfix\"></div>
<!-- Ensure that GAuth-component div's height is computed correctly. -->\n
\ </div>\n <!-- end login component-->\n\n\t\t</div>\n\t\t<!--
end GAuth component -->\n\n <script type=\"text/javascript\">\n jQuery(document).ready(function(){\n
\ \tresizePageOnLoad(jQuery(\"#GAuth-component\").height());\n\n\t\t
\ if(isUsernameDefined == true){\n\t\t // If the user's login
just failed, redisplay the email/username specified, and focus them in the
password field.\n\t\t jQuery(\"#password\").focus();\n\t\t }
else if(false == true && result != \"PASSWORD_RESET_RESULT\"){\n //
Otherwise focus them in the username field of the login dialog.\n jQuery(\"#username\").focus();\n
\ }\n\n // Scroll to top of iframe to fix problem
where Firefox 3.0-3.6 browsers initially show top of iframe cutoff.\n location.href=\"#\";\n\n
\ if(!embedWidget){\n \tjQuery('.createAccountLink').click(function(){\n\t
\ send({'openLiteBox':'createAccountLink', 'popupUrl': createAccountConfigURL,
'popupTitle':'Create An Account', 'clientId':clientId});\n\t });\n
\ }\n });\n </script>\n <script>(function(){function
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83d17d414f14',t:'MTc0ODkyNTY1Mi4wMDAwMDA='};var
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- 949c83d17d414f14-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Tue, 03 Jun 2025 04:40:52 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=REffANT2%2FfOZY9xYp%2FXinxsCOsc73u6TBWc0qVetJyK9oQhy63N6Qk3fNr5TDiEV9JM9RIKw5uZhoVeBr7vDZK1f0UsNdTsjHdr19V0Lnt%2FCqbU6Y3MTWpcTaQYMqUIo"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
- SESSION=SANITIZED; Path=SANITIZED; Secure; HttpOnly
- __VCAP_ID__=SANITIZED; Path=SANITIZED; HttpOnly; Secure
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
X-Application-Context:
- casServer:cloud,prod,prod-US_Olathe:6
X-B3-Traceid:
- 77e60c0ac1d641c074820aac41fbde80
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- 77e60c0a-c1d6-41c0-7482-0aac41fbde80
status:
code: 200
message: OK
- request:
body: username=SANITIZED&password=SANITIZED&embed=true&_csrf=90280BE13709DE2C0CF38CAB2A77E3FC82F62894F2396D07630AD246706B197735797D02C4592A6D5AB3B8BF1F3B80460522
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
Content-Length:
- '177'
Content-Type:
- application/x-www-form-urlencoded
Cookie:
- SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
__cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED
User-Agent:
- GCM-iOS-5.7.2.1
referer:
- https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: POST
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
response:
body:
string: ''
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- 949c83d32cf657bd-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Length:
- '0'
Date:
- Tue, 03 Jun 2025 04:40:54 GMT
Location:
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=8rQsZTg41dTwDgWNQriYHPcbY3UG6NQ0v%2FN6zaizxXzpDFLJALfe7s%2BopIWHB0dvU9WeEEUreQPI2Wlkgz2Gp6z9fx51UvQZtS3N2hIGKyEW7QNno8eCyuMyHYGXjJOx"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
- __cfruid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
Vary:
- Accept-Encoding
X-Application-Context:
- casServer:cloud,prod,prod-US_Olathe:6
X-B3-Traceid:
- 1da874cc48894fdf4e1ac9d9e8e269c8
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- 1da874cc-4889-4fdf-4e1a-c9d9e8e269c8
status:
code: 302
message: Found
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
Cookie:
- SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
__cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED;
__cfruid=SANITIZED
User-Agent:
- GCM-iOS-5.7.2.1
referer:
- https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: GET
uri: https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
response:
body:
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n\n<head>\n <script
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n <script
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20231020\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/setupMfaRequiredView.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/enterMfaCode.js?20230127\"></script>\n
\ <script type=\"text/javascript\">\n var embedWidget = \"true\";\n
\ if (embedWidget == \"\") {\n embedWidget = \"\";\n }\n
\ embedWidget = (embedWidget == \"true\");\n var parent_url =
\"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n window.onload = function()
{\n ifrememberMyBrowserChecked();\n };\n\n jQuery(document).ready(
function() {\n if (!embedWidget) {\n send({'gauthHeight':
jQuery(\"#GAuth-component\").height()});\n }\n jQuery(\"#mfa-verification-code-submit\").click(function(){\n
\ if (!validateMfaCodeAndPrivacyConsents()){\n return
false;\n }\n jQuery('#submit-mfa-verification-code-form').submit();\n
\ return false;\n });\n });\n var customerGuid
= \"0690cc1d-d23d-4412-b027-80fd4ed1c0f6\";\n var mfaMethod = \"email\";\n
\ var locale = \"\";\n var clientId = \"\";\n var codeSentTo
= \"mt*****@gmail.com\";\n </script>\n <meta charset=\"utf-8\">\n <title>Enter
MFA code for login</title>\n <meta name=\"description\" content=\"\">\n
\ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n
\ <meta http-equiv=\"cleartype\" content=\"on\">\n <meta http-equiv=\"X-UA-Compatible\"
content=\"IE=edge;\" />\n <link href=\"/sso/css/GAuth.css?20170505\" rel=\"stylesheet\"
type=\"text/css\" media=\"all\" />\n <link rel=\"stylesheet\" href=\"\"
/>\n</head>\n\n<body>\n <div id=\"GAuth-component\">\n <h2 id=\"enter-mfa-code-h2\">Enter
security code</h2>\n <input type=\"hidden\" id=\"queryString\" value=\"id=gauth-widget&amp;embedWidget=true&amp;gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
/>\n <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n\n
\ <div id=\"login-component\" class=\"blueForm-basic\">\n <div
id=\"login-state-verifymfa\">\n <td>\n \n
\ <span >Code sent to <b>mt*****@gmail.com</b></span>\n
\ </td>\n <form id=\"submit-mfa-verification-code-form\"
name=\"submit-mfa-verification-code-form\" method=\"post\" novalidate=\"novalidate\">\n
\ <div class=\"blueForm-v2\">\n <div
class=\"form-alert\">\n <div id=\"genericError\"
class=\"error\" hidden>An unexpected error has occurred.</div>\n <div
id=\"codeSentAttention\" class=\"attention\" hidden>A new code has been sent.
You can request another code in 30 seconds.</div>\n \n
\ \n <div id=\"maxLimit\"
class=\"error\" hidden=\"hidden\">You have reached the maximum amount of codes
requested. Please use a code you&#39;ve received or wait 24 hours and try
again.</div>\n \n </div>\n
\ <div class=\"formTextField\">\n <div
class=\"mfaFormLabel\">\n <label>\n <span>Security
code</span>\n <br/>\n <input
type=\"number\" pattern=\"[0-9]*\" inputmode=\"numeric\" maxlength=\"6\" id=\"mfa-code\"
name=\"mfa-code\" autofocus oninput=\"validateMfaCodeAndPrivacyConsents()\"/>\n
\ </label>\n </div>\n
\ </div>\n <br><br>\n <div>\n
\ <a href=\"https://support.garmin.com/en-US/?faq=uGHS8ZqOIhA0usBzBMdJu7\"
target=\"_blank\" id=\"havingTrouble\">Get help</a><br>\n </div>\n
\ <div id=\"requestNewCodeWrapper\" class=\"requestNewCode\">\n
\ <a href=\"#\" id=\"newCode\">Request a new code</a>\n
\ </div>\n \n \n
\ <br>\n \n <br/>\n
\ <button type=\"submit\" id=\"mfa-verification-code-submit\"
class=\"btn1\">Next</button>\n </div>\n <input
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
type=\"hidden\" name=\"_csrf\" value=\"9AF199177EE70FB2511C2DE25FE2780DEF8327EDCA5AB81C391FAF6E419E83EDF20E1EE31B76E282D8AA46124E3DC5EB1391\"
/>\n <input type=\"hidden\" name=\"fromPage\" value=\"setupEnterMfaCode\"/>\n
\ <br/>\n </form>\n </div>\n <div
class=\"clearfix\"></div> <!-- Ensure that GAuth-component div's height is
computed correctly. -->\n </div>\n </div>\n <script type=\"text/javascript\">\n
\ resizePageOnLoad(jQuery(\"#GAuth-component\").height());\n </script>\n<script>(function(){function
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83da2a5ac1ca',t:'MTc0ODkyNTY1NC4wMDAwMDA='};var
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-RAY:
- 949c83da2a5ac1ca-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Tue, 03 Jun 2025 04:40:54 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=BrVkK9BHKPEC700UIxYqeYMAufPXrsMtXb56Z5naqivj9pfj%2FKyqvweC0oLp4v4n%2BecNLLGdP4o5WUnke2Iu62u0i0gzh9hqR49I8mYeEw6ABEfR8ZFJbx0waSuNNous"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
Transfer-Encoding:
- chunked
X-Application-Context:
- casServer:cloud,prod,prod-US_Olathe:6
X-B3-Traceid:
- acd069da786e436a7d98ba4e5220bcfc
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- acd069da-786e-436a-7d98-ba4e5220bcfc
cf-cache-status:
- DYNAMIC
status:
code: 200
message: OK
- request:
body: mfa-code=123456&embed=true&_csrf=9AF199177EE70FB2511C2DE25FE2780DEF8327EDCA5AB81C391FAF6E419E83EDF20E1EE31B76E282D8AA46124E3DC5EB1391&fromPage=setupEnterMfaCode
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer SANITIZED
Connection:
- keep-alive
Content-Length:
- '160'
Content-Type:
- application/x-www-form-urlencoded
Cookie:
- SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
__cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED;
__cfruid=SANITIZED
User-Agent:
- GCM-iOS-5.7.2.1
referer:
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
method: POST
uri: https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
response:
body:
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n\n<head>\n <script
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n <script
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20231020\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/setupMfaRequiredView.js?20210319\"></script>\n
\ <script type=\"text/javascript\" src=\"/sso/js/enterMfaCode.js?20230127\"></script>\n
\ <script type=\"text/javascript\">\n var embedWidget = \"true\";\n
\ if (embedWidget == \"\") {\n embedWidget = \"\";\n }\n
\ embedWidget = (embedWidget == \"true\");\n var parent_url =
\"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n window.onload = function()
{\n ifrememberMyBrowserChecked();\n };\n\n jQuery(document).ready(
function() {\n if (!embedWidget) {\n send({'gauthHeight':
jQuery(\"#GAuth-component\").height()});\n }\n jQuery(\"#mfa-verification-code-submit\").click(function(){\n
\ if (!validateMfaCodeAndPrivacyConsents()){\n return
false;\n }\n jQuery('#submit-mfa-verification-code-form').submit();\n
\ return false;\n });\n });\n var customerGuid
= \"0690cc1d-d23d-4412-b027-80fd4ed1c0f6\";\n var mfaMethod = \"email\";\n
\ var locale = \"\";\n var clientId = \"\";\n var codeSentTo
= \"mt*****@gmail.com\";\n </script>\n <meta charset=\"utf-8\">\n <title>Enter
MFA code for login</title>\n <meta name=\"description\" content=\"\">\n
\ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n
\ <meta http-equiv=\"cleartype\" content=\"on\">\n <meta http-equiv=\"X-UA-Compatible\"
content=\"IE=edge;\" />\n <link href=\"/sso/css/GAuth.css?20170505\" rel=\"stylesheet\"
type=\"text/css\" media=\"all\" />\n <link rel=\"stylesheet\" href=\"\"
/>\n</head>\n\n<body>\n <div id=\"GAuth-component\">\n <h2 id=\"enter-mfa-code-h2\">Enter
security code</h2>\n <input type=\"hidden\" id=\"queryString\" value=\"id=gauth-widget&amp;embedWidget=true&amp;gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&amp;redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
/>\n <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n\n
\ <div id=\"login-component\" class=\"blueForm-basic\">\n <div
id=\"login-state-verifymfa\">\n <td>\n \n
\ <span >Code sent to <b>mt*****@gmail.com</b></span>\n
\ </td>\n <form id=\"submit-mfa-verification-code-form\"
name=\"submit-mfa-verification-code-form\" method=\"post\" novalidate=\"novalidate\">\n
\ <div class=\"blueForm-v2\">\n <div
class=\"form-alert\">\n <div id=\"genericError\"
class=\"error\" hidden>An unexpected error has occurred.</div>\n <div
id=\"codeSentAttention\" class=\"attention\" hidden>A new code has been sent.
You can request another code in 30 seconds.</div>\n <div
id=\"invalidCode\" class=\"error\">Invalid code. Please enter a valid code.</div>\n
\ \n <div id=\"maxLimit\"
class=\"error\" hidden=\"hidden\">You have reached the maximum amount of codes
requested. Please use a code you&#39;ve received or wait 24 hours and try
again.</div>\n \n </div>\n
\ <div class=\"formTextField\">\n <div
class=\"mfaFormLabel\">\n <label>\n <span>Security
code</span>\n <br/>\n <input
type=\"number\" pattern=\"[0-9]*\" inputmode=\"numeric\" maxlength=\"6\" id=\"mfa-code\"
name=\"mfa-code\" autofocus oninput=\"validateMfaCodeAndPrivacyConsents()\"/>\n
\ </label>\n </div>\n
\ </div>\n <br><br>\n <div>\n
\ <a href=\"https://support.garmin.com/en-US/?faq=uGHS8ZqOIhA0usBzBMdJu7\"
target=\"_blank\" id=\"havingTrouble\">Get help</a><br>\n </div>\n
\ <div id=\"requestNewCodeWrapper\" class=\"requestNewCode\">\n
\ <a href=\"#\" id=\"newCode\">Request a new code</a>\n
\ </div>\n \n \n
\ <br>\n \n <br/>\n
\ <button type=\"submit\" id=\"mfa-verification-code-submit\"
class=\"btn1\">Next</button>\n </div>\n <input
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
type=\"hidden\" name=\"_csrf\" value=\"\" />\n <input
type=\"hidden\" name=\"fromPage\" value=\"setupEnterMfaCode\"/>\n
\ <br/>\n </form>\n </div>\n <div
class=\"clearfix\"></div> <!-- Ensure that GAuth-component div's height is
computed correctly. -->\n </div>\n </div>\n <script type=\"text/javascript\">\n
\ resizePageOnLoad(jQuery(\"#GAuth-component\").height());\n </script>\n<script>(function(){function
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83dc9aa555c3',t:'MTc0ODkyNTY1NC4wMDAwMDA='};var
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
Access-Control-Request-Method, Access-Control-Request-Headers
Access-Control-Allow-Methods:
- GET,POST,OPTIONS
Access-Control-Allow-Origin:
- https://www.garmin.com
CF-RAY:
- 949c83dc9aa555c3-QRO
Connection:
- keep-alive
Content-Language:
- en
Content-Type:
- text/html;charset=UTF-8
Date:
- Tue, 03 Jun 2025 04:40:54 GMT
NEL:
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=WVjXMWAY7m%2FICrofTUaszDZoZ1kIv1%2BQTcx49UDCpdhESBjLNt9LucYPatIj%2BHOhRkqNPuM%2F65Tz1kTrR4naiCX0yEAOcMcEAh1yxyiX%2BlU7qvovsvWodipj8YHB19mH"}],"group":"cf-nel","max_age":604800}'
Server:
- cloudflare
Set-Cookie:
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
Path=SANITIZED
Transfer-Encoding:
- chunked
X-Application-Context:
- casServer:cloud,prod,prod-US_Olathe:6
X-B3-Traceid:
- 104ac62c483244cf73fb9266e97d22bb
X-Robots-Tag:
- noindex
X-Vcap-Request-Id:
- 104ac62c-4832-44cf-73fb-9266e97d22bb
cf-cache-status:
- DYNAMIC
status:
code: 200
message: OK
version: 1

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More