From 58e1f37ee4093c5b1ce52d19c272a9c302d557d7 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 14 Feb 2026 22:08:52 +0000 Subject: [PATCH 01/16] chore: util script to cleanup bad releases --- var/release-utils/unrelease.sh | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100755 var/release-utils/unrelease.sh diff --git a/var/release-utils/unrelease.sh b/var/release-utils/unrelease.sh new file mode 100755 index 0000000..6d5243c --- /dev/null +++ b/var/release-utils/unrelease.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +set -euo pipefail + +RELEASE_NAME="${1:-}" +GHCR_IMAGE="ghcr.io/olivetin/olivetin" +DOCKERHUB_IMAGE="jamesread/olivetin" + +log() { + echo "[unrelease] $*" +} + +prompt_confirm() { + local prompt="$1" + local default="${2:-n}" + if [[ "$default" == "y" ]]; then + read -r -p "$prompt [Y/n] " reply + else + read -r -p "$prompt [y/N] " reply + fi + reply="${reply:-$default}" + case "$(echo "$reply" | tr '[:upper:]' '[:lower:]')" in + y|yes) return 0 ;; + *) return 1 ;; + esac +} + +if [[ -z "$RELEASE_NAME" ]]; then + echo "Usage: $0 " >&2 + echo "Example: $0 3000.10.0" >&2 + exit 1 +fi + +log "Release to remove: $RELEASE_NAME" +log "This will: 1) Delete GitHub release, 2) Delete GitHub tag, 3) Delete GHCR image tag, 4) Delete Docker Hub image tag" +echo + +# --- GitHub release --- +log "Step 1: Delete GitHub release '$RELEASE_NAME'" +if prompt_confirm "Delete GitHub release?" "n"; then + if err=$(gh release delete "$RELEASE_NAME" --yes 2>&1); then + log "Deleted GitHub release." + else + log "Failed to delete GitHub release:" >&2 + echo "$err" | sed 's/^/[unrelease] /' >&2 + fi +else + log "Skipped GitHub release." +fi +echo + +# --- GitHub tag --- +log "Step 2: Delete GitHub tag '$RELEASE_NAME'" +if prompt_confirm "Delete GitHub tag?" "n"; then + repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null) || repo="olivetin/olivetin" + if err=$(gh api -X DELETE "repos/$repo/git/refs/tags/$RELEASE_NAME" 2>&1); then + log "Deleted GitHub tag." + else + log "Failed to delete GitHub tag:" >&2 + echo "$err" | sed 's/^/[unrelease] /' >&2 + fi +else + log "Skipped GitHub tag." +fi +echo + +# --- GHCR --- +log "Step 3: Delete GHCR image tag $GHCR_IMAGE:$RELEASE_NAME" +if prompt_confirm "Delete GHCR container image version?" "n"; then + list_err=$(gh api "orgs/olivetin/packages/container/olivetin/versions" --jq ".[] | select(.metadata.container.tags[]? == \"$RELEASE_NAME\") | .id" 2>&1) || true + version_id=$(echo "$list_err" | head -1) + if [[ -z "$version_id" || ! "$version_id" =~ ^[0-9]+$ ]]; then + log "Could not resolve GHCR version for tag '$RELEASE_NAME' (need read:packages scope, or tag may not exist)." >&2 + if [[ "$list_err" == *"message"* ]]; then + msg=$(echo "$list_err" | sed -n 's/.*"message":"\([^"]*\)".*/\1/p' | head -1) + [[ -n "$msg" ]] && log " $msg" >&2 + fi + else + if err=$(gh api -X DELETE "orgs/olivetin/packages/container/olivetin/versions/$version_id" 2>&1); then + log "Deleted GHCR version (id: $version_id)." + else + log "Failed to delete GHCR version:" >&2 + echo "$err" | sed 's/^/[unrelease] /' >&2 + fi + fi +else + log "Skipped GHCR." +fi +echo + +# --- Docker Hub --- +log "Step 4: Delete Docker Hub image tag $DOCKERHUB_IMAGE:$RELEASE_NAME" +if prompt_confirm "Delete Docker Hub image tag? (requires DOCKERHUB_TOKEN)" "n"; then + if [[ -z "${DOCKERHUB_TOKEN:-}" ]]; then + log "DOCKERHUB_TOKEN is not set. Get a token from https://hub.docker.com/settings/security and run: DOCKERHUB_TOKEN=xxx $0 $RELEASE_NAME" >&2 + log "Skipped Docker Hub." + else + status=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: Bearer $DOCKERHUB_TOKEN" \ + "https://hub.docker.com/v2/repositories/$DOCKERHUB_IMAGE/tags/$RELEASE_NAME/") + if [[ "$status" == "204" ]]; then + log "Deleted Docker Hub tag." + else + log "Docker Hub delete returned HTTP $status (tag may not exist or token invalid)." >&2 + fi + fi +else + log "Skipped Docker Hub." +fi + +log "Done." From 544515c2a6bd44454bbdb86829e5b63da798fd71 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 15 Feb 2026 00:19:36 +0000 Subject: [PATCH 02/16] chore: #829, json support in template engine --- service/internal/tpl/templates.go | 15 ++++- service/internal/tpl/templates_test.go | 81 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 service/internal/tpl/templates_test.go diff --git a/service/internal/tpl/templates.go b/service/internal/tpl/templates.go index 580b907..cb9978f 100644 --- a/service/internal/tpl/templates.go +++ b/service/internal/tpl/templates.go @@ -1,6 +1,7 @@ package tpl import ( + "encoding/json" "fmt" "regexp" "strings" @@ -12,8 +13,20 @@ import ( log "github.com/sirupsen/logrus" ) +func jsonFunc(v any) (string, error) { + if v == nil { + return "null", nil + } + data, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(data), nil +} + var tpl = template.New("tpl"). - Option("missingkey=error") + Option("missingkey=error"). + Funcs(template.FuncMap{"Json": jsonFunc}) type olivetinInfo struct { Build *installationinfo.BuildInfo diff --git a/service/internal/tpl/templates_test.go b/service/internal/tpl/templates_test.go new file mode 100644 index 0000000..d46f902 --- /dev/null +++ b/service/internal/tpl/templates_test.go @@ -0,0 +1,81 @@ +package tpl + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/OliveTin/OliveTin/internal/entities" + "github.com/stretchr/testify/assert" +) + +func TestParseTemplateWithActionContext_Json(t *testing.T) { + tests := []struct { + name string + source string + ent *entities.Entity + args map[string]string + expectedOutput string + expectError bool + checkJsonOnly bool + }{ + { + name: "Arguments piped to Json", + source: `echo {{ .Arguments | Json }}`, + ent: nil, + args: map[string]string{"value": "true", "ot_username": "alice"}, + expectedOutput: `echo `, + expectError: false, + checkJsonOnly: true, + }, + { + name: "CurrentEntity field piped to Json", + source: `curl -d {{ .CurrentEntity.foo.bar | Json }}`, + ent: &entities.Entity{Data: map[string]any{"foo": map[string]any{"bar": "baz"}}}, + args: nil, + expectedOutput: `curl -d "baz"`, + expectError: false, + }, + { + name: "CurrentEntity nested object piped to Json", + source: `curl --json -d {{ .CurrentEntity.payload | Json }}`, + ent: &entities.Entity{Data: map[string]any{"payload": map[string]any{"on": true}}}, + args: nil, + expectedOutput: `curl --json -d {"on":true}`, + expectError: false, + }, + { + name: "Single argument value as Json", + source: `echo {{ .Arguments.value | Json }}`, + ent: nil, + args: map[string]string{"value": "hello"}, + expectedOutput: `echo "hello"`, + expectError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := ParseTemplateWithActionContext(tt.source, tt.ent, tt.args) + if tt.expectError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + if tt.checkJsonOnly { + prefix := strings.TrimSuffix(tt.expectedOutput, " ") + assert.True(t, strings.HasPrefix(output, prefix), "output %q should start with %q", output, prefix) + jsonPart := strings.TrimPrefix(output, prefix) + jsonPart = strings.TrimSpace(jsonPart) + var decoded map[string]string + err := json.Unmarshal([]byte(jsonPart), &decoded) + assert.NoError(t, err) + for k, v := range tt.args { + assert.Equal(t, v, decoded[k], "decoded JSON should contain %s=%s", k, v) + } + assert.Len(t, decoded, len(tt.args)) + } else { + assert.Equal(t, tt.expectedOutput, output) + } + }) + } +} From 903fd15bdb28695d466018faee95d8e3aceb8945 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 15 Feb 2026 00:31:26 +0000 Subject: [PATCH 03/16] fix: #860 respect popupOnStart even if the action has args. --- frontend/resources/vue/views/ArgumentForm.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index b1a430c..f749daf 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -73,6 +73,7 @@ const confirmationChecked = ref(false) const hasConfirmation = ref(false) const formErrors = ref({}) const actionArguments = ref([]) +const popupOnStart = ref('') // Computed properties @@ -93,6 +94,7 @@ async function setup() { title.value = action.title icon.value = action.icon + popupOnStart.value = action.popupOnStart || '' actionArguments.value = action.arguments || [] argValues.value = {} formErrors.value = {} @@ -418,7 +420,11 @@ async function handleSubmit(event) { try { const response = await startAction(argvs) - router.push(`/logs/${response.executionTrackingId}`) + if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) { + router.push(`/logs/${response.executionTrackingId}`) + } else { + router.back() + } } catch (err) { console.error('Failed to start action:', err) } From 7dcbca31fcb9ccb896fe315d3cf0a181fd609eff Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 15 Feb 2026 01:03:22 +0000 Subject: [PATCH 04/16] fix: #803 enable custom JS (again!) --- frontend/main.js | 8 ++++++++ frontend/resources/vue/App.vue | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/frontend/main.js b/frontend/main.js index a434cce..5574e46 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -61,6 +61,14 @@ async function initClient () { window.client = createClient(OliveTinApiService, transport) window.initResponse = await window.client.init({}) + if (window.initResponse.enableCustomJs) { + const script = document.createElement('script') + script.src = '/custom-webui/custom.js' + script.async = true + script.id = 'olivetin-custom-js' + document.head.appendChild(script) + } + const i18nSettings = createI18n({ legacy: false, locale: getSelectedLanguage(), diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 5c1fa52..5648191 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -231,6 +231,7 @@ function updateHeaderFromInit() { } applyStyleMods() + loadCustomJsIfEnabled() renderNavigation() applyTheme() @@ -369,6 +370,17 @@ function applyTheme() { } } +function loadCustomJsIfEnabled() { + if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) { + return + } + const script = document.createElement('script') + script.src = '/custom-webui/custom.js' + script.async = true + script.id = 'olivetin-custom-js' + document.head.appendChild(script) +} + function applyStyleMods() { if (!window.initResponse || !window.initResponse.styleMods) { return From 4700f998e3db63955fb93ec73e99052b7a978028 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 05:49:56 +0000 Subject: [PATCH 05/16] chore(deps): bump wait-on from 9.0.3 to 9.0.4 in /integration-tests Bumps [wait-on](https://github.com/jeffbski/wait-on) from 9.0.3 to 9.0.4. - [Release notes](https://github.com/jeffbski/wait-on/releases) - [Commits](https://github.com/jeffbski/wait-on/compare/v9.0.3...v9.0.4) --- updated-dependencies: - dependency-name: wait-on dependency-version: 9.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- integration-tests/package-lock.json | 49 +++++++++++++++-------------- integration-tests/package.json | 2 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json index 1251464..12d651e 100644 --- a/integration-tests/package-lock.json +++ b/integration-tests/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "AGPL-3.0-only", "dependencies": { - "wait-on": "^9.0.3" + "wait-on": "^9.0.4" }, "devDependencies": { "chai": "^6.2.2", @@ -209,9 +209,9 @@ "license": "BSD-3-Clause" }, "node_modules/@hapi/tlds": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz", - "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.5.tgz", + "integrity": "sha512-Vq/1gnIIsvFUpKlDdfrPd/ssHDpAyBP/baVukh3u2KSG2xoNjsnRNjQiPmuyPPGqsn1cqVWWhtZHfOBaLizFRQ==", "license": "BSD-3-Clause", "engines": { "node": ">=14.0.0" @@ -321,9 +321,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@types/estree": { @@ -421,13 +421,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -1437,9 +1437,9 @@ } }, "node_modules/joi": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", - "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", "license": "BSD-3-Clause", "dependencies": { "@hapi/address": "^5.1.1", @@ -1547,9 +1547,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -2210,14 +2211,14 @@ "dev": true }, "node_modules/wait-on": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz", - "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", + "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", "license": "MIT", "dependencies": { - "axios": "^1.13.2", - "joi": "^18.0.1", - "lodash": "^4.17.21", + "axios": "^1.13.5", + "joi": "^18.0.2", + "lodash": "^4.17.23", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, diff --git a/integration-tests/package.json b/integration-tests/package.json index 81f10a7..21bbcc8 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -17,6 +17,6 @@ "selenium-webdriver": "^4.40.0" }, "dependencies": { - "wait-on": "^9.0.3" + "wait-on": "^9.0.4" } } From ea4cdf9df2766bb9ba267d0edcc936a9fc896e6a Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 19 Feb 2026 20:33:24 +0000 Subject: [PATCH 06/16] fix: Logs page pagination (#883) --- .../scripts/gen/olivetin/api/v1/olivetin_pb.d.ts | 8 ++++++++ .../scripts/gen/olivetin/api/v1/olivetin_pb.js | 3 ++- frontend/resources/vue/views/ActionDetailsView.vue | 2 +- frontend/resources/vue/views/LogsListView.vue | 4 ++-- proto/olivetin/api/v1/olivetin.proto | 1 + service/gen/olivetin/api/v1/olivetin.pb.go | 13 +++++++++++-- service/internal/api/api.go | 11 ++++++++++- service/internal/api/dashboard_entities.go | 2 +- 8 files changed, 36 insertions(+), 8 deletions(-) diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index b28fdd9..8e308fe 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -495,6 +495,13 @@ export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> & * @generated from field: string date_filter = 2; */ dateFilter: string; + + /** + * Number of logs per page (optional; server default used if 0 or unset) + * + * @generated from field: int64 page_size = 3; + */ + pageSize: bigint; }; /** @@ -1846,3 +1853,4 @@ export declare const OliveTinApiService: GenService<{ output: typeof EntitySchema; }, }>; + diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index 02156ca..4e6e723 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLcAQoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiQgoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSJ9ChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJImcKGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjsKDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCSKaAwoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJIpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyJlChtWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QSDQoFdmFsdWUYASABKAkSDAoEdHlwZRgCIAEoCRISCgpiaW5kaW5nX2lkGAMgASgJEhUKDWFyZ3VtZW50X25hbWUYBCABKAkiQgocVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZRINCgV2YWxpZBgBIAEoCBITCgtkZXNjcmlwdGlvbhgCIAEoCSI2ChVXYXRjaEV4ZWN1dGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIiYKFFdhdGNoRXhlY3V0aW9uVXBkYXRlEg4KBnVwZGF0ZRgBIAEoCSJKChZFeGVjdXRpb25TdGF0dXNSZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIRCglhY3Rpb25faWQYAiABKAkiRwoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSISChBTb3NSZXBvcnRSZXF1ZXN0IiIKEVNvc1JlcG9ydFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJIhEKD0R1bXBWYXJzUmVxdWVzdCKVAQoQRHVtcFZhcnNSZXNwb25zZRINCgVhbGVydBgBIAEoCRJBCghjb250ZW50cxgCIAMoCzIvLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlLkNvbnRlbnRzRW50cnkaLwoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjsKDERlYnVnQmluZGluZxIUCgxhY3Rpb25fdGl0bGUYASABKAkSFQoNZW50aXR5X3ByZWZpeBgCIAEoCSIeChxEdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Is4BCh1EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZRINCgVhbGVydBgBIAEoCRJOCghjb250ZW50cxgCIAMoCzI8Lm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZS5Db250ZW50c0VudHJ5Gk4KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0ub2xpdmV0aW4uYXBpLnYxLkRlYnVnQmluZGluZzoCOAEiEgoQR2V0UmVhZHl6UmVxdWVzdCIjChFHZXRSZWFkeXpSZXNwb25zZRIOCgZzdGF0dXMYASABKAkiFAoSRXZlbnRTdHJlYW1SZXF1ZXN0IuMCChNFdmVudFN0cmVhbVJlc3BvbnNlEj0KDmVudGl0eV9jaGFuZ2VkGAIgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50RW50aXR5Q2hhbmdlZEgAEj0KDmNvbmZpZ19jaGFuZ2VkGAMgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50Q29uZmlnQ2hhbmdlZEgAEkUKEmV4ZWN1dGlvbl9maW5pc2hlZBgEIAEoCzInLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvbkZpbmlzaGVkSAASQwoRZXhlY3V0aW9uX3N0YXJ0ZWQYBSABKAsyJi5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25TdGFydGVkSAASOQoMb3V0cHV0X2NodW5rGAYgASgLMiEub2xpdmV0aW4uYXBpLnYxLkV2ZW50T3V0cHV0Q2h1bmtIAEIHCgVldmVudCJBChBFdmVudE91dHB1dENodW5rEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZvdXRwdXQYAiABKAkiFAoSRXZlbnRFbnRpdHlDaGFuZ2VkIhQKEkV2ZW50Q29uZmlnQ2hhbmdlZCJGChZFdmVudEV4ZWN1dGlvbkZpbmlzaGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJFChVFdmVudEV4ZWN1dGlvblN0YXJ0ZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjIKEUtpbGxBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSJtChJLaWxsQWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBmtpbGxlZBgCIAEoCBIZChFhbHJlYWR5X2NvbXBsZXRlZBgDIAEoCBINCgVmb3VuZBgEIAEoCCI7ChVMb2NhbFVzZXJMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiKQoWTG9jYWxVc2VyTG9naW5SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIicKE1Bhc3N3b3JkSGFzaFJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkiJAoUUGFzc3dvcmRIYXNoUmVzcG9uc2USDAoEaGFzaBgBIAEoCSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlIhcKFUdldERpYWdub3N0aWNzUmVxdWVzdCJFChZHZXREaWFnbm9zdGljc1Jlc3BvbnNlEhMKC1NzaEZvdW5kS2V5GAEgASgJEhYKDlNzaEZvdW5kQ29uZmlnGAIgASgJIg0KC0luaXRSZXF1ZXN0IusFCgxJbml0UmVzcG9uc2USEgoKc2hvd0Zvb3RlchgBIAEoCBIWCg5zaG93TmF2aWdhdGlvbhgCIAEoCBIXCg9zaG93TmV3VmVyc2lvbnMYAyABKAgSGAoQYXZhaWxhYmxlVmVyc2lvbhgEIAEoCRIWCg5jdXJyZW50VmVyc2lvbhgFIAEoCRIRCglwYWdlVGl0bGUYBiABKAkSHgoWc2VjdGlvbk5hdmlnYXRpb25TdHlsZRgHIAEoCRIaChJkZWZhdWx0SWNvbkZvckJhY2sYCCABKAkSFgoOZW5hYmxlQ3VzdG9tSnMYCSABKAgSFAoMYXV0aExvZ2luVXJsGAogASgJEhYKDmF1dGhMb2NhbExvZ2luGAsgASgIEhEKCXN0eWxlTW9kcxgMIAMoCRI4Cg9vQXV0aDJQcm92aWRlcnMYDSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuT0F1dGgyUHJvdmlkZXISOAoPYWRkaXRpb25hbExpbmtzGA4gAygLMh8ub2xpdmV0aW4uYXBpLnYxLkFkZGl0aW9uYWxMaW5rEhYKDnJvb3REYXNoYm9hcmRzGA8gAygJEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgQIAEoCRIjChthdXRoZW50aWNhdGVkX3VzZXJfcHJvdmlkZXIYESABKAkSOgoQZWZmZWN0aXZlX3BvbGljeRgSIAEoCzIgLm9saXZldGluLmFwaS52MS5FZmZlY3RpdmVQb2xpY3kSFgoOYmFubmVyX21lc3NhZ2UYEyABKAkSEgoKYmFubmVyX2NzcxgUIAEoCRIYChBzaG93X2RpYWdub3N0aWNzGBUgASgIEhUKDXNob3dfbG9nX2xpc3QYFiABKAgSFgoObG9naW5fcmVxdWlyZWQYFyABKAgSGAoQYXZhaWxhYmxlX3RoZW1lcxgYIAMoCRIkChxzaG93X25hdmlnYXRlX29uX3N0YXJ0X2ljb25zGBkgASgIIiwKDkFkZGl0aW9uYWxMaW5rEg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCSI6Cg5PQXV0aDJQcm92aWRlchINCgV0aXRsZRgBIAEoCRIMCgRpY29uGAMgASgJEgsKA2tleRgEIAEoCSItChdHZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJIkMKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uIhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMugSChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASdQoUVmFsaWRhdGVBcmd1bWVudFR5cGUSLC5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Gi0ub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2UiABJLCgZXaG9BbUkSHi5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVxdWVzdBofLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXNwb25zZSIAElQKCVNvc1JlcG9ydBIhLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlc3BvbnNlIgASUQoIRHVtcFZhcnMSIC5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXF1ZXN0GiEub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UiABJ4ChVEdW1wUHVibGljSWRBY3Rpb25NYXASLS5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdBouLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZSIAElQKCUdldFJlYWR5ehIhLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlc3BvbnNlIgASYwoOTG9jYWxVc2VyTG9naW4SJi5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVzcG9uc2UiABJdCgxQYXNzd29yZEhhc2gSJC5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXNwb25zZSIAEksKBkxvZ291dBIeLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlc3BvbnNlIgASXAoLRXZlbnRTdHJlYW0SIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVzcG9uc2UiADABEmMKDkdldERpYWdub3N0aWNzEiYub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1Jlc3BvbnNlIgASRQoESW5pdBIcLm9saXZldGluLmFwaS52MS5Jbml0UmVxdWVzdBodLm9saXZldGluLmFwaS52MS5Jbml0UmVzcG9uc2UiABJpChBHZXRBY3Rpb25CaW5kaW5nEigub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXNwb25zZSIAEloKC0dldEVudGl0aWVzEiMub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1Jlc3BvbnNlIgASSQoJR2V0RW50aXR5EiEub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0eVJlcXVlc3QaFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5IgBCOFo2Z2l0aHViLmNvbS9PbGl2ZVRpbi9PbGl2ZVRpbi9nZW4vb2xpdmV0aW4vYXBpL3YxO2FwaXYxYgZwcm90bzM"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLcAQoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiQgoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSJ9ChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJImcKGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ik4KDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCRIRCglwYWdlX3NpemUYAyABKAMimgMKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCSKRAQoPR2V0TG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiPwoUR2V0QWN0aW9uTG9nc1JlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEhQKDHN0YXJ0X29mZnNldBgCIAEoAyKXAQoVR2V0QWN0aW9uTG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJIkcKF0V4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIPCg1XaG9BbUlSZXF1ZXN0ImwKDldob0FtSVJlc3BvbnNlEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgBIAEoCRIRCgl1c2VyZ3JvdXAYAiABKAkSEAoIcHJvdmlkZXIYAyABKAkSDAoEYWNscxgEIAMoCRILCgNzaWQYBSABKAkiEgoQU29zUmVwb3J0UmVxdWVzdCIiChFTb3NSZXBvcnRSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCLjAgoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSJDChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbiIUChJHZXRFbnRpdGllc1JlcXVlc3QiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiJpChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLoEgoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJUCglTb3NSZXBvcnQSIS5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z"); /** * Describes the message olivetin.api.v1.Action. @@ -491,3 +491,4 @@ export const RestartActionRequestSchema = /*@__PURE__*/ */ export const OliveTinApiService = /*@__PURE__*/ serviceDesc(file_olivetin_api_v1_olivetin, 0); + diff --git a/frontend/resources/vue/views/ActionDetailsView.vue b/frontend/resources/vue/views/ActionDetailsView.vue index 7b51f9d..9d8b31e 100644 --- a/frontend/resources/vue/views/ActionDetailsView.vue +++ b/frontend/resources/vue/views/ActionDetailsView.vue @@ -78,7 +78,7 @@ - diff --git a/frontend/resources/vue/views/LogsListView.vue b/frontend/resources/vue/views/LogsListView.vue index b1a93b5..e6278f0 100644 --- a/frontend/resources/vue/views/LogsListView.vue +++ b/frontend/resources/vue/views/LogsListView.vue @@ -68,7 +68,7 @@ - @@ -150,6 +150,7 @@ async function fetchLogs() { const args = { "startOffset": BigInt(startOffset), + "pageSize": BigInt(pageSize.value), } // Add date filter if selected @@ -160,7 +161,6 @@ async function fetchLogs() { const response = await window.client.getLogs(args) logs.value = response.logs - pageSize.value = Number(response.pageSize) || 0 totalCount.value = Number(response.totalCount) || 0 } catch (err) { console.error('Failed to fetch logs:', err) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index cc12212..8554f6b 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -121,6 +121,7 @@ message StartActionByGetAndWaitResponse { message GetLogsRequest{ int64 start_offset = 1; string date_filter = 2; // Optional date filter in YYYY-MM-DD format + int64 page_size = 3; // Number of logs per page (optional; server default used if 0 or unset) }; message LogEntry { diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index 5614c01..682ca2c 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -1105,6 +1105,7 @@ type GetLogsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` DateFilter string `protobuf:"bytes,2,opt,name=date_filter,json=dateFilter,proto3" json:"date_filter,omitempty"` // Optional date filter in YYYY-MM-DD format + PageSize int64 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // Number of logs per page (optional; server default used if 0 or unset) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1153,6 +1154,13 @@ func (x *GetLogsRequest) GetDateFilter() string { return "" } +func (x *GetLogsRequest) GetPageSize() int64 { + if x != nil { + return x.PageSize + } + return 0 +} + type LogEntry struct { state protoimpl.MessageState `protogen:"open.v1"` DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"` @@ -3972,11 +3980,12 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x1eStartActionByGetAndWaitRequest\x12\x1b\n" + "\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" + "\x1fStartActionByGetAndWaitResponse\x126\n" + - "\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"T\n" + + "\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"q\n" + "\x0eGetLogsRequest\x12!\n" + "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" + "\vdate_filter\x18\x02 \x01(\tR\n" + - "dateFilter\"\x89\x05\n" + + "dateFilter\x12\x1b\n" + + "\tpage_size\x18\x03 \x01(\x03R\bpageSize\"\x89\x05\n" + "\bLogEntry\x12)\n" + "\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" + "\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" + diff --git a/service/internal/api/api.go b/service/internal/api/api.go index faf58d1..68ef978 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -506,7 +506,16 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetL if req.Msg.DateFilter != "" { dateFilter = req.Msg.DateFilter } - logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize, dateFilter) + pageSize := api.cfg.LogHistoryPageSize + if req.Msg.GetPageSize() > 0 { + pageSize = req.Msg.GetPageSize() + if pageSize < 10 { + pageSize = 10 + } else if pageSize > 100 { + pageSize = 100 + } + } + logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, pageSize, dateFilter) for _, le := range logEntries { ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user)) } diff --git a/service/internal/api/dashboard_entities.go b/service/internal/api/dashboard_entities.go index 7bec464..2ddf964 100644 --- a/service/internal/api/dashboard_entities.go +++ b/service/internal/api/dashboard_entities.go @@ -57,7 +57,7 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent for _, subitem := range contents { c := cloneItem(subitem, ent, entityType, rr) - log.Infof("cloneItem: %+v", c) + log.Tracef("cloneItem: %+v", c) if c != nil { ret = append(ret, c) From 4bbd2eab153287dc744ad061c58af7693f0c3ddc Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 22 Feb 2026 10:19:08 +0000 Subject: [PATCH 07/16] security: GHSA-49gm-hh7w-wfvf --- SECURITY.md | 29 +++++- service/internal/executor/arguments.go | 2 +- service/internal/executor/arguments_test.go | 34 +++++++ service/internal/executor/executor.go | 27 ++++++ service/internal/executor/executor_test.go | 100 ++++++++++++++++++++ service/internal/webhooks/handler.go | 17 +++- service/internal/webhooks/handler_test.go | 32 +++++++ 7 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 service/internal/webhooks/handler_test.go diff --git a/SECURITY.md b/SECURITY.md index f0a4010..df1c506 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,12 +2,35 @@ ## Supported Versions -Currently, only the `main` branch is "supported". +The following branches are currently being supported with security updates: | Version | Supported | | ------- | ------------------ | -| `main` | :white_check_mark: | +| `main` (3k release branch) | :white_check_mark: | +| `release/2k` (2k release branch) | :white_check_mark: | + +To understand more about 2k vs 3k, see the following docs; https://docs.olivetin.app/upgrade/2k3k.html + +## OliveTin *is* a remote code execution (RCE) "tool" + +The very purpose of OliveTin is to allow users to execute commands remotely on a machine. + +This means that, by design, OliveTin has might higher potential to be used for remote code execution (RCE), and any security vulnerabilities that do occour have the potential to be much more severe than in other types of software. + +We hope that you understand that while the project goes to great aims to be safe, and mitigate, that security vulnerabilities are inevitable, as they are with all software of all sizes - like Kubernetes, the Kernel, etc - and OliveTin has substancially less resources than those projects. + +With that being said, OliveTin tries to follow examples of best practice, so judge the project not on if/when it has security issues, but how security issues are responded to as the measure of quality. + +This is why we take security very seriously, and why we encourage responsible disclosure practices when reporting vulnerabilities. ## Reporting a Vulnerability -Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit. +Please use responsible disclosure practices when reporting a vulnerability. **You will receive full credit for your discovery**, and we will work with you to ensure that the issue is resolved as quickly as **possible**. Please note that only James Read has access to security issues at the moment, so please be patient and understanding if you do not receive an immediate response. + +* **Option A (preferred)**: GitHub Security Advisories, which allows you to report a vulnerability privately and securely. You can find the option to report a security issue in the "Issues" tab of this repository, and then select "Report a security vulnerability". This will allow you to provide details about the vulnerability without making it public. + +* **Option B**: Please email `contact@jread.com` for responsible disclosure. + +## Disclosure of how vulnerabilities were found + +It is incredibly useful to not just patch security vulnerabilities, but also to understand how they were found. If you are able to share this information, it can help us and the community to better understand potential attack vectors and improve the overall security of the project. diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index a63eb0b..655ecc4 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -310,7 +310,7 @@ func checkShellArgumentSafety(action *config.Action) error { if action.Shell == "" { return nil } - unsafe := map[string]struct{}{"url": {}, "email": {}, "raw_string_multiline": {}, "very_dangerous_raw_string": {}} + unsafe := map[string]struct{}{"url": {}, "email": {}, "raw_string_multiline": {}, "very_dangerous_raw_string": {}, "password": {}} for _, arg := range action.Arguments { if _, bad := unsafe[arg.Type]; bad { return fmt.Errorf("unsafe argument type '%s' cannot be used with Shell execution. Use 'exec' instead. See https://docs.olivetin.app/action_execution/shellvsexec.html", arg.Type) diff --git a/service/internal/executor/arguments_test.go b/service/internal/executor/arguments_test.go index 877b10c..8cf03a9 100644 --- a/service/internal/executor/arguments_test.go +++ b/service/internal/executor/arguments_test.go @@ -302,6 +302,40 @@ func TestCheckShellArgumentSafetyWithSafeTypes(t *testing.T) { assert.Nil(t, err) } +func TestCheckShellArgumentSafetyWithPassword(t *testing.T) { + a1 := config.Action{ + Title: "Auth with password", + Shell: "somecommand --password '{{password}}'", + Arguments: []config.ActionArgument{ + { + Name: "password", + Type: "password", + }, + }, + } + + err := checkShellArgumentSafety(&a1) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "unsafe argument type 'password' cannot be used with Shell execution") + assert.Contains(t, err.Error(), "https://docs.olivetin.app/action_execution/shellvsexec.html") +} + +func TestCheckShellArgumentSafetyWithPasswordAndExec(t *testing.T) { + a1 := config.Action{ + Title: "Auth with password via exec", + Exec: []string{"somecommand", "--password", "{{password}}"}, + Arguments: []config.ActionArgument{ + { + Name: "password", + Type: "password", + }, + }, + } + + err := checkShellArgumentSafety(&a1) + assert.Nil(t, err) +} + func TestTypeSafetyCheckUrl(t *testing.T) { assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com") assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments") diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 274d4b1..5fe2dec 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -664,6 +664,7 @@ func stepParseArgs(req *ExecutionRequest) bool { return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil")) } + filterToDefinedArgumentsOnly(req) mangleInvalidArgumentValues(req) if hasExec(req) { @@ -686,6 +687,9 @@ func handleExecBranch(req *ExecutionRequest) bool { } func handleShellBranch(req *ExecutionRequest) bool { + if hasWebhookTag(req) { + return fail(req, fmt.Errorf("webhooks cannot use Shell execution; use exec instead. See https://docs.olivetin.app/action_execution/shellvsexec.html")) + } if err := checkShellArgumentSafety(req.Binding.Action); err != nil { return fail(req, err) } @@ -707,6 +711,29 @@ func ensureArgumentMap(req *ExecutionRequest) { } } +func filterToDefinedArgumentsOnly(req *ExecutionRequest) { + definedNames := make(map[string]struct{}) + for _, arg := range req.Binding.Action.Arguments { + definedNames[arg.Name] = struct{}{} + } + filtered := make(map[string]string) + for k, v := range req.Arguments { + if _, ok := definedNames[k]; ok || strings.HasPrefix(k, "ot_") { + filtered[k] = v + } + } + req.Arguments = filtered +} + +func hasWebhookTag(req *ExecutionRequest) bool { + for _, tag := range req.Tags { + if tag == "webhook" { + return true + } + } + return false +} + func injectSystemArgs(req *ExecutionRequest) { req.Arguments["ot_executionTrackingId"] = req.TrackingID req.Arguments["ot_username"] = req.AuthenticatedUser.Username diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index f9efefe..00a4535 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -295,3 +295,103 @@ func TestMangleInvalidArgumentValues(t *testing.T) { assert.Equal(t, req.logEntry.Output, "The date is: 1990-01-10T12:00:00\n", "Date should be mangled to a valid format") } + +func TestWebhookRejectsShellExecution(t *testing.T) { + cfg := config.DefaultConfig() + e := DefaultExecutor(cfg) + a1 := &config.Action{ + Title: "Webhook Shell Reject", + Shell: "echo '{{ msg }}'", + Arguments: []config.ActionArgument{ + {Name: "msg", Type: "ascii"}, + }, + } + cfg.Actions = append(cfg.Actions, a1) + cfg.Sanitize() + e.RebuildActionMap() + + req := ExecutionRequest{ + Tags: []string{"webhook"}, + AuthenticatedUser: auth.UserFromSystem(cfg, "webhook"), + Cfg: cfg, + Arguments: map[string]string{"msg": "hello"}, + Binding: e.FindBindingWithNoEntity(a1), + } + + wg, _ := e.ExecRequest(&req) + wg.Wait() + + assert.NotNil(t, req.logEntry) + assert.Equal(t, int32(-1337), req.logEntry.ExitCode) + assert.Contains(t, req.logEntry.Output, "webhooks cannot use Shell execution") +} + +func TestWebhookAllowsExecExecution(t *testing.T) { + cfg := config.DefaultConfig() + e := DefaultExecutor(cfg) + a1 := &config.Action{ + Title: "Webhook Exec OK", + Exec: []string{"echo", "{{ msg }}"}, + Arguments: []config.ActionArgument{ + {Name: "msg", Type: "ascii"}, + }, + } + cfg.Actions = append(cfg.Actions, a1) + cfg.Sanitize() + e.RebuildActionMap() + + req := ExecutionRequest{ + Tags: []string{"webhook"}, + AuthenticatedUser: auth.UserFromSystem(cfg, "webhook"), + Cfg: cfg, + Arguments: map[string]string{"msg": "hello"}, + Binding: e.FindBindingWithNoEntity(a1), + } + + wg, _ := e.ExecRequest(&req) + wg.Wait() + + assert.NotNil(t, req.logEntry) + assert.Equal(t, int32(0), req.logEntry.ExitCode) + assert.Contains(t, req.logEntry.Output, "hello") +} + +func TestFilterToDefinedArgumentsOnly(t *testing.T) { + req := newExecRequest() + req.Binding.Action = &config.Action{ + Title: "Filter test", + Shell: "echo '{{ name }}'", + Arguments: []config.ActionArgument{ + {Name: "name", Type: "ascii"}, + }, + } + req.Arguments = map[string]string{ + "name": "Alice", + "webhook_path": "/malicious/$(id)", + "extra_undefined": "ignored", + } + + filterToDefinedArgumentsOnly(req) + + assert.Equal(t, "Alice", req.Arguments["name"]) + assert.Empty(t, req.Arguments["webhook_path"]) + assert.Empty(t, req.Arguments["extra_undefined"]) +} + +func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) { + req := newExecRequest() + req.Binding.Action = &config.Action{ + Title: "Filter test", + Shell: "echo test", + Arguments: []config.ActionArgument{}, + } + req.Arguments = map[string]string{ + "ot_executionTrackingId": "track-123", + "ot_username": "webhook", + } + + filterToDefinedArgumentsOnly(req) + + assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"]) + assert.Equal(t, "webhook", req.Arguments["ot_username"]) +} diff --git a/service/internal/webhooks/handler.go b/service/internal/webhooks/handler.go index a530690..92cdd8a 100644 --- a/service/internal/webhooks/handler.go +++ b/service/internal/webhooks/handler.go @@ -150,13 +150,28 @@ func (h *WebhookHandler) executeAction(action *config.Action, args map[string]st return } + definedArgs := filterToDefinedArguments(args, action) req := &executor.ExecutionRequest{ Binding: binding, Cfg: h.cfg, Tags: []string{"webhook"}, - Arguments: args, + Arguments: definedArgs, AuthenticatedUser: auth.UserFromSystem(h.cfg, "webhook"), } h.executor.ExecRequest(req) } + +func filterToDefinedArguments(args map[string]string, action *config.Action) map[string]string { + definedNames := make(map[string]struct{}) + for _, arg := range action.Arguments { + definedNames[arg.Name] = struct{}{} + } + filtered := make(map[string]string) + for k, v := range args { + if _, ok := definedNames[k]; ok { + filtered[k] = v + } + } + return filtered +} diff --git a/service/internal/webhooks/handler_test.go b/service/internal/webhooks/handler_test.go new file mode 100644 index 0000000..48a2108 --- /dev/null +++ b/service/internal/webhooks/handler_test.go @@ -0,0 +1,32 @@ +package webhooks + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + config "github.com/OliveTin/OliveTin/internal/config" +) + +func TestFilterToDefinedArguments(t *testing.T) { + action := &config.Action{ + Arguments: []config.ActionArgument{ + {Name: "repo", Type: "ascii_identifier"}, + {Name: "branch", Type: "ascii_identifier"}, + }, + } + args := map[string]string{ + "repo": "my-repo", + "branch": "main", + "webhook_path": "/deploy/prod", + "webhook_header_x_custom": "malicious", + } + + filtered := filterToDefinedArguments(args, action) + + assert.Equal(t, "my-repo", filtered["repo"]) + assert.Equal(t, "main", filtered["branch"]) + assert.Empty(t, filtered["webhook_path"]) + assert.Empty(t, filtered["webhook_header_x_custom"]) + assert.Len(t, filtered, 2) +} From c4a8eadd3fa17f5d37a5f0f72dd30943674f7ea3 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 25 Feb 2026 23:15:57 +0000 Subject: [PATCH 08/16] chore: Fix broken test configs --- integration-tests/tests/checkbox/config.yaml | 1 + integration-tests/tests/datetime/config.yaml | 1 + integration-tests/tests/suggestionsBrowserKey/config.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/integration-tests/tests/checkbox/config.yaml b/integration-tests/tests/checkbox/config.yaml index 3ca7060..589bb7b 100644 --- a/integration-tests/tests/checkbox/config.yaml +++ b/integration-tests/tests/checkbox/config.yaml @@ -3,6 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false +popupOnStart: execution-dialog actions: - title: Test checkbox argument diff --git a/integration-tests/tests/datetime/config.yaml b/integration-tests/tests/datetime/config.yaml index 8647e15..d657cd2 100644 --- a/integration-tests/tests/datetime/config.yaml +++ b/integration-tests/tests/datetime/config.yaml @@ -3,6 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false +popupOnStart: execution-dialog actions: - title: Test datetime argument diff --git a/integration-tests/tests/suggestionsBrowserKey/config.yaml b/integration-tests/tests/suggestionsBrowserKey/config.yaml index eb2c1df..2bd5876 100644 --- a/integration-tests/tests/suggestionsBrowserKey/config.yaml +++ b/integration-tests/tests/suggestionsBrowserKey/config.yaml @@ -3,6 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false +popupOnStart: execution-dialog actions: - title: Test suggestionsBrowserKey From 38d6b57077028300a0c9bee9176a75dd4a44ba83 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 25 Feb 2026 23:24:02 +0000 Subject: [PATCH 09/16] chore: codefmt --- service/internal/executor/executor.go | 7 +++++- service/internal/executor/executor_test.go | 10 ++++---- service/internal/tpl/templates_test.go | 27 +++++++++++++--------- service/internal/webhooks/handler_test.go | 6 ++--- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 5fe2dec..694cc9b 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -718,13 +718,18 @@ func filterToDefinedArgumentsOnly(req *ExecutionRequest) { } filtered := make(map[string]string) for k, v := range req.Arguments { - if _, ok := definedNames[k]; ok || strings.HasPrefix(k, "ot_") { + if keepArgument(k, definedNames) { filtered[k] = v } } req.Arguments = filtered } +func keepArgument(name string, definedNames map[string]struct{}) bool { + _, ok := definedNames[name] + return ok || strings.HasPrefix(name, "ot_") +} + func hasWebhookTag(req *ExecutionRequest) bool { for _, tag := range req.Tags { if tag == "webhook" { diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index 00a4535..2608bbe 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -366,8 +366,8 @@ func TestFilterToDefinedArgumentsOnly(t *testing.T) { }, } req.Arguments = map[string]string{ - "name": "Alice", - "webhook_path": "/malicious/$(id)", + "name": "Alice", + "webhook_path": "/malicious/$(id)", "extra_undefined": "ignored", } @@ -381,13 +381,13 @@ func TestFilterToDefinedArgumentsOnly(t *testing.T) { func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) { req := newExecRequest() req.Binding.Action = &config.Action{ - Title: "Filter test", - Shell: "echo test", + Title: "Filter test", + Shell: "echo test", Arguments: []config.ActionArgument{}, } req.Arguments = map[string]string{ "ot_executionTrackingId": "track-123", - "ot_username": "webhook", + "ot_username": "webhook", } filterToDefinedArgumentsOnly(req) diff --git a/service/internal/tpl/templates_test.go b/service/internal/tpl/templates_test.go index d46f902..b49fe33 100644 --- a/service/internal/tpl/templates_test.go +++ b/service/internal/tpl/templates_test.go @@ -62,20 +62,25 @@ func TestParseTemplateWithActionContext_Json(t *testing.T) { } assert.NoError(t, err) if tt.checkJsonOnly { - prefix := strings.TrimSuffix(tt.expectedOutput, " ") - assert.True(t, strings.HasPrefix(output, prefix), "output %q should start with %q", output, prefix) - jsonPart := strings.TrimPrefix(output, prefix) - jsonPart = strings.TrimSpace(jsonPart) - var decoded map[string]string - err := json.Unmarshal([]byte(jsonPart), &decoded) - assert.NoError(t, err) - for k, v := range tt.args { - assert.Equal(t, v, decoded[k], "decoded JSON should contain %s=%s", k, v) - } - assert.Len(t, decoded, len(tt.args)) + assertJsonOutput(t, output, tt.expectedOutput, tt.args) } else { assert.Equal(t, tt.expectedOutput, output) } }) } } + +func assertJsonOutput(t *testing.T, output, expectedPrefix string, args map[string]string) { + t.Helper() + prefix := strings.TrimSuffix(expectedPrefix, " ") + assert.True(t, strings.HasPrefix(output, prefix), "output %q should start with %q", output, prefix) + jsonPart := strings.TrimPrefix(output, prefix) + jsonPart = strings.TrimSpace(jsonPart) + var decoded map[string]string + err := json.Unmarshal([]byte(jsonPart), &decoded) + assert.NoError(t, err) + for k, v := range args { + assert.Equal(t, v, decoded[k], "decoded JSON should contain %s=%s", k, v) + } + assert.Len(t, decoded, len(args)) +} diff --git a/service/internal/webhooks/handler_test.go b/service/internal/webhooks/handler_test.go index 48a2108..30831aa 100644 --- a/service/internal/webhooks/handler_test.go +++ b/service/internal/webhooks/handler_test.go @@ -16,9 +16,9 @@ func TestFilterToDefinedArguments(t *testing.T) { }, } args := map[string]string{ - "repo": "my-repo", - "branch": "main", - "webhook_path": "/deploy/prod", + "repo": "my-repo", + "branch": "main", + "webhook_path": "/deploy/prod", "webhook_header_x_custom": "malicious", } From 10294e203038b8a2817efdb005eeeb2e1ddc9990 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 25 Feb 2026 23:59:49 +0000 Subject: [PATCH 10/16] chore: type in SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index df1c506..f42ff45 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,7 +15,7 @@ To understand more about 2k vs 3k, see the following docs; https://docs.olivetin The very purpose of OliveTin is to allow users to execute commands remotely on a machine. -This means that, by design, OliveTin has might higher potential to be used for remote code execution (RCE), and any security vulnerabilities that do occour have the potential to be much more severe than in other types of software. +This means that, by design, OliveTin has much higher potential to be used for remote code execution (RCE), and any security vulnerabilities that do occur have the potential to be much more severe than in other types of software. We hope that you understand that while the project goes to great aims to be safe, and mitigate, that security vulnerabilities are inevitable, as they are with all software of all sizes - like Kubernetes, the Kernel, etc - and OliveTin has substancially less resources than those projects. From 5a0d94700c87aa967a9e74ef8ffbc556178a1615 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 00:01:00 +0000 Subject: [PATCH 11/16] chore: typo in SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index f42ff45..2558809 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,7 +17,7 @@ The very purpose of OliveTin is to allow users to execute commands remotely on a This means that, by design, OliveTin has much higher potential to be used for remote code execution (RCE), and any security vulnerabilities that do occur have the potential to be much more severe than in other types of software. -We hope that you understand that while the project goes to great aims to be safe, and mitigate, that security vulnerabilities are inevitable, as they are with all software of all sizes - like Kubernetes, the Kernel, etc - and OliveTin has substancially less resources than those projects. +We hope that you understand that while the project goes to great aims to be safe, and mitigate, that security vulnerabilities are inevitable, as they are with all software of all sizes - like Kubernetes, the Kernel, etc - and OliveTin has substantially less resources than those projects. With that being said, OliveTin tries to follow examples of best practice, so judge the project not on if/when it has security issues, but how security issues are responded to as the measure of quality. From 87f2a3287b295716a2b994a3faa34416930e5ed8 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 00:04:14 +0000 Subject: [PATCH 12/16] chore: invalid key in test config --- integration-tests/tests/checkbox/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/tests/checkbox/config.yaml b/integration-tests/tests/checkbox/config.yaml index 589bb7b..2afc362 100644 --- a/integration-tests/tests/checkbox/config.yaml +++ b/integration-tests/tests/checkbox/config.yaml @@ -3,7 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false -popupOnStart: execution-dialog +defaultPopupOnStart: execution-dialog actions: - title: Test checkbox argument From 4e2fdc2d9686d7cfc265fd4428352d45004422df Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 00:11:21 +0000 Subject: [PATCH 13/16] chore: fix security advisory link --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 2558809..cc1a212 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,7 @@ This is why we take security very seriously, and why we encourage responsible di Please use responsible disclosure practices when reporting a vulnerability. **You will receive full credit for your discovery**, and we will work with you to ensure that the issue is resolved as quickly as **possible**. Please note that only James Read has access to security issues at the moment, so please be patient and understanding if you do not receive an immediate response. -* **Option A (preferred)**: GitHub Security Advisories, which allows you to report a vulnerability privately and securely. You can find the option to report a security issue in the "Issues" tab of this repository, and then select "Report a security vulnerability". This will allow you to provide details about the vulnerability without making it public. +* **Option A (preferred)**: GitHub Security Advisories, which allows you to report a vulnerability privately and securely. Use this direct link to report privately: `https://github.com/OliveTin/OliveTin/security/advisories/new`. This allows you to provide details without making them public. * **Option B**: Please email `contact@jread.com` for responsible disclosure. From ad479651b7287c33da1c1d33bd507f824ecdf823 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 00:33:21 +0000 Subject: [PATCH 14/16] chore: Fix broken test configs --- integration-tests/tests/datetime/config.yaml | 2 +- integration-tests/tests/suggestionsBrowserKey/config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/datetime/config.yaml b/integration-tests/tests/datetime/config.yaml index d657cd2..7e3c38b 100644 --- a/integration-tests/tests/datetime/config.yaml +++ b/integration-tests/tests/datetime/config.yaml @@ -3,7 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false -popupOnStart: execution-dialog +defaultPopupOnStart: execution-dialog actions: - title: Test datetime argument diff --git a/integration-tests/tests/suggestionsBrowserKey/config.yaml b/integration-tests/tests/suggestionsBrowserKey/config.yaml index 2bd5876..957d187 100644 --- a/integration-tests/tests/suggestionsBrowserKey/config.yaml +++ b/integration-tests/tests/suggestionsBrowserKey/config.yaml @@ -3,7 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false -popupOnStart: execution-dialog +defaultPopupOnStart: execution-dialog actions: - title: Test suggestionsBrowserKey From 1335302e8008443508eaa57a937a31d2d9ba17b2 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 00:48:21 +0000 Subject: [PATCH 15/16] chore: codestyle --- service/internal/api/api.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 68ef978..02781d2 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -494,6 +494,19 @@ func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest, return connect.NewResponse(res), nil } +func resolveLogsPageSize(requestPageSize, defaultPageSize int64) int64 { + if requestPageSize == 0 { + return defaultPageSize + } + if requestPageSize < 10 { + return 10 + } + if requestPageSize > 100 { + return 100 + } + return requestPageSize +} + func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) { user := auth.UserFromApiCall(ctx, req, api.cfg) @@ -501,21 +514,9 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetL return nil, err } + pageSize := resolveLogsPageSize(req.Msg.GetPageSize(), api.cfg.LogHistoryPageSize) + logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, pageSize, req.Msg.DateFilter) ret := &apiv1.GetLogsResponse{} - dateFilter := "" - if req.Msg.DateFilter != "" { - dateFilter = req.Msg.DateFilter - } - pageSize := api.cfg.LogHistoryPageSize - if req.Msg.GetPageSize() > 0 { - pageSize = req.Msg.GetPageSize() - if pageSize < 10 { - pageSize = 10 - } else if pageSize > 100 { - pageSize = 100 - } - } - logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, pageSize, dateFilter) for _, le := range logEntries { ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user)) } From 04d627d36c51d3b92758d07fb2a4ede071e03ce4 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 07:17:22 +0000 Subject: [PATCH 16/16] chore: dependabot less spam --- .github/dependabot.yml | 50 +++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5d9c15a..d778df1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,14 +7,20 @@ updates: interval: "weekly" target-branch: "next" open-pull-requests-limit: 10 + labels: + - "3k" + - "dependencies" - # npm updates for frontend - targeting "release/2k" branch + # npm updates for frontend - targeting "release/2k" branch (security updates only) - package-ecosystem: "npm" directory: "/frontend" schedule: interval: "weekly" target-branch: "release/2k" - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 + labels: + - "2k" + - "dependencies" # npm updates for integration-tests - targeting "next" branch - package-ecosystem: "npm" @@ -23,14 +29,20 @@ updates: interval: "weekly" target-branch: "next" open-pull-requests-limit: 10 + labels: + - "3k" + - "dependencies" - # npm updates for integration-tests - targeting "release/2k" branch + # npm updates for integration-tests - targeting "release/2k" branch (security updates only) - package-ecosystem: "npm" directory: "/integration-tests" schedule: interval: "weekly" target-branch: "release/2k" - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 + labels: + - "2k" + - "dependencies" # Go modules updates for service - targeting "next" branch - package-ecosystem: "gomod" @@ -39,14 +51,20 @@ updates: interval: "weekly" target-branch: "next" open-pull-requests-limit: 10 + labels: + - "3k" + - "dependencies" - # Go modules updates for service - targeting "release/2k" branch + # Go modules updates for service - targeting "release/2k" branch (security updates only) - package-ecosystem: "gomod" directory: "/service" schedule: interval: "weekly" target-branch: "release/2k" - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 + labels: + - "2k" + - "dependencies" # Go modules updates for lang - targeting "next" branch - package-ecosystem: "gomod" @@ -55,14 +73,20 @@ updates: interval: "weekly" target-branch: "next" open-pull-requests-limit: 10 + labels: + - "3k" + - "dependencies" - # Go modules updates for lang - targeting "release/2k" branch + # Go modules updates for lang - targeting "release/2k" branch (security updates only) - package-ecosystem: "gomod" directory: "/lang" schedule: interval: "weekly" target-branch: "release/2k" - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 + labels: + - "2k" + - "dependencies" # Docker updates - targeting "next" branch - package-ecosystem: "docker" @@ -71,12 +95,18 @@ updates: interval: "weekly" target-branch: "next" open-pull-requests-limit: 10 + labels: + - "3k" + - "dependencies" - # Docker updates - targeting "release/2k" branch + # Docker updates - targeting "release/2k" branch (security updates only) - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" target-branch: "release/2k" - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 + labels: + - "2k" + - "dependencies"