From 20b348a8c37f8951c8fc5938155f8c345ee4784a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:18:09 +0000 Subject: [PATCH 001/148] chore(deps): bump axios from 1.13.2 to 1.13.5 in /integration-tests Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.13.5. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.13.5) --- updated-dependencies: - dependency-name: axios dependency-version: 1.13.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- integration-tests/package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json index 1251464..a6557b8 100644 --- a/integration-tests/package-lock.json +++ b/integration-tests/package-lock.json @@ -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" } }, From 58e1f37ee4093c5b1ce52d19c272a9c302d557d7 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 14 Feb 2026 22:08:52 +0000 Subject: [PATCH 002/148] 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 003/148] 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 004/148] 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 005/148] 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 006/148] 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 007/148] 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 008/148] 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 c3028bb3b52f24919ca536a7c1b646b126c3aff9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:52:28 +0000 Subject: [PATCH 009/148] chore(deps-dev): bump selenium-webdriver in /integration-tests Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.40.0 to 4.41.0. - [Release notes](https://github.com/SeleniumHQ/selenium/releases) - [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.40.0...selenium-4.41.0) --- updated-dependencies: - dependency-name: selenium-webdriver dependency-version: 4.41.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- integration-tests/package-lock.json | 16 ++++++++-------- integration-tests/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json index 12d651e..c7657ba 100644 --- a/integration-tests/package-lock.json +++ b/integration-tests/package-lock.json @@ -15,7 +15,7 @@ "chai": "^6.2.2", "eslint": "^9.39.2", "mocha": "^11.7.5", - "selenium-webdriver": "^4.40.0" + "selenium-webdriver": "^4.41.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1954,9 +1954,9 @@ "dev": true }, "node_modules/selenium-webdriver": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.40.0.tgz", - "integrity": "sha512-dU0QbnVKdPmoNP8OtMCazRdtU2Ux6Wl4FEpG1iwUbDeajJK1dBAywBLrC1D7YFRtogHzN96AbXBgBAJaarcysw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.41.0.tgz", + "integrity": "sha512-1XxuKVhr9az24xwixPBEDGSZP+P0z3ZOnCmr9Oiep0MlJN2Mk+flIjD3iBS9BgyjS4g14dikMqnrYUPIjhQBhA==", "dev": true, "funding": [ { @@ -1973,7 +1973,7 @@ "@bazel/runfiles": "^6.5.0", "jszip": "^3.10.1", "tmp": "^0.2.5", - "ws": "^8.18.3" + "ws": "^8.19.0" }, "engines": { "node": ">= 20.0.0" @@ -2347,9 +2347,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { diff --git a/integration-tests/package.json b/integration-tests/package.json index 21bbcc8..ff5de66 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -14,7 +14,7 @@ "chai": "^6.2.2", "eslint": "^9.39.2", "mocha": "^11.7.5", - "selenium-webdriver": "^4.40.0" + "selenium-webdriver": "^4.41.0" }, "dependencies": { "wait-on": "^9.0.4" From c4a8eadd3fa17f5d37a5f0f72dd30943674f7ea3 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 25 Feb 2026 23:15:57 +0000 Subject: [PATCH 010/148] 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 011/148] 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 012/148] 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 013/148] 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 014/148] 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 015/148] 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 016/148] 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 017/148] 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 018/148] 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" From ff3620bca9fe3ca1368ceba6dcfebcb5898db352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:24:25 +0000 Subject: [PATCH 019/148] chore(deps): bump github.com/bufbuild/buf in /service Bumps [github.com/bufbuild/buf](https://github.com/bufbuild/buf) from 1.65.0 to 1.66.0. - [Release notes](https://github.com/bufbuild/buf/releases) - [Changelog](https://github.com/bufbuild/buf/blob/main/CHANGELOG.md) - [Commits](https://github.com/bufbuild/buf/compare/v1.65.0...v1.66.0) --- updated-dependencies: - dependency-name: github.com/bufbuild/buf dependency-version: 1.66.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- service/go.mod | 16 ++++++++-------- service/go.sum | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/service/go.mod b/service/go.mod index 659b3ab..e03f2da 100644 --- a/service/go.mod +++ b/service/go.mod @@ -1,6 +1,6 @@ module github.com/OliveTin/OliveTin -go 1.25.0 +go 1.25.6 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 @@ -10,7 +10,7 @@ require ( github.com/MicahParks/keyfunc/v3 v3.8.0 github.com/PaesslerAG/jsonpath v0.1.1 github.com/alexedwards/argon2id v1.0.0 - github.com/bufbuild/buf v1.65.0 + github.com/bufbuild/buf v1.66.0 github.com/fsnotify/fsnotify v1.9.0 github.com/fzipp/gocyclo v0.6.0 github.com/go-critic/go-critic v0.14.3 @@ -27,7 +27,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 go.akshayshah.org/connectproto v0.6.0 - golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa golang.org/x/oauth2 v0.35.0 golang.org/x/sys v0.41.0 google.golang.org/protobuf v1.36.11 @@ -45,7 +45,7 @@ require ( buf.build/go/bufplugin v0.9.0 // indirect buf.build/go/bufprivateusage v0.1.0 // indirect buf.build/go/interrupt v1.1.0 // indirect - buf.build/go/protovalidate v1.1.2 // indirect + buf.build/go/protovalidate v1.1.3 // indirect buf.build/go/protoyaml v0.6.0 // indirect buf.build/go/spdx v0.2.0 // indirect buf.build/go/standard v0.1.0 // indirect @@ -57,7 +57,7 @@ require ( github.com/PaesslerAG/gval v1.2.4 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect + github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 // indirect github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cli/browser v1.3.0 // indirect @@ -90,7 +90,7 @@ require ( github.com/gofrs/flock v0.13.0 // indirect github.com/google/cel-go v0.27.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-containerregistry v0.20.7 // indirect + github.com/google/go-containerregistry v0.21.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jdx/go-netrc v1.0.0 // indirect @@ -155,8 +155,8 @@ require ( golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/grpc v1.75.1 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect pluginrpc.com/pluginrpc v0.5.0 // indirect diff --git a/service/go.sum b/service/go.sum index 5ef6552..373c8c6 100644 --- a/service/go.sum +++ b/service/go.sum @@ -32,6 +32,8 @@ buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0= buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE= +buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE= +buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= @@ -84,6 +86,8 @@ github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs= github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw= github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg= github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ= +github.com/bufbuild/buf v1.66.0 h1:6kksYJpu6r45bvPJSTwNSwRqiAjrwB9YyU7skjNzFVo= +github.com/bufbuild/buf v1.66.0/go.mod h1:tWVlwtIPZ7kzlCB9D0hbbfrroT0GNCybPdPQXq1i1Ac= github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs= github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI= github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs= @@ -92,6 +96,8 @@ github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREB github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI= github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co= github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE= +github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 h1:nxt1QhP9rMQNFhHTdcNFwJ9wKCSdBjd28gz+qGDv4kM= +github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -200,6 +206,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI= +github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= @@ -375,6 +383,7 @@ go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIl go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= @@ -421,6 +430,8 @@ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7 golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk= @@ -519,6 +530,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1: google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= @@ -527,6 +540,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 69c1276a131ee93d9539873e817254b9b27196f2 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 13:38:52 +0000 Subject: [PATCH 020/148] chore: Setup buildx --- .github/workflows/build-and-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index ba3362f..9b77515 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -87,6 +87,9 @@ jobs: with: install-only: true + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: release if: github.ref_type != 'tag' uses: cycjimmy/semantic-release-action@v4 From cb71ddf401cb55024a2b4eed19247af812ee99e0 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 16:14:41 +0000 Subject: [PATCH 021/148] fix: Set common security headers by default --- service/internal/config/config.go | 15 ++++++++++ service/internal/config/sanitize.go | 20 +++++++++++++ service/internal/httpservers/frontend.go | 36 +++++++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index de5bdcc..aa35153 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -107,6 +107,15 @@ type PrometheusConfig struct { DefaultGoMetrics bool `koanf:"defaultGoMetrics"` } +// SecurityConfig allows users to fine tune the security related HTTP headers. +type SecurityConfig struct { + HeaderContentSecurityPolicy bool `koanf:"headerContentSecurityPolicy"` + ContentSecurityPolicy string `koanf:"contentSecurityPolicy"` + HeaderXContentTypeOptions bool `koanf:"headerXContentTypeOptions"` + HeaderXFrameOptions bool `koanf:"headerXFrameOptions"` + XFrameOptions string `koanf:"xFrameOptions"` +} + // Config is the global config used through the whole app. type Config struct { UseSingleHTTPFrontend bool `koanf:"useSingleHTTPFrontend"` @@ -160,6 +169,7 @@ type Config struct { InsecureAllowDumpActionMap bool `koanf:"insecureAllowDumpActionMap"` InsecureAllowDumpJwtClaims bool `koanf:"insecureAllowDumpJwtClaims"` Prometheus PrometheusConfig `koanf:"prometheus"` + Security SecurityConfig `koanf:"security"` SaveLogs SaveLogsConfig `koanf:"saveLogs"` DefaultIconForActions string `koanf:"defaultIconForActions"` DefaultIconForDirectories string `koanf:"defaultIconForDirectories"` @@ -268,6 +278,11 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.InsecureAllowDumpJwtClaims = false config.Prometheus.Enabled = false config.Prometheus.DefaultGoMetrics = false + config.Security.HeaderContentSecurityPolicy = true + config.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'" + config.Security.HeaderXContentTypeOptions = true + config.Security.HeaderXFrameOptions = true + config.Security.XFrameOptions = "DENY" config.DefaultIconForActions = "😀" config.DefaultIconForDirectories = "📁" config.DefaultIconForBack = "«" diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 43364f5..6b64438 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -16,6 +16,7 @@ func (cfg *Config) Sanitize() { cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLocalUserPasswords() + cfg.sanitizeSecurityHeaders() // log.Infof("cfg %p", cfg) @@ -183,6 +184,25 @@ func (cfg *Config) sanitizeLocalUserPasswords() { } } +func (cfg *Config) sanitizeSecurityHeaders() { + cfg.sanitizeSecurityHeadersCSP() + cfg.sanitizeSecurityHeadersXFrameOptions() +} + +func (cfg *Config) sanitizeSecurityHeadersCSP() { + if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" { + return + } + cfg.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'" +} + +func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() { + if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions != "" { + return + } + cfg.Security.XFrameOptions = "DENY" +} + // parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment. func parsePasswordTemplate(source string) string { t, err := template.New("password").Option("missingkey=error").Parse(source) diff --git a/service/internal/httpservers/frontend.go b/service/internal/httpservers/frontend.go index 9860650..c1c1a1f 100644 --- a/service/internal/httpservers/frontend.go +++ b/service/internal/httpservers/frontend.go @@ -23,6 +23,40 @@ import ( log "github.com/sirupsen/logrus" ) +func applySecurityHeaders(cfg *config.Config, w http.ResponseWriter) { + applyCSP(cfg, w) + applyXContentTypeOptions(cfg, w) + applyXFrameOptions(cfg, w) +} + +func applyCSP(cfg *config.Config, w http.ResponseWriter) { + if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy == "" { + return + } + w.Header().Set("Content-Security-Policy", cfg.Security.ContentSecurityPolicy) +} + +func applyXContentTypeOptions(cfg *config.Config, w http.ResponseWriter) { + if !cfg.Security.HeaderXContentTypeOptions { + return + } + w.Header().Set("X-Content-Type-Options", "nosniff") +} + +func applyXFrameOptions(cfg *config.Config, w http.ResponseWriter) { + if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions == "" { + return + } + w.Header().Set("X-Frame-Options", cfg.Security.XFrameOptions) +} + +func securityHeadersMiddleware(cfg *config.Config, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + applySecurityHeaders(cfg, w) + next.ServeHTTP(w, r) + }) +} + func logDebugRequest(cfg *config.Config, source string, r *http.Request) { if cfg.LogDebugOptions.SingleFrontendRequests { log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL) @@ -96,7 +130,7 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) { srv := &http.Server{ Addr: cfg.ListenAddressSingleHTTPFrontend, - Handler: mux, + Handler: securityHeadersMiddleware(cfg, mux), } log.Fatal(srv.ListenAndServe()) From e487288287b654c821e83a3261b944a0b0a6391b Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 16:15:54 +0000 Subject: [PATCH 022/148] doc: Updated agents codestyle --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 7ff9f84..db2412f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,8 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - From repo root: `go run ./service` - Unit tests (Go): - From repo root: `cd service && make unittests` +- Code style (after editing code in `service/`): + - From repo root: `cd service && make codestyle` - Integration tests (Mocha + Selenium): - Single test: `cd integration-tests && npx --yes mocha test/general.mjs` - All tests: `cd integration-tests && npx --yes mocha` @@ -41,6 +43,7 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - Do not swallow errors; propagate or log meaningfully. - Match existing formatting; avoid unrelated reformatting. - Be safe around nils in executor steps (e.g., guard `req.Binding` and `req.Binding.Action`). +- Cyclomatic complexity over 4 is not permitted. ### API and Execution Flow (High-level) 1. Client calls Connect RPC (e.g., `Init`, `GetDashboard`, `StartAction`). From a7be68b35917682b3af022191d72f8af8af0dee9 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 16:49:29 +0000 Subject: [PATCH 023/148] security: 10-slot Semaphore around password hash functions to prevent resource exhaustion attacks --- service/internal/api/api.go | 12 +++++++- service/internal/api/local_user_login.go | 39 ++++++++++++++++++------ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 02781d2..911f2ab 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -3,6 +3,7 @@ package api import ( ctx "context" "encoding/json" + "errors" "os" "path" "sort" @@ -144,6 +145,9 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 hash, err := createHash(req.Msg.Password) if err != nil { + if errors.Is(err, ErrArgon2Busy) { + return nil, connect.NewError(connect.CodeResourceExhausted, err) + } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err)) } @@ -162,7 +166,13 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api }), nil } - match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) + match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) + if err != nil { + if errors.Is(err, ErrArgon2Busy) { + return nil, connect.NewError(connect.CodeResourceExhausted, err) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err)) + } response := connect.NewResponse(&apiv1.LocalUserLoginResponse{ Success: match, diff --git a/service/internal/api/local_user_login.go b/service/internal/api/local_user_login.go index db6e99e..13e77ce 100644 --- a/service/internal/api/local_user_login.go +++ b/service/internal/api/local_user_login.go @@ -1,6 +1,7 @@ package api import ( + "errors" "runtime" config "github.com/OliveTin/OliveTin/internal/config" @@ -8,6 +9,12 @@ import ( log "github.com/sirupsen/logrus" ) +var ErrArgon2Busy = errors.New("too many concurrent password operations") + +const argon2MaxConcurrent = 10 + +var argon2Sem = make(chan struct{}, argon2MaxConcurrent) + var defaultParams = argon2id.Params{ Memory: 64 * 1024, Iterations: 4, @@ -17,6 +24,12 @@ var defaultParams = argon2id.Params{ } func CreateHash(password string) (string, error) { + select { + case argon2Sem <- struct{}{}: + defer func() { <-argon2Sem }() + default: + return "", ErrArgon2Busy + } hash, err := argon2id.CreateHash(password, &defaultParams) if err != nil { @@ -31,30 +44,38 @@ func createHash(password string) (string, error) { return CreateHash(password) } -func comparePasswordAndHash(password, hash string) bool { +func comparePasswordAndHash(password, hash string) (bool, error) { + select { + case argon2Sem <- struct{}{}: + defer func() { <-argon2Sem }() + default: + return false, ErrArgon2Busy + } match, err := argon2id.ComparePasswordAndHash(password, hash) if err != nil { log.Errorf("Error comparing password and hash: %v", err) - return false + return false, nil } - return match + return match, nil } -func checkUserPassword(cfg *config.Config, username, password string) bool { +func checkUserPassword(cfg *config.Config, username, password string) (bool, error) { for _, user := range cfg.AuthLocalUsers.Users { if user.Username == username { - match := comparePasswordAndHash(password, user.Password) - + match, err := comparePasswordAndHash(password, user.Password) + if err != nil { + return false, err + } if match { - return true + return true, nil } else { log.WithFields(log.Fields{ "username": username, }).Warn("Password does not match for user") - return false + return false, nil } } } @@ -63,5 +84,5 @@ func checkUserPassword(cfg *config.Config, username, password string) bool { "username": username, }).Warn("Failed to check password for user, as username was not found") - return false + return false, nil } From 7717f735aa0a044e6f403d40513855953e6d4109 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 17:42:12 +0000 Subject: [PATCH 024/148] chore: dep update --- frontend/package-lock.json | 187 ++++++++-------- frontend/package.json | 8 +- integration-tests/package-lock.json | 319 ++++++++++------------------ integration-tests/package.json | 4 +- 4 files changed, 204 insertions(+), 314 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 51cfad8..3bc98c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-web": "^2.1.1", - "@hugeicons/core-free-icons": "^3.1.1", + "@hugeicons/core-free-icons": "^3.3.0", "@hugeicons/vue": "^1.0.4", "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", @@ -21,13 +21,13 @@ "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.28", + "vue": "^3.5.29", "vue-i18n": "^11.2.8", - "vue-router": "^5.0.2" + "vue-router": "^5.0.3" }, "devDependencies": { "process": "^0.11.10", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0" } }, @@ -905,9 +905,9 @@ } }, "node_modules/@hugeicons/core-free-icons": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.1.1.tgz", - "integrity": "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.3.0.tgz", + "integrity": "sha512-qYyr4JQ2eQIHTSTbITvnJvs6ERNK64D9gpwZnf2IyuG0exzqfyABLO/oTB71FB3RZPfu1GbwycdiGSo46apjMQ==", "license": "MIT" }, "node_modules/@hugeicons/vue": { @@ -1435,39 +1435,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", - "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.28", + "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", - "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", - "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.28", - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -1475,13 +1475,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", - "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/devtools-api": { @@ -1491,12 +1491,12 @@ "license": "MIT" }, "node_modules/@vue/devtools-kit": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", - "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz", + "integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.0.5", + "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1506,62 +1506,62 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", - "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz", + "integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==", "license": "MIT", "dependencies": { "rfdc": "^1.4.1" } }, "node_modules/@vue/reactivity": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", - "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.28" + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", - "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", - "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/runtime-core": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", - "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { - "vue": "3.5.28" + "vue": "3.5.29" } }, "node_modules/@vue/shared": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", - "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, "node_modules/@xterm/addon-fit": { @@ -2121,13 +2121,13 @@ } }, "node_modules/css-functions-list": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", - "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12 || >=16" + "node": ">=12" } }, "node_modules/css-tree": { @@ -4276,13 +4276,6 @@ "node": ">=0.10.0" } }, - "node_modules/known-css-properties": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", - "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", - "dev": true, - "license": "MIT" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5854,9 +5847,9 @@ } }, "node_modules/stylelint": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.3.0.tgz", - "integrity": "sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.4.0.tgz", + "integrity": "sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==", "dev": true, "funding": [ { @@ -5872,15 +5865,14 @@ "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", + "@csstools/css-syntax-patches-for-csstree": "^1.0.27", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", - "balanced-match": "^3.0.1", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", - "css-functions-list": "^3.2.3", + "css-functions-list": "^3.3.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", @@ -5894,7 +5886,6 @@ "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.37.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", @@ -5979,16 +5970,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/stylelint/node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/stylelint/node_modules/file-entry-cache": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz", @@ -6615,16 +6596,16 @@ } }, "node_modules/vue": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", - "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-sfc": "3.5.28", - "@vue/runtime-dom": "3.5.28", - "@vue/server-renderer": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" @@ -6656,14 +6637,14 @@ } }, "node_modules/vue-router": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz", - "integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz", + "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==", "license": "MIT", "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.0.0", + "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", @@ -6701,12 +6682,12 @@ } }, "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz", - "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz", + "integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.0.5" + "@vue/devtools-kit": "^8.0.6" } }, "node_modules/vue-router/node_modules/json5": { diff --git a/frontend/package.json b/frontend/package.json index a56e2bb..a64d473 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "source": "index.html", "devDependencies": { "process": "^0.11.10", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0" }, "scripts": { @@ -24,7 +24,7 @@ "dependencies": { "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-web": "^2.1.1", - "@hugeicons/core-free-icons": "^3.1.1", + "@hugeicons/core-free-icons": "^3.3.0", "@hugeicons/vue": "^1.0.4", "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", @@ -34,8 +34,8 @@ "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.28", + "vue": "^3.5.29", "vue-i18n": "^11.2.8", - "vue-router": "^5.0.2" + "vue-router": "^5.0.3" } } diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json index 12d651e..bb664ca 100644 --- a/integration-tests/package-lock.json +++ b/integration-tests/package-lock.json @@ -13,9 +13,9 @@ }, "devDependencies": { "chai": "^6.2.2", - "eslint": "^9.39.2", + "eslint": "^10.0.2", "mocha": "^11.7.5", - "selenium-webdriver": "^4.40.0" + "selenium-webdriver": "^4.41.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -67,9 +67,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -77,105 +77,68 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.0", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@hapi/address": { @@ -326,10 +289,17 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -341,9 +311,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -364,9 +334,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -438,14 +408,26 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/browser-stdout": { @@ -467,16 +449,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -639,13 +611,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -818,33 +783,30 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -854,8 +816,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -863,7 +824,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -878,58 +839,61 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -955,6 +919,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1216,19 +1181,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1314,23 +1266,6 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1552,12 +1487,6 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1612,16 +1541,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -1793,19 +1725,6 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1928,16 +1847,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -1954,9 +1863,9 @@ "dev": true }, "node_modules/selenium-webdriver": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.40.0.tgz", - "integrity": "sha512-dU0QbnVKdPmoNP8OtMCazRdtU2Ux6Wl4FEpG1iwUbDeajJK1dBAywBLrC1D7YFRtogHzN96AbXBgBAJaarcysw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.41.0.tgz", + "integrity": "sha512-1XxuKVhr9az24xwixPBEDGSZP+P0z3ZOnCmr9Oiep0MlJN2Mk+flIjD3iBS9BgyjS4g14dikMqnrYUPIjhQBhA==", "dev": true, "funding": [ { @@ -1973,7 +1882,7 @@ "@bazel/runfiles": "^6.5.0", "jszip": "^3.10.1", "tmp": "^0.2.5", - "ws": "^8.18.3" + "ws": "^8.19.0" }, "engines": { "node": ">= 20.0.0" @@ -2347,9 +2256,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { diff --git a/integration-tests/package.json b/integration-tests/package.json index 21bbcc8..331945f 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -12,9 +12,9 @@ "license": "AGPL-3.0-only", "devDependencies": { "chai": "^6.2.2", - "eslint": "^9.39.2", + "eslint": "^10.0.2", "mocha": "^11.7.5", - "selenium-webdriver": "^4.40.0" + "selenium-webdriver": "^4.41.0" }, "dependencies": { "wait-on": "^9.0.4" From 24cced0c8c806800f79cc2d8462ab0a193ddebcd Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 20:23:48 +0000 Subject: [PATCH 025/148] security: IDOR on ExecutionStatus API --- service/internal/api/api.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 911f2ab..339cbc4 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -384,10 +384,11 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap if ile == nil { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId)) - } else { - res.LogEntry = api.internalLogEntryToPb(ile, user) } - + if !isValidLogEntry(ile) || !api.isLogEntryAllowed(ile, user) { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view this execution")) + } + res.LogEntry = api.internalLogEntryToPb(ile, user) return connect.NewResponse(res), nil } From 4af4d516be018d260a8d184648aacff8d0198328 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 20:43:14 +0000 Subject: [PATCH 026/148] fix: ShowDiagnostics now behind policy checks --- service/internal/api/api.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 339cbc4..ef9d3f5 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -893,11 +893,17 @@ func (api *oliveTinAPI) OnExecutionFinished(ile *executor.InternalLogEntry) { } func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) { + user := auth.UserFromApiCall(ctx, req, api.cfg) + if err := api.checkDashboardAccess(user); err != nil { + return nil, err + } + if !user.EffectivePolicy.ShowDiagnostics { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("diagnostics are not available for your account")) + } res := &apiv1.GetDiagnosticsResponse{ SshFoundKey: installationinfo.Runtime.SshFoundKey, SshFoundConfig: installationinfo.Runtime.SshFoundConfig, } - return connect.NewResponse(res), nil } From f3549b035e788d73fc277354b622e202124d1497 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 20:46:11 +0000 Subject: [PATCH 027/148] Remove dead CORS package (L-2) The CORS helper was unused; its import was commented out in webuiServer.go. Deleting the package removes the dormant origin-reflection security issue. --- service/internal/cors/cors.go | 23 --------------------- service/internal/cors/cors_test.go | 22 -------------------- service/internal/httpservers/webuiServer.go | 2 -- 3 files changed, 47 deletions(-) delete mode 100644 service/internal/cors/cors.go delete mode 100644 service/internal/cors/cors_test.go diff --git a/service/internal/cors/cors.go b/service/internal/cors/cors.go deleted file mode 100644 index 905ed46..0000000 --- a/service/internal/cors/cors.go +++ /dev/null @@ -1,23 +0,0 @@ -package cors - -import ( - log "github.com/sirupsen/logrus" - "net/http" -) - -// AllowCors takes a HTTP handler and adds Access-Control-Allow-Origin headers to -// responses. -// -// Note: HTTP OPTIONS requests (which need to be preflighted" for CORS) are not -// handled because this app does not use HTTP PUT/PATCH/etc. -func AllowCors(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if origin := r.Header.Get("Origin"); origin != "" { - log.Debugf("Adding CORS header origin: %q", origin) - - w.Header().Set("Access-Control-Allow-Origin", origin) - } - - h.ServeHTTP(w, r) - }) -} diff --git a/service/internal/cors/cors_test.go b/service/internal/cors/cors_test.go deleted file mode 100644 index bfd87e2..0000000 --- a/service/internal/cors/cors_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package cors - -import ( - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" -) - -func TestCors(t *testing.T) { - req, _ := http.NewRequest("GET", "/health-check", nil) - req.Header.Add("Origin", "1.2.3.4") - - blat := AllowCors(http.FileServer(http.Dir("."))) - - rr := httptest.NewRecorder() - - blat.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusNotFound, rr.Code, "HTTP 404 on CORS") - assert.Equal(t, "1.2.3.4", rr.Header().Get("Access-Control-Allow-Origin"), "CORS Header set") -} diff --git a/service/internal/httpservers/webuiServer.go b/service/internal/httpservers/webuiServer.go index fb718ba..5b76c06 100644 --- a/service/internal/httpservers/webuiServer.go +++ b/service/internal/httpservers/webuiServer.go @@ -1,8 +1,6 @@ package httpservers import ( - - // cors "github.com/OliveTin/OliveTin/internal/cors" "net/http" "os" "path" From e9a3863b1b9da97ce9377e8f6e87f3fc90de7449 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 20:56:51 +0000 Subject: [PATCH 028/148] chore: codestyle --- service/internal/api/api.go | 114 ++++++++++------------- service/internal/api/local_user_login.go | 41 ++++---- 2 files changed, 69 insertions(+), 86 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index ef9d3f5..c557243 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -158,14 +158,32 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 return connect.NewResponse(ret), nil } -func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) { - // Check if local user authentication is enabled - if !api.cfg.AuthLocalUsers.Enabled { - return connect.NewResponse(&apiv1.LocalUserLoginResponse{ - Success: false, - }), nil +func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool) { + if match { + user := api.cfg.FindUserByUsername(req.Username) + if user != nil { + sid := uuid.NewString() + auth.RegisterUserSession(api.cfg, "local", sid, user.Username) + log.WithFields(log.Fields{"username": user.Username}).Info("LocalUserLogin: Session created and registered") + cookie := &http.Cookie{ + Name: "olivetin-sid-local", + Value: sid, + MaxAge: 31556952, + HttpOnly: true, + Path: "/", + } + response.Header().Set("Set-Cookie", cookie.String()) + } + log.WithFields(log.Fields{"username": req.Username}).Info("LocalUserLogin: User logged in successfully.") + } else { + log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: User login failed.") } +} +func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) { + if !api.cfg.AuthLocalUsers.Enabled { + return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false}), nil + } match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) if err != nil { if errors.Is(err, ErrArgon2Busy) { @@ -173,43 +191,8 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err)) } - - response := connect.NewResponse(&apiv1.LocalUserLoginResponse{ - Success: match, - }) - - if match { - // Set authentication cookie for successful login - user := api.cfg.FindUserByUsername(req.Msg.Username) - if user != nil { - sid := uuid.NewString() - // Register the session in the session storage - auth.RegisterUserSession(api.cfg, "local", sid, user.Username) - - log.WithFields(log.Fields{ - "username": user.Username, - }).Info("LocalUserLogin: Session created and registered") - - // Set the authentication cookie in the response headers - cookie := &http.Cookie{ - Name: "olivetin-sid-local", - Value: sid, - MaxAge: 31556952, // 1 year - HttpOnly: true, - Path: "/", - } - response.Header().Set("Set-Cookie", cookie.String()) - } - - log.WithFields(log.Fields{ - "username": req.Msg.Username, - }).Info("LocalUserLogin: User logged in successfully.") - } else { - log.WithFields(log.Fields{ - "username": req.Msg.Username, - }).Warn("LocalUserLogin: User login failed.") - } - + response := connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: match}) + api.applyLocalLoginResult(req.Msg, response, match) return response, nil } @@ -364,31 +347,36 @@ func getMostRecentExecutionStatusByActionId(api *oliveTinAPI, actionId string) * return ile } -func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) { - res := &apiv1.ExecutionStatusResponse{} - - user := auth.UserFromApiCall(ctx, req, api.cfg) - - if err := api.checkDashboardAccess(user); err != nil { - return nil, err - } - - var ile *executor.InternalLogEntry - - if req.Msg.ExecutionTrackingId != "" { - ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId) - - } else { - ile = getMostRecentExecutionStatusByActionId(api, req.Msg.ActionId) - } - +func (api *oliveTinAPI) resolveExecutionStatusForView(msg *apiv1.ExecutionStatusRequest, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, error) { + ile := api.getExecutionStatusByRequest(msg) if ile == nil { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId)) + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", msg.ExecutionTrackingId, msg.ActionId)) } if !isValidLogEntry(ile) || !api.isLogEntryAllowed(ile, user) { return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view this execution")) } - res.LogEntry = api.internalLogEntryToPb(ile, user) + return ile, nil +} + +func (api *oliveTinAPI) getExecutionStatusByRequest(msg *apiv1.ExecutionStatusRequest) *executor.InternalLogEntry { + if msg.ExecutionTrackingId != "" { + return getExecutionStatusByTrackingID(api, msg.ExecutionTrackingId) + } + return getMostRecentExecutionStatusByActionId(api, msg.ActionId) +} + +func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) { + user := auth.UserFromApiCall(ctx, req, api.cfg) + if err := api.checkDashboardAccess(user); err != nil { + return nil, err + } + ile, err := api.resolveExecutionStatusForView(req.Msg, user) + if err != nil { + return nil, err + } + res := &apiv1.ExecutionStatusResponse{ + LogEntry: api.internalLogEntryToPb(ile, user), + } return connect.NewResponse(res), nil } diff --git a/service/internal/api/local_user_login.go b/service/internal/api/local_user_login.go index 13e77ce..11485f9 100644 --- a/service/internal/api/local_user_login.go +++ b/service/internal/api/local_user_login.go @@ -33,7 +33,7 @@ func CreateHash(password string) (string, error) { hash, err := argon2id.CreateHash(password, &defaultParams) if err != nil { - log.Fatal("Error creating hash: ", err) + log.Warnf("Error creating hash: %v", err) return "", err } @@ -62,27 +62,22 @@ func comparePasswordAndHash(password, hash string) (bool, error) { } func checkUserPassword(cfg *config.Config, username, password string) (bool, error) { - for _, user := range cfg.AuthLocalUsers.Users { - if user.Username == username { - match, err := comparePasswordAndHash(password, user.Password) - if err != nil { - return false, err - } - if match { - return true, nil - } else { - log.WithFields(log.Fields{ - "username": username, - }).Warn("Password does not match for user") - - return false, nil - } - } + user := cfg.FindUserByUsername(username) + if user == nil { + log.WithFields(log.Fields{"username": username}).Warn("Failed to check password for user, as username was not found") + return false, nil } - - log.WithFields(log.Fields{ - "username": username, - }).Warn("Failed to check password for user, as username was not found") - - return false, nil + return comparePasswordAndLogResult(password, user.Password, username) +} + +func comparePasswordAndLogResult(password, hash, username string) (bool, error) { + match, err := comparePasswordAndHash(password, hash) + if err != nil { + return false, err + } + if !match { + log.WithFields(log.Fields{"username": username}).Warn("Password does not match for user") + return false, nil + } + return true, nil } From 4744169aa013a7360449418d5bfef8845b2ff8b4 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 23:07:07 +0000 Subject: [PATCH 029/148] chore: code cleanup, remove todos, etc --- .pre-commit-config.yaml | 33 +++++++++- service/internal/api/api.go | 2 +- service/internal/config/sanitize.go | 3 +- service/internal/executor/executor.go | 65 +++++++++++-------- service/internal/executor/executor_actions.go | 34 ++++++++++ 5 files changed, 105 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4efad37..dda308e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,19 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files + - id: check-merge-conflict + - id: detect-private-key + - id: mixed-line-ending + args: ['--fix', 'lf'] + - id: check-json + exclude: | + (?x)^( + service/internal/entities/testdata/.*\.json| + integration-tests/tests/.*/entities/.*\.json| + var/entities/.*\.json + )$ + - id: check-case-conflict + - id: detect-aws-credentials # Alternative semantic commit checker - repo: https://github.com/compilerla/conventional-pre-commit @@ -34,9 +47,23 @@ repos: pass_filenames: false always_run: true - - id: it - name: it - entry: make service-codestyle frontend-codestyle + - id: service-unittests + name: service-unittests + entry: make service-unittests + language: system + pass_filenames: false + always_run: true + + - id: service-build + name: service-build + entry: make service + language: system + pass_filenames: false + always_run: true + + - id: it + name: integration-tests + entry: make it language: system pass_filenames: false always_run: true diff --git a/service/internal/api/api.go b/service/internal/api/api.go index c557243..b479e96 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -1268,7 +1268,7 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{ Msg: &apiv1.StartActionRequest{ - // FIXME + BindingId: execReqLogEntry.GetBindingId(), UniqueTrackingId: req.Msg.ExecutionTrackingId, }, }) diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 6b64438..f699b80 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -259,8 +259,7 @@ func (arg *ActionArgument) sanitize() { arg.sanitizeNoType() - // TODO Validate the default against the type checker, but this creates a - // import loop + // Default value validation runs in executor at config load (validateArgumentDefaults). } func (arg *ActionArgument) sanitizeNoType() { diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 694cc9b..91312d4 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -603,14 +603,14 @@ func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int { then := time.Now().Add(-duration) + currentEntityPrefix := "" + if req.Binding != nil && req.Binding.Entity != nil { + currentEntityPrefix = req.Binding.Entity.UniqueKey + } for _, logEntry := range req.executor.GetLogsByBindingId(req.Binding.ID) { - // FIXME - /* - if logEntry.EntityPrefix != req.EntityPrefix { - continue - } - */ - + if logEntry.EntityPrefix != currentEntityPrefix { + continue + } if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked { executions += 1 @@ -761,28 +761,12 @@ func fail(req *ExecutionRequest, err error) bool { func stepRequestAction(req *ExecutionRequest) bool { metricActionsRequested.Inc() - // If there is no binding or action, do not proceed. Leave default - // log entry values (icon/title/id) and stop execution gracefully. - if req.Binding == nil || req.Binding.Action == nil { - log.Warnf("Action request has no binding/action; skipping execution") + if !stepRequestActionHasBinding(req) { return false } - req.logEntry.Binding = req.Binding - req.logEntry.ActionConfigTitle = req.Binding.Action.Title - req.logEntry.ActionTitle = tpl.ParseTemplateOfActionBeforeExec(req.Binding.Action.Title, req.Binding.Entity) - req.logEntry.ActionIcon = req.Binding.Action.Icon - req.logEntry.Tags = req.Tags - - req.executor.logmutex.Lock() - - if _, containsKey := req.executor.LogsByBindingId[req.Binding.ID]; !containsKey { - req.executor.LogsByBindingId[req.Binding.ID] = make([]*InternalLogEntry, 0) - } - - req.executor.LogsByBindingId[req.Binding.ID] = append(req.executor.LogsByBindingId[req.Binding.ID], req.logEntry) - - req.executor.logmutex.Unlock() + stepRequestActionPopulateLogEntry(req) + stepRequestActionRegisterLog(req) log.WithFields(log.Fields{ "actionTitle": req.logEntry.ActionTitle, @@ -794,6 +778,35 @@ func stepRequestAction(req *ExecutionRequest) bool { return true } +func stepRequestActionHasBinding(req *ExecutionRequest) bool { + if req.Binding == nil || req.Binding.Action == nil { + log.Warnf("Action request has no binding/action; skipping execution") + return false + } + return true +} + +func stepRequestActionPopulateLogEntry(req *ExecutionRequest) { + req.logEntry.Binding = req.Binding + req.logEntry.ActionConfigTitle = req.Binding.Action.Title + req.logEntry.ActionTitle = tpl.ParseTemplateOfActionBeforeExec(req.Binding.Action.Title, req.Binding.Entity) + req.logEntry.ActionIcon = req.Binding.Action.Icon + req.logEntry.Tags = req.Tags + if req.Binding.Entity != nil { + req.logEntry.EntityPrefix = req.Binding.Entity.UniqueKey + } +} + +func stepRequestActionRegisterLog(req *ExecutionRequest) { + req.executor.logmutex.Lock() + defer req.executor.logmutex.Unlock() + + if _, containsKey := req.executor.LogsByBindingId[req.Binding.ID]; !containsKey { + req.executor.LogsByBindingId[req.Binding.ID] = make([]*InternalLogEntry, 0) + } + req.executor.LogsByBindingId[req.Binding.ID] = append(req.executor.LogsByBindingId[req.Binding.ID], req.logEntry) +} + func stepLogStart(req *ExecutionRequest) bool { log.WithFields(log.Fields{ "actionTitle": req.logEntry.ActionTitle, diff --git a/service/internal/executor/executor_actions.go b/service/internal/executor/executor_actions.go index fdf504c..c55f114 100644 --- a/service/internal/executor/executor_actions.go +++ b/service/internal/executor/executor_actions.go @@ -41,7 +41,41 @@ type RebuildActionMapRequest struct { DashboardActionTitles []string } +func validateArgumentDefaults(cfg *config.Config) { + if cfg == nil { + return + } + for _, action := range cfg.Actions { + validateActionArgumentDefaults(action) + } +} + +func validateActionArgumentDefaults(action *config.Action) { + if action == nil { + return + } + for i := range action.Arguments { + validateArgumentDefault(action, &action.Arguments[i]) + } +} + +func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) { + if arg.Default == "" { + return + } + if err := ValidateArgument(arg, arg.Default, action); err != nil { + log.WithFields(log.Fields{ + "actionTitle": action.Title, + "argName": arg.Name, + "default": arg.Default, + "error": err, + }).Warn("Argument default value failed validation") + } +} + func (e *Executor) RebuildActionMap() { + validateArgumentDefaults(e.Cfg) + e.MapActionBindingsLock.Lock() clear(e.MapActionBindings) From 03da2ff2e7c8a93f415c464a5044b033fc69c569 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 26 Feb 2026 23:43:50 +0000 Subject: [PATCH 030/148] security: Try to set cookies secure, with force override option --- service/internal/api/api.go | 16 ++++++++++++++-- .../auth/otoauth2/restapi_auth_oauth2.go | 8 +++++++- service/internal/config/config.go | 3 ++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index b479e96..bf168a7 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -158,7 +158,12 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 return connect.NewResponse(ret), nil } -func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool) { +func (api *oliveTinAPI) cookieSecure(header http.Header) bool { + useTLS := header.Get("X-Forwarded-Proto") == "https" + return useTLS || api.cfg.Security.ForceSecureCookies +} + +func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool, secure bool) { if match { user := api.cfg.FindUserByUsername(req.Username) if user != nil { @@ -171,6 +176,8 @@ func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, MaxAge: 31556952, HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", cookie.String()) } @@ -192,7 +199,7 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err)) } response := connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: match}) - api.applyLocalLoginResult(req.Msg, response, match) + api.applyLocalLoginResult(req.Msg, response, match, api.cookieSecure(req.Header())) return response, nil } @@ -389,6 +396,7 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou }).Info("Logout: User logged out") response := connect.NewResponse(&apiv1.LogoutResponse{}) + secure := api.cookieSecure(req.Header()) // Clear the local authentication cookie by setting it to expire localCookie := &http.Cookie{ @@ -397,6 +405,8 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou MaxAge: -1, // This tells the browser to delete the cookie HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", localCookie.String()) @@ -407,6 +417,8 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou MaxAge: -1, // This tells the browser to delete the cookie HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Add("Set-Cookie", oauth2Cookie.String()) diff --git a/service/internal/auth/otoauth2/restapi_auth_oauth2.go b/service/internal/auth/otoauth2/restapi_auth_oauth2.go index 1dea7d0..5266124 100644 --- a/service/internal/auth/otoauth2/restapi_auth_oauth2.go +++ b/service/internal/auth/otoauth2/restapi_auth_oauth2.go @@ -108,14 +108,20 @@ func randString(nByte int) (string, error) { return base64.URLEncoding.EncodeToString(b), nil } +func (h *OAuth2Handler) cookieSecure(r *http.Request) bool { + useTLS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" + return useTLS || h.cfg.Security.ForceSecureCookies +} + func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { cookie := &http.Cookie{ Name: name, Value: value, MaxAge: 900, // 15 minutes - Secure: r.TLS != nil, + Secure: h.cookieSecure(r), HttpOnly: true, Path: "/", + SameSite: http.SameSiteLaxMode, } http.SetCookie(w, cookie) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index aa35153..b235325 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -107,13 +107,14 @@ type PrometheusConfig struct { DefaultGoMetrics bool `koanf:"defaultGoMetrics"` } -// SecurityConfig allows users to fine tune the security related HTTP headers. +// SecurityConfig allows users to fine tune the security related HTTP headers and cookie options. type SecurityConfig struct { HeaderContentSecurityPolicy bool `koanf:"headerContentSecurityPolicy"` ContentSecurityPolicy string `koanf:"contentSecurityPolicy"` HeaderXContentTypeOptions bool `koanf:"headerXContentTypeOptions"` HeaderXFrameOptions bool `koanf:"headerXFrameOptions"` XFrameOptions string `koanf:"xFrameOptions"` + ForceSecureCookies bool `koanf:"forceSecureCookies"` } // Config is the global config used through the whole app. From 54eb2a658681b232cad8abe3cf15ee0b5dda0884 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 27 Feb 2026 00:10:45 +0000 Subject: [PATCH 031/148] fix: User login log message fixed when password matches, but user lookup fails --- service/internal/api/api.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index bf168a7..e2b235f 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -180,8 +180,10 @@ func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", cookie.String()) + log.WithFields(log.Fields{"username": user.Username}).Info("LocalUserLogin: User logged in successfully.") + } else { + log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: Password matched but user lookup failed.") } - log.WithFields(log.Fields{"username": req.Username}).Info("LocalUserLogin: User logged in successfully.") } else { log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: User login failed.") } From 1c32a389b37689bf2fbb13c6afbf31c926315912 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:25:25 +0000 Subject: [PATCH 032/148] chore(deps): bump minimatch from 3.1.2 to 3.1.5 in /frontend Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3bc98c8..041f6bc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4479,9 +4479,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" From f41fe2caba751c70ffb98a1271234ba32c4b3e4f Mon Sep 17 00:00:00 2001 From: James Read Date: Fri, 27 Feb 2026 11:21:34 +0000 Subject: [PATCH 033/148] docs: Add autonomy level to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3bc9214..792d035 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050) [![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin) +[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%201%20of%205%20(assistance--only)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/) [OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html) From aa2bd95ccb8f00a1c97ebcb0a03e8d49867ffde6 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 27 Feb 2026 21:26:55 +0000 Subject: [PATCH 034/148] feat(policy): add policy to show/hide version number Made-with: Cursor --- .../gen/olivetin/api/v1/olivetin_pb.d.ts | 6 ++++- .../gen/olivetin/api/v1/olivetin_pb.js | 3 +-- frontend/resources/vue/App.vue | 20 ++++++++-------- .../resources/vue/views/DiagnosticsView.vue | 7 ++++-- proto/olivetin/api/v1/olivetin.proto | 1 + service/gen/olivetin/api/v1/olivetin.pb.go | 23 +++++++++++++------ service/internal/api/api.go | 17 ++++++++++---- service/internal/api/apiActions.go | 5 ++-- .../auth/authpublic/authenticateduser.go | 9 ++++++-- service/internal/config/config.go | 6 +++-- .../internal/installationinfo/sosreport.go | 14 ++++++++--- 11 files changed, 77 insertions(+), 34 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 8e308fe..3961635 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 @@ -210,6 +210,11 @@ export declare type EffectivePolicy = Message<"olivetin.api.v1.EffectivePolicy"> * @generated from field: bool show_log_list = 2; */ showLogList: boolean; + + /** + * @generated from field: bool show_version_number = 3; + */ + showVersionNumber: boolean; }; /** @@ -1853,4 +1858,3 @@ 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 4e6e723..dd61f31 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("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLcAQoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiQgoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSJ9ChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJImcKGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ik4KDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCRIRCglwYWdlX3NpemUYAyABKAMimgMKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCSKRAQoPR2V0TG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiPwoUR2V0QWN0aW9uTG9nc1JlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEhQKDHN0YXJ0X29mZnNldBgCIAEoAyKXAQoVR2V0QWN0aW9uTG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJIkcKF0V4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIPCg1XaG9BbUlSZXF1ZXN0ImwKDldob0FtSVJlc3BvbnNlEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgBIAEoCRIRCgl1c2VyZ3JvdXAYAiABKAkSEAoIcHJvdmlkZXIYAyABKAkSDAoEYWNscxgEIAMoCRILCgNzaWQYBSABKAkiEgoQU29zUmVwb3J0UmVxdWVzdCIiChFTb3NSZXBvcnRSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCLjAgoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSJDChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbiIUChJHZXRFbnRpdGllc1JlcXVlc3QiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiJpChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLoEgoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJUCglTb3NSZXBvcnQSIS5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLcAQoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIn0KElN0YXJ0QWN0aW9uUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhoKEnVuaXF1ZV90cmFja2luZ19pZBgDIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiZwoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiTgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAyKaAwoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJIpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyJlChtWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QSDQoFdmFsdWUYASABKAkSDAoEdHlwZRgCIAEoCRISCgpiaW5kaW5nX2lkGAMgASgJEhUKDWFyZ3VtZW50X25hbWUYBCABKAkiQgocVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZRINCgV2YWxpZBgBIAEoCBITCgtkZXNjcmlwdGlvbhgCIAEoCSI2ChVXYXRjaEV4ZWN1dGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIiYKFFdhdGNoRXhlY3V0aW9uVXBkYXRlEg4KBnVwZGF0ZRgBIAEoCSJKChZFeGVjdXRpb25TdGF0dXNSZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIRCglhY3Rpb25faWQYAiABKAkiRwoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSISChBTb3NSZXBvcnRSZXF1ZXN0IiIKEVNvc1JlcG9ydFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJIhEKD0R1bXBWYXJzUmVxdWVzdCKVAQoQRHVtcFZhcnNSZXNwb25zZRINCgVhbGVydBgBIAEoCRJBCghjb250ZW50cxgCIAMoCzIvLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlLkNvbnRlbnRzRW50cnkaLwoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjsKDERlYnVnQmluZGluZxIUCgxhY3Rpb25fdGl0bGUYASABKAkSFQoNZW50aXR5X3ByZWZpeBgCIAEoCSIeChxEdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Is4BCh1EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZRINCgVhbGVydBgBIAEoCRJOCghjb250ZW50cxgCIAMoCzI8Lm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZS5Db250ZW50c0VudHJ5Gk4KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0ub2xpdmV0aW4uYXBpLnYxLkRlYnVnQmluZGluZzoCOAEiEgoQR2V0UmVhZHl6UmVxdWVzdCIjChFHZXRSZWFkeXpSZXNwb25zZRIOCgZzdGF0dXMYASABKAkiFAoSRXZlbnRTdHJlYW1SZXF1ZXN0IuMCChNFdmVudFN0cmVhbVJlc3BvbnNlEj0KDmVudGl0eV9jaGFuZ2VkGAIgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50RW50aXR5Q2hhbmdlZEgAEj0KDmNvbmZpZ19jaGFuZ2VkGAMgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50Q29uZmlnQ2hhbmdlZEgAEkUKEmV4ZWN1dGlvbl9maW5pc2hlZBgEIAEoCzInLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvbkZpbmlzaGVkSAASQwoRZXhlY3V0aW9uX3N0YXJ0ZWQYBSABKAsyJi5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25TdGFydGVkSAASOQoMb3V0cHV0X2NodW5rGAYgASgLMiEub2xpdmV0aW4uYXBpLnYxLkV2ZW50T3V0cHV0Q2h1bmtIAEIHCgVldmVudCJBChBFdmVudE91dHB1dENodW5rEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZvdXRwdXQYAiABKAkiFAoSRXZlbnRFbnRpdHlDaGFuZ2VkIhQKEkV2ZW50Q29uZmlnQ2hhbmdlZCJGChZFdmVudEV4ZWN1dGlvbkZpbmlzaGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJFChVFdmVudEV4ZWN1dGlvblN0YXJ0ZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjIKEUtpbGxBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSJtChJLaWxsQWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBmtpbGxlZBgCIAEoCBIZChFhbHJlYWR5X2NvbXBsZXRlZBgDIAEoCBINCgVmb3VuZBgEIAEoCCI7ChVMb2NhbFVzZXJMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiKQoWTG9jYWxVc2VyTG9naW5SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIicKE1Bhc3N3b3JkSGFzaFJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkiJAoUUGFzc3dvcmRIYXNoUmVzcG9uc2USDAoEaGFzaBgBIAEoCSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlIhcKFUdldERpYWdub3N0aWNzUmVxdWVzdCJFChZHZXREaWFnbm9zdGljc1Jlc3BvbnNlEhMKC1NzaEZvdW5kS2V5GAEgASgJEhYKDlNzaEZvdW5kQ29uZmlnGAIgASgJIg0KC0luaXRSZXF1ZXN0IusFCgxJbml0UmVzcG9uc2USEgoKc2hvd0Zvb3RlchgBIAEoCBIWCg5zaG93TmF2aWdhdGlvbhgCIAEoCBIXCg9zaG93TmV3VmVyc2lvbnMYAyABKAgSGAoQYXZhaWxhYmxlVmVyc2lvbhgEIAEoCRIWCg5jdXJyZW50VmVyc2lvbhgFIAEoCRIRCglwYWdlVGl0bGUYBiABKAkSHgoWc2VjdGlvbk5hdmlnYXRpb25TdHlsZRgHIAEoCRIaChJkZWZhdWx0SWNvbkZvckJhY2sYCCABKAkSFgoOZW5hYmxlQ3VzdG9tSnMYCSABKAgSFAoMYXV0aExvZ2luVXJsGAogASgJEhYKDmF1dGhMb2NhbExvZ2luGAsgASgIEhEKCXN0eWxlTW9kcxgMIAMoCRI4Cg9vQXV0aDJQcm92aWRlcnMYDSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuT0F1dGgyUHJvdmlkZXISOAoPYWRkaXRpb25hbExpbmtzGA4gAygLMh8ub2xpdmV0aW4uYXBpLnYxLkFkZGl0aW9uYWxMaW5rEhYKDnJvb3REYXNoYm9hcmRzGA8gAygJEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgQIAEoCRIjChthdXRoZW50aWNhdGVkX3VzZXJfcHJvdmlkZXIYESABKAkSOgoQZWZmZWN0aXZlX3BvbGljeRgSIAEoCzIgLm9saXZldGluLmFwaS52MS5FZmZlY3RpdmVQb2xpY3kSFgoOYmFubmVyX21lc3NhZ2UYEyABKAkSEgoKYmFubmVyX2NzcxgUIAEoCRIYChBzaG93X2RpYWdub3N0aWNzGBUgASgIEhUKDXNob3dfbG9nX2xpc3QYFiABKAgSFgoObG9naW5fcmVxdWlyZWQYFyABKAgSGAoQYXZhaWxhYmxlX3RoZW1lcxgYIAMoCRIkChxzaG93X25hdmlnYXRlX29uX3N0YXJ0X2ljb25zGBkgASgIIiwKDkFkZGl0aW9uYWxMaW5rEg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCSI6Cg5PQXV0aDJQcm92aWRlchINCgV0aXRsZRgBIAEoCRIMCgRpY29uGAMgASgJEgsKA2tleRgEIAEoCSItChdHZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJIkMKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uIhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMugSChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASdQoUVmFsaWRhdGVBcmd1bWVudFR5cGUSLC5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Gi0ub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2UiABJLCgZXaG9BbUkSHi5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVxdWVzdBofLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXNwb25zZSIAElQKCVNvc1JlcG9ydBIhLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlc3BvbnNlIgASUQoIRHVtcFZhcnMSIC5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXF1ZXN0GiEub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UiABJ4ChVEdW1wUHVibGljSWRBY3Rpb25NYXASLS5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdBouLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZSIAElQKCUdldFJlYWR5ehIhLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlc3BvbnNlIgASYwoOTG9jYWxVc2VyTG9naW4SJi5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVzcG9uc2UiABJdCgxQYXNzd29yZEhhc2gSJC5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXNwb25zZSIAEksKBkxvZ291dBIeLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlc3BvbnNlIgASXAoLRXZlbnRTdHJlYW0SIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVzcG9uc2UiADABEmMKDkdldERpYWdub3N0aWNzEiYub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1Jlc3BvbnNlIgASRQoESW5pdBIcLm9saXZldGluLmFwaS52MS5Jbml0UmVxdWVzdBodLm9saXZldGluLmFwaS52MS5Jbml0UmVzcG9uc2UiABJpChBHZXRBY3Rpb25CaW5kaW5nEigub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXNwb25zZSIAEloKC0dldEVudGl0aWVzEiMub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1Jlc3BvbnNlIgASSQoJR2V0RW50aXR5EiEub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0eVJlcXVlc3QaFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5IgBCOFo2Z2l0aHViLmNvbS9PbGl2ZVRpbi9PbGl2ZVRpbi9nZW4vb2xpdmV0aW4vYXBpL3YxO2FwaXYxYgZwcm90bzM"); /** * Describes the message olivetin.api.v1.Action. @@ -491,4 +491,3 @@ export const RestartActionRequestSchema = /*@__PURE__*/ */ export const OliveTinApiService = /*@__PURE__*/ serviceDesc(file_olivetin_api_v1_olivetin, 0); - diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 5648191..7dfae4a 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -31,7 +31,7 @@

- OliveTin {{ currentVersion }} + OliveTin {{ currentVersion }}

@@ -52,7 +52,7 @@ {{ t('connected') }}

-

+

@@ -68,7 +68,7 @@

- {{ t('language-dialog.browser-languages') }}: + {{ t('language-dialog.browser-languages') }}: {{ browserLanguages.join(', ') }} {{ t('language-dialog.not-available') }}

@@ -126,6 +126,7 @@ const showFooter = ref(true) const showNavigation = ref(true) const showLogs = ref(true) const showDiagnostics = ref(true) +const showVersionNumber = ref(true) const showLoginLink = ref(true) const sectionNavigationStyle = ref('sidebar') @@ -184,7 +185,7 @@ function normalizeBrowserLanguage() { if (navigator.languages && navigator.languages.length > 0) { for (const candidate of navigator.languages) { const lowerCandidate = candidate.toLowerCase() - + // Try exact match (case-insensitive) const exact = available.find(locale => locale.toLowerCase() === lowerCandidate) if (exact) { @@ -223,6 +224,7 @@ function updateHeaderFromInit() { showNavigation.value = window.initResponse.showNavigation showLogs.value = window.initResponse.showLogList showDiagnostics.value = window.initResponse.showDiagnostics + showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar' availableThemes.value = window.initResponse.availableThemes || [] @@ -277,7 +279,7 @@ function renderNavigation() { function openLanguageDialog() { selectedLanguage.value = languagePreference.value - + if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) { browserLanguages.value = navigator.languages } else { @@ -327,7 +329,7 @@ function handleLanguageDialogClick(event) { function openThemeDialog() { selectedTheme.value = themePreference.value || '' - + if (themeDialog.value) { themeDialog.value.showModal() } @@ -354,7 +356,7 @@ function changeTheme() { function applyTheme() { let themeStyle = document.getElementById('theme-style') - + if (!themeStyle) { themeStyle = document.createElement('style') themeStyle.id = 'theme-style' @@ -404,10 +406,10 @@ window.updateHeaderFromInit = updateHeaderFromInit onMounted(() => { serverConnection.value = true; updateHeaderFromInit() - + // Initialize selected language from stored preference selectedLanguage.value = languagePreference.value - + // Initialize selected theme from stored preference selectedTheme.value = themePreference.value || '' diff --git a/frontend/resources/vue/views/DiagnosticsView.vue b/frontend/resources/vue/views/DiagnosticsView.vue index a818090..675ffd9 100644 --- a/frontend/resources/vue/views/DiagnosticsView.vue +++ b/frontend/resources/vue/views/DiagnosticsView.vue @@ -162,7 +162,10 @@ async function generateBrowserInfo() { userAgentData: userAgentData } - const olivetinVersion = window.initResponse?.currentVersion || t('diagnostics.unknown') + const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true + const olivetinVersion = showVersionNumber + ? (window.initResponse?.currentVersion || t('diagnostics.unknown')) + : '[hidden]' const currentLanguage = locale.value || t('diagnostics.unknown') let output = ''; @@ -300,4 +303,4 @@ onMounted(() => { flex-direction: column; gap: 1em; } - \ No newline at end of file + diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index 8554f6b..baf71e2 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -51,6 +51,7 @@ message GetDashboardResponse { message EffectivePolicy { bool show_diagnostics = 1; bool show_log_list = 2; + bool show_version_number = 3; } message GetDashboardRequest { diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index 682ca2c..182827b 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -410,11 +410,12 @@ func (x *GetDashboardResponse) GetDashboard() *Dashboard { } type EffectivePolicy struct { - state protoimpl.MessageState `protogen:"open.v1"` - ShowDiagnostics bool `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"` - ShowLogList bool `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ShowDiagnostics bool `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"` + ShowLogList bool `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"` + ShowVersionNumber bool `protobuf:"varint,3,opt,name=show_version_number,json=showVersionNumber,proto3" json:"show_version_number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EffectivePolicy) Reset() { @@ -461,6 +462,13 @@ func (x *EffectivePolicy) GetShowLogList() bool { return false } +func (x *EffectivePolicy) GetShowVersionNumber() bool { + if x != nil { + return x.ShowVersionNumber + } + return false +} + type GetDashboardRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` @@ -3934,10 +3942,11 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"f\n" + "\x14GetDashboardResponse\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x128\n" + - "\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"`\n" + + "\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"\x90\x01\n" + "\x0fEffectivePolicy\x12)\n" + "\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" + - "\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"k\n" + + "\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\x12.\n" + + "\x13show_version_number\x18\x03 \x01(\bR\x11showVersionNumber\"k\n" + "\x13GetDashboardRequest\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" + "\ventity_type\x18\x02 \x01(\tR\n" + diff --git a/service/internal/api/api.go b/service/internal/api/api.go index e2b235f..629d039 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -710,7 +710,9 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAm } func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) { - sos := installationinfo.GetSosReport() + user := auth.UserFromApiCall(ctx, req, api.cfg) + redactVersion := !user.EffectivePolicy.ShowVersionNumber + sos := installationinfo.GetSosReport(redactVersion) if !api.cfg.InsecureAllowDumpSos { log.Info(sos) @@ -914,12 +916,19 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin + showVersion := user.EffectivePolicy.ShowVersionNumber + currentVersion := "" + availableVersion := "" + if showVersion { + currentVersion = installationinfo.Build.Version + availableVersion = installationinfo.Runtime.AvailableVersion + } res := &apiv1.InitResponse{ ShowFooter: api.cfg.ShowFooter, ShowNavigation: api.cfg.ShowNavigation, - ShowNewVersions: api.cfg.ShowNewVersions, - AvailableVersion: installationinfo.Runtime.AvailableVersion, - CurrentVersion: installationinfo.Build.Version, + ShowNewVersions: showVersion && api.cfg.ShowNewVersions, + AvailableVersion: availableVersion, + CurrentVersion: currentVersion, PageTitle: api.cfg.PageTitle, SectionNavigationStyle: api.cfg.SectionNavigationStyle, DefaultIconForBack: api.cfg.DefaultIconForBack, diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 23f8f40..355f179 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -55,8 +55,9 @@ func matchesEntity(binding *executor.ActionBinding, entity *entities.Entity) boo func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy { ret := &apiv1.EffectivePolicy{ - ShowDiagnostics: policy.ShowDiagnostics, - ShowLogList: policy.ShowLogList, + ShowDiagnostics: policy.ShowDiagnostics, + ShowLogList: policy.ShowLogList, + ShowVersionNumber: policy.ShowVersionNumber, } return ret diff --git a/service/internal/auth/authpublic/authenticateduser.go b/service/internal/auth/authpublic/authenticateduser.go index e4ab6f0..077f220 100644 --- a/service/internal/auth/authpublic/authenticateduser.go +++ b/service/internal/auth/authpublic/authenticateduser.go @@ -76,8 +76,9 @@ func (u *AuthenticatedUser) BuildUserAcls(cfg *config.Config) { func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy { ret := &config.ConfigurationPolicy{ - ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics, - ShowLogList: cfg.DefaultPolicy.ShowLogList, + ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics, + ShowLogList: cfg.DefaultPolicy.ShowLogList, + ShowVersionNumber: cfg.DefaultPolicy.ShowVersionNumber, } for _, acl := range cfg.AccessControlLists { @@ -98,5 +99,9 @@ func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.Con ret.ShowLogList = policy.ShowLogList } + if policy.ShowVersionNumber { + ret.ShowVersionNumber = policy.ShowVersionNumber + } + return ret } diff --git a/service/internal/config/config.go b/service/internal/config/config.go index b235325..82c466f 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -98,8 +98,9 @@ type AccessControlList struct { // ConfigurationPolicy defines global settings which are overridden with an ACL. type ConfigurationPolicy struct { - ShowDiagnostics bool `koanf:"showDiagnostics"` - ShowLogList bool `koanf:"showLogList"` + ShowDiagnostics bool `koanf:"showDiagnostics"` + ShowLogList bool `koanf:"showLogList"` + ShowVersionNumber bool `koanf:"showVersionNumber"` } type PrometheusConfig struct { @@ -297,6 +298,7 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.DefaultPolicy.ShowDiagnostics = true config.DefaultPolicy.ShowLogList = true + config.DefaultPolicy.ShowVersionNumber = true return &config } diff --git a/service/internal/installationinfo/sosreport.go b/service/internal/installationinfo/sosreport.go index 3dd1e77..4d6c2ce 100644 --- a/service/internal/installationinfo/sosreport.go +++ b/service/internal/installationinfo/sosreport.go @@ -40,15 +40,23 @@ func configToSosreport(cfg *config.Config) *sosReportConfig { } } -func GetSosReport() string { +func GetSosReport(redactVersion bool) string { ret := "" ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n" - out, _ := yaml.Marshal(Build) + buildForReport := *Build + if redactVersion { + buildForReport.Version = "[redacted]" + } + out, _ := yaml.Marshal(&buildForReport) ret += fmt.Sprintf("# Build: \n%+v\n", string(out)) - out, _ = yaml.Marshal(Runtime) + runtimeForReport := *Runtime + if redactVersion { + runtimeForReport.AvailableVersion = "[redacted]" + } + out, _ = yaml.Marshal(&runtimeForReport) ret += fmt.Sprintf("# Runtime:\n%+v\n", string(out)) out, _ = yaml.Marshal(configToSosreport(Config)) From 7051aad5995ec771314128437d4ce7d525de0d83 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 1 Mar 2026 23:17:44 +0000 Subject: [PATCH 035/148] feat: Clickable links in outout (#900) --- frontend/js/OutputTerminal.js | 14 +++- frontend/package-lock.json | 7 ++ frontend/package.json | 3 +- .../tests/xtermLinkHandling/config.yaml | 13 ++++ .../xtermLinkHandling/xtermLinkHandling.mjs | 69 +++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 integration-tests/tests/xtermLinkHandling/config.yaml create mode 100644 integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs diff --git a/frontend/js/OutputTerminal.js b/frontend/js/OutputTerminal.js index 1526318..ebda77b 100644 --- a/frontend/js/OutputTerminal.js +++ b/frontend/js/OutputTerminal.js @@ -1,5 +1,6 @@ import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' +import { WebLinksAddon } from '@xterm/addon-web-links' import { Mutex } from './Mutex.js' /** @@ -18,13 +19,24 @@ export class OutputTerminal { constructor (executionTrackingId) { this.executionTrackingId = executionTrackingId this.writeMutex = new Mutex() + const linkHandler = { + activate (event, text, _range) { + event.preventDefault() + window.open(text, '_blank') + } + } + this.terminal = new Terminal({ - convertEol: true + convertEol: true, + linkHandler }) const fitAddon = new FitAddon() this.terminal.loadAddon(fitAddon) this.terminal.fit = fitAddon + + this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri))) + this.linkHandlerConfigured = true } async write (out, then) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3bc98c8..47b2ac6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@hugeicons/vue": "^1.0.4", "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", "picocrank": "^1.14.0", @@ -1570,6 +1571,12 @@ "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", "license": "MIT" }, + "node_modules/@xterm/addon-web-links": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", + "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", + "license": "MIT" + }, "node_modules/@xterm/xterm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a64d473..a08f799 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,13 +28,14 @@ "@hugeicons/vue": "^1.0.4", "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", "picocrank": "^1.14.0", "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.29", + "vue": "^3.5.29", "vue-i18n": "^11.2.8", "vue-router": "^5.0.3" } diff --git a/integration-tests/tests/xtermLinkHandling/config.yaml b/integration-tests/tests/xtermLinkHandling/config.yaml new file mode 100644 index 0000000..f70848d --- /dev/null +++ b/integration-tests/tests/xtermLinkHandling/config.yaml @@ -0,0 +1,13 @@ +# +# Integration Test Config: xterm link handling +# + +listenAddressSingleHTTPFrontend: 0.0.0.0:1337 + +logLevel: "DEBUG" +checkForUpdates: false + +actions: + - title: Echo URL + shell: echo "See https://example.com for more info" + popupOnStart: execution-dialog-stdout-only diff --git a/integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs b/integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs new file mode 100644 index 0000000..849f5a0 --- /dev/null +++ b/integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs @@ -0,0 +1,69 @@ +import { describe, it, before, after } from 'mocha' +import { expect } from 'chai' +import { By, Condition } from 'selenium-webdriver' +import { + getRootAndWait, + takeScreenshotOnFailure, + getTerminalBuffer, +} from '../../lib/elements.js' + +describe('config: xtermLinkHandling', function () { + before(async function () { + await runner.start('xtermLinkHandling') + }) + + after(async () => { + await runner.stop() + }) + + afterEach(function () { + takeScreenshotOnFailure(this.currentTest, webdriver) + }) + + it('xterm output shows URL and link handling is configured', async function () { + await getRootAndWait() + + await webdriver.wait(new Condition('wait for Echo URL button', async () => { + const btns = await webdriver.findElements(By.css('[title="Echo URL"]')) + return btns.length === 1 + }), 10000) + + const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]')) + await echoUrlButton.click() + + await webdriver.wait(new Condition('wait for execution view', async () => { + const url = await webdriver.getCurrentUrl() + return url.includes('/logs/') && !url.endsWith('/logs') + }), 10000) + + await webdriver.wait(new Condition('wait for execution status', async () => { + const statusElements = await webdriver.findElements(By.id('execution-dialog-status')) + return statusElements.length > 0 + }), 5000) + + await webdriver.wait(new Condition('wait for execution to finish', async () => { + try { + const statusElement = await webdriver.findElement(By.id('execution-dialog-status')) + const statusText = await statusElement.getText() + return !statusText.includes('Executing') + } catch (e) { + return false + } + }), 5000) + + await webdriver.sleep(500) + + const bufferText = await getTerminalBuffer() + expect(bufferText).to.not.be.null + expect(bufferText).to.include('https://example.com') + + const linkHandlerSet = await webdriver.executeScript(` + try { + return !!(window.terminal && window.terminal.linkHandlerConfigured === true) + } catch (e) { + return false + } + `) + expect(linkHandlerSet).to.equal(true) + }) +}) From f044d90d5525c4c8e3f421b32ed7eff771c22d36 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 1 Mar 2026 23:52:25 +0000 Subject: [PATCH 036/148] security: Remote crash in OAuth2 GHSA-45m3-398w-m2m9 Thanks @kule500 for the responsible disclosure. CVE to follow. --- .../auth/otoauth2/restapi_auth_oauth2.go | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/service/internal/auth/otoauth2/restapi_auth_oauth2.go b/service/internal/auth/otoauth2/restapi_auth_oauth2.go index 5266124..a84e1f6 100644 --- a/service/internal/auth/otoauth2/restapi_auth_oauth2.go +++ b/service/internal/auth/otoauth2/restapi_auth_oauth2.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "os" + "sync" "time" authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic" @@ -21,6 +22,7 @@ import ( type OAuth2Handler struct { cfg *config.Config + mu sync.RWMutex registeredStates map[string]*oauth2State registeredProviders map[string]*oauth2.Config } @@ -144,11 +146,13 @@ func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) return } + h.mu.Lock() h.registeredStates[state] = &oauth2State{ providerConfig: provider, providerName: providerName, Username: "", } + h.mu.Unlock() h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state) @@ -177,7 +181,9 @@ func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http. return nil, state, false } + h.mu.RLock() registeredState, ok := h.registeredStates[state] + h.mu.RUnlock() if !ok { log.Errorf("State not found in server: %v", state) http.Error(w, "State not found in server", http.StatusBadRequest) @@ -287,8 +293,10 @@ func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Reque userInfoClient := h.createUserInfoClient(ctx, registeredState.providerConfig, tok, clientSettings) userinfo := getUserInfo(h.cfg, userInfoClient, providerConfig) + h.mu.Lock() h.registeredStates[state].Username = userinfo.Username h.registeredStates[state].Usergroup = h.computeUsergroup(userinfo, providerConfig) + h.mu.Unlock() http.Redirect(w, r, "/", http.StatusFound) } @@ -366,34 +374,36 @@ func getDataField(data map[string]any, field string) string { return stringVal } +func (h *OAuth2Handler) lookupOAuth2UserByState(state string) (*authTypes.AuthenticatedUser, bool) { + h.mu.RLock() + serverState, found := h.registeredStates[state] + if !found { + h.mu.RUnlock() + return nil, false + } + user := &authTypes.AuthenticatedUser{ + Username: serverState.Username, + UsergroupLine: serverState.Usergroup, + Provider: "oauth2", + SID: state, + } + h.mu.RUnlock() + return user, true +} + func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser { cookie, err := context.Request.Cookie("olivetin-sid-oauth") - - user := &authTypes.AuthenticatedUser{} - - if err != nil { + if err != nil || cookie.Value == "" { return nil } - if cookie.Value == "" { - return nil - } - - serverState, found := h.registeredStates[cookie.Value] - + user, found := h.lookupOAuth2UserByState(cookie.Value) if !found { log.WithFields(log.Fields{ "sid": cookie.Value, "provider": "oauth2", }).Warnf("Stale session") - return nil } - - user.Username = serverState.Username - user.UsergroupLine = serverState.Usergroup - user.Provider = "oauth2" - user.SID = cookie.Value - return user } From d9804182eae43cf49f735e6533ddbe1541c2b9a9 Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 2 Mar 2026 00:29:02 +0000 Subject: [PATCH 037/148] security: GHSA-4fqm-6fmh-82mq Authentication bypass in KillAction - thanks for the responsible disclosure @kule500 and making OliveTin better --- service/internal/config/sanitize.go | 1 + 1 file changed, 1 insertion(+) diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index f699b80..ddeac5b 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -164,6 +164,7 @@ func (cfg *Config) sanitizeAuthRequireGuestsToLogin() { cfg.DefaultPermissions.View = false cfg.DefaultPermissions.Exec = false cfg.DefaultPermissions.Logs = false + cfg.DefaultPermissions.Kill = false } } From 87148f05bd2541c6a8d02120c2675c23aa910fd4 Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 2 Mar 2026 00:44:49 +0000 Subject: [PATCH 038/148] chore: Update SECURITY.md --- SECURITY.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index cc1a212..13074e1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,6 +31,12 @@ Please use responsible disclosure practices when reporting a vulnerability. **Yo * **Option B**: Please email `contact@jread.com` for responsible disclosure. +The following notes might be helpful when reporting a vulnerability: + +* OliveTin does not offer a bug bounty program. +* GitHub usernames are how we you will be credited for discoveries reported via GitHub, if using emails we'll ask for your preferred name/handle to credit you with. +* CVEs will be requested via GitHub Security Advisories when appropriate, but we do not guarantee that all vulnerabilities will receive CVEs, as this is determined on a case-by-case basis. + ## 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. From bb14c5da3e64b03f207c7f38139eb60e97c278fc Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 4 Mar 2026 22:51:58 +0000 Subject: [PATCH 039/148] security: (MED) GHSA-fwhj-785h-43hh Crash on NPE by calling APIs with invalid bindings or log references --- service/internal/api/api.go | 115 +++++++++++++++++++---------- service/internal/api/apiActions.go | 52 ++++++++----- service/internal/api/dashboards.go | 5 +- 3 files changed, 111 insertions(+), 61 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 629d039..2b9fe42 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -70,20 +70,21 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.K execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId) if !ret.Found { - log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + if execReqLogEntry.Binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId)) + } action := execReqLogEntry.Binding.Action if action == nil { - log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) - ret.Killed = false - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } + log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + user := auth.UserFromApiCall(ctx, req, api.cfg) api.killActionByTrackingId(user, action, execReqLogEntry, ret) @@ -205,42 +206,58 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api return response, nil } -func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) { - args := make(map[string]string) - - for _, arg := range req.Msg.Arguments { - args[arg.Name] = arg.Value - } - - user := auth.UserFromApiCall(ctx, req, api.cfg) - +func (api *oliveTinAPI) startActionAndWaitRun(binding *executor.ActionBinding, args map[string]string, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, bool) { execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: user, Cfg: api.cfg, } - wg, _ := api.executor.ExecRequest(&execReq) wg.Wait() + return api.executor.GetLog(execReq.TrackingID) +} - internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID) - - if ok { - return connect.NewResponse(&apiv1.StartActionAndWaitResponse{ - LogEntry: api.internalLogEntryToPb(internalLogEntry, user), - }), nil - } else { - return nil, fmt.Errorf("execution not found") +func (api *oliveTinAPI) findBindingOrNotFound(actionId string) (*executor.ActionBinding, error) { + binding := api.executor.FindBindingByID(actionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", actionId)) } + return binding, nil +} + +func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) { + binding, err := api.findBindingOrNotFound(req.Msg.ActionId) + if err != nil { + return nil, err + } + + args := make(map[string]string) + for _, arg := range req.Msg.Arguments { + args[arg.Name] = arg.Value + } + user := auth.UserFromApiCall(ctx, req, api.cfg) + + internalLogEntry, ok := api.startActionAndWaitRun(binding, args, user) + if !ok { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) + } + return connect.NewResponse(&apiv1.StartActionAndWaitResponse{ + LogEntry: api.internalLogEntryToPb(internalLogEntry, user), + }), nil } func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) { + binding := api.executor.FindBindingByID(req.Msg.ActionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId)) + } + args := make(map[string]string) execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: auth.UserFromApiCall(ctx, req, api.cfg), @@ -255,12 +272,17 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a } func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) { + binding := api.executor.FindBindingByID(req.Msg.ActionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId)) + } + args := make(map[string]string) user := auth.UserFromApiCall(ctx, req, api.cfg) execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: user, @@ -276,9 +298,8 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{ LogEntry: api.internalLogEntryToPb(internalLogEntry, user), }), nil - } else { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string { @@ -436,7 +457,7 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a binding := api.executor.FindBindingByID(req.Msg.BindingId) - if binding == nil { + if binding == nil || binding.Action == nil { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId)) } @@ -646,7 +667,16 @@ error messages more quickly before starting the action. It uses the same validation logic as the executor, including mangling argument values (e.g., datetime formatting, checkbox title-to-value conversion). */ +func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumentTypeRequest) bool { + arg, _ := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName) + return arg == nil && (msg.BindingId != "" || msg.ArgumentName != "") +} + func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) { + if api.argumentNotFoundForValidation(req.Msg) { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action or argument not found for binding ID %s", req.Msg.BindingId)) + } + err := api.validateArgumentTypeInternal(req.Msg) desc := "" if err != nil { @@ -747,6 +777,13 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum return connect.NewResponse(res), nil } +func debugBindingActionTitle(binding *executor.ActionBinding) string { + if binding == nil || binding.Action == nil { + return "" + } + return binding.Action.Title +} + func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) { res := &apiv1.DumpPublicIdActionMapResponse{} res.Contents = make(map[string]*apiv1.DebugBinding) @@ -761,7 +798,7 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Requ for k, v := range api.executor.MapActionBindings { res.Contents[k] = &apiv1.DebugBinding{ - ActionTitle: v.Action.Title, + ActionTitle: debugBindingActionTitle(v), } } @@ -1267,28 +1304,26 @@ func serializeEntityFields(data any) map[string]string { } func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv1.RestartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) { - ret := &apiv1.StartActionResponse{ - ExecutionTrackingId: req.Msg.ExecutionTrackingId, - } - var execReqLogEntry *executor.InternalLogEntry execReqLogEntry, found := api.executor.GetLog(req.Msg.ExecutionTrackingId) if !found { - log.Warnf("Restarting execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - log.Warnf("Restarting execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + if execReqLogEntry.Binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId)) + } action := execReqLogEntry.Binding.Action if action == nil { - log.Warnf("Restarting execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } + log.Warnf("Restarting execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{ Msg: &apiv1.StartActionRequest{ BindingId: execReqLogEntry.GetBindingId(), diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 355f179..13bcb4a 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -28,18 +28,19 @@ func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action { return rr.findActionForEntity(title, nil) } +func bindingMatchesTitleAndEntity(binding *executor.ActionBinding, title string, entity *entities.Entity) bool { + return binding != nil && binding.Action != nil && binding.Action.Title == title && matchesEntity(binding, entity) +} + func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action { rr.ex.MapActionBindingsLock.RLock() defer rr.ex.MapActionBindingsLock.RUnlock() for _, binding := range rr.ex.MapActionBindings { - if binding.Action.Title != title { + if !bindingMatchesTitleAndEntity(binding, title, entity) { continue } - - if matchesEntity(binding, entity) { - return buildAction(binding, rr) - } + return buildAction(binding, rr) } return nil @@ -117,26 +118,37 @@ func getDefaultArgumentValue(cfgArg config.ActionArgument, entity *entities.Enti return defaultValue } +func formatRateLimitExpiry(expiryUnix int64) string { + if expiryUnix <= 0 { + return "" + } + return time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05") +} + +func actionFromBinding(actionBinding *executor.ActionBinding) (*executor.ActionBinding, *config.Action) { + if actionBinding == nil || actionBinding.Action == nil { + return nil, nil + } + return actionBinding, actionBinding.Action +} + func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action { - action := actionBinding.Action - - aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action) - enabledExprCanExec := evaluateEnabledExpression(action, actionBinding.Entity) - - // Calculate rate limit expiry time - expiryUnix := rr.ex.GetTimeUntilAvailable(actionBinding) - datetimeRateLimitExpires := "" - if expiryUnix > 0 { - datetimeRateLimitExpires = time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05") + binding, action := actionFromBinding(actionBinding) + if binding == nil { + return nil } + aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action) + enabledExprCanExec := evaluateEnabledExpression(action, binding.Entity) + datetimeRateLimitExpires := formatRateLimitExpiry(rr.ex.GetTimeUntilAvailable(binding)) + btn := apiv1.Action{ - BindingId: actionBinding.ID, - Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, actionBinding.Entity), - Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, actionBinding.Entity), + BindingId: binding.ID, + Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, binding.Entity), + Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, binding.Entity), CanExec: aclCanExec && enabledExprCanExec, PopupOnStart: action.PopupOnStart, - Order: int32(actionBinding.ConfigOrder), + Order: int32(binding.ConfigOrder), Timeout: int32(action.Timeout), DatetimeRateLimitExpires: datetimeRateLimitExpires, } @@ -147,7 +159,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque Title: cfgArg.Title, Type: cfgArg.Type, Description: cfgArg.Description, - DefaultValue: getDefaultArgumentValue(cfgArg, actionBinding.Entity), + DefaultValue: getDefaultArgumentValue(cfgArg, binding.Entity), Choices: buildChoices(cfgArg), Suggestions: cfgArg.Suggestions, SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey, diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 11c6cf3..2bc2244 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -130,7 +130,7 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { } for _, binding := range rr.ex.MapActionBindings { - if binding.Action.Hidden { + if binding == nil || binding.Action == nil || binding.Action.Hidden { continue } @@ -139,6 +139,9 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { } action := buildAction(binding, rr) + if action == nil { + continue + } fieldset.Contents = append(fieldset.Contents, &apiv1.DashboardComponent{ Type: "link", From 0c47564652367749c255b22013ce5af82085e4ad Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 2 Mar 2026 13:48:22 +0000 Subject: [PATCH 040/148] chore: logs are written with 0600 instead of 0644 --- service/internal/executor/executor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 91312d4..65f7be6 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -1059,7 +1059,7 @@ func saveLogResults(req *ExecutionRequest, filename string) { } filepath := path.Join(dir, filename+".yaml") - err = os.WriteFile(filepath, data, 0644) + err = os.WriteFile(filepath, data, 0600) if err != nil { log.Warnf("%v", err) @@ -1073,7 +1073,7 @@ func saveLogOutput(req *ExecutionRequest, filename string) { if dir != "" { data := req.logEntry.Output filepath := path.Join(dir, filename+".log") - err := os.WriteFile(filepath, []byte(data), 0644) + err := os.WriteFile(filepath, []byte(data), 0600) if err != nil { log.Warnf("%v", err) From 00cb5a2abfd024e6de680af4256687056dc9a041 Mon Sep 17 00:00:00 2001 From: jamesread Date: Tue, 3 Mar 2026 22:20:45 +0000 Subject: [PATCH 041/148] fix: action triggers were broken #914 --- service/internal/executor/executor.go | 11 ++- service/internal/executor/executor_test.go | 90 ++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 65f7be6..f6807cf 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -1015,8 +1015,15 @@ func stepTrigger(req *ExecutionRequest) bool { } func triggerLoop(req *ExecutionRequest) { - for _, triggerReq := range req.Binding.Action.Triggers { - binding := req.executor.FindBindingByID(triggerReq) + for _, triggerTitle := range req.Binding.Action.Triggers { + binding := req.executor.findBindingByActionTitle(triggerTitle, "") + if binding == nil { + log.WithFields(log.Fields{ + "triggerTitle": triggerTitle, + "fromAction": req.logEntry.ActionTitle, + }).Warnf("Trigger references unknown action title; skipping") + continue + } trigger := &ExecutionRequest{ Binding: binding, TrackingID: uuid.NewString(), diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index 2608bbe..16f491d 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -2,6 +2,7 @@ package executor import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -395,3 +396,92 @@ func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) { assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"]) assert.Equal(t, "webhook", req.Arguments["ot_username"]) } + +func TestTriggerExecutesTriggeredAction(t *testing.T) { + cfg := config.DefaultConfig() + e := DefaultExecutor(cfg) + helloAction := &config.Action{ + Title: "Hello world", + Shell: "echo 'Hello World!'", + } + triggerAction := &config.Action{ + Title: "Simple action that triggers another action", + Shell: "echo 'Hi'", + Triggers: []string{"Hello world"}, + } + cfg.Actions = append(cfg.Actions, helloAction, triggerAction) + cfg.Sanitize() + e.RebuildActionMap() + + finishedTitles := make(chan string, 4) + collector := &executionFinishedCollector{ch: finishedTitles} + e.AddListener(collector) + + req := &ExecutionRequest{ + AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"), + Cfg: cfg, + Binding: e.FindBindingWithNoEntity(triggerAction), + } + wg, _ := e.ExecRequest(req) + wg.Wait() + + var got []string + for i := 0; i < 2; i++ { + select { + case title := <-finishedTitles: + got = append(got, title) + case <-time.After(2 * time.Second): + t.Fatalf("timed out waiting for execution %d; got %v", i+1, got) + } + } + assert.Contains(t, got, "Hello world", "triggered action must run") + assert.Contains(t, got, "Simple action that triggers another action", "triggering action must run") +} + +func TestTriggerUnknownActionTitleSkipsWithoutPanic(t *testing.T) { + cfg := config.DefaultConfig() + e := DefaultExecutor(cfg) + triggerAction := &config.Action{ + Title: "Action with bad trigger", + Shell: "echo 'ok'", + Triggers: []string{"Nonexistent action"}, + } + cfg.Actions = append(cfg.Actions, triggerAction) + cfg.Sanitize() + e.RebuildActionMap() + + finishedTitles := make(chan string, 4) + collector := &executionFinishedCollector{ch: finishedTitles} + e.AddListener(collector) + + req := &ExecutionRequest{ + AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"), + Cfg: cfg, + Binding: e.FindBindingWithNoEntity(triggerAction), + } + wg, _ := e.ExecRequest(req) + wg.Wait() + + var got []string + select { + case title := <-finishedTitles: + got = append(got, title) + case <-time.After(500 * time.Millisecond): + } + assert.Len(t, got, 1, "only the triggering action runs; unknown trigger is skipped") + assert.Equal(t, "Action with bad trigger", got[0]) +} + +type executionFinishedCollector struct { + ch chan string +} + +func (c *executionFinishedCollector) OnExecutionStarted(_ *InternalLogEntry) {} + +func (c *executionFinishedCollector) OnExecutionFinished(entry *InternalLogEntry) { + c.ch <- entry.ActionTitle +} + +func (c *executionFinishedCollector) OnOutputChunk(_ []byte, _ string) {} + +func (c *executionFinishedCollector) OnActionMapRebuilt() {} From 92a1346edf0e8c6202b6e575806eaf57b9df4ef5 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 4 Mar 2026 23:00:28 +0000 Subject: [PATCH 042/148] docs: update security.md with the fix process --- SECURITY.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 13074e1..49fcf13 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -40,3 +40,16 @@ The following notes might be helpful when reporting a vulnerability: ## 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. + +## Process + +Once a vulnerability is reported, the process is; + +* Accept or reject the report, and communicate with the reporter about next steps. +* If accepted, patch using a temporary branch, and code review will be requested from the original reporter if they are interested. +* The severity of the vulnerability will be assessed using CVSS, and the patch will be prioritized accordingly. +* Once the patch is ready, it will be queued for a release onto the `next` branch (3k) or `release/2k` branch (2k) +* The reporter will be credited in the advistory and the release notes, but not the commit message. +* The commit message will contain a reference to the CVSS score (eg: MED) and the advisory ID. + + From e97d8ecbd8d6ba468c418ca496fcd18f78131233 Mon Sep 17 00:00:00 2001 From: jamesread Date: Tue, 3 Mar 2026 22:47:03 +0000 Subject: [PATCH 043/148] security: GHSA-g962-2j28-3cg9 (HIGH) JWT Audience Validation Bypass in Local Key and HMAC Modes --- service/internal/auth/otjwt/jwt.go | 24 +++++++++++++++++++----- service/internal/auth/otjwt/jwt_test.go | 22 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/service/internal/auth/otjwt/jwt.go b/service/internal/auth/otjwt/jwt.go index 103224a..2b06da5 100644 --- a/service/internal/auth/otjwt/jwt.go +++ b/service/internal/auth/otjwt/jwt.go @@ -33,6 +33,13 @@ func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) { return parseJwtTokenWithHMAC(cfg, jwtString) } +func parserOptionsWithAudience(cfg *config.Config) []jwt.ParserOption { + if cfg.AuthJwtAud == "" { + return nil + } + return []jwt.ParserOption{jwt.WithAudience(cfg.AuthJwtAud)} +} + func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) { token, err := parseJwtToken(cfg, jwtString) @@ -56,7 +63,8 @@ func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token return nil, err } - return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud)) + opts := parserOptionsWithAudience(cfg) + return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, opts...) } var ( @@ -148,24 +156,30 @@ func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token return nil, err } - return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) { + keyFunc := func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("parseJwt expected token algorithm RSA but got: %v", token.Header["alg"]) } return pubKey, nil - }) + } + + opts := parserOptionsWithAudience(cfg) + return jwt.Parse(jwtString, keyFunc, opts...) } // Hash-based Message Authentication Code func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, error) { - return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) { + keyFunc := func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"]) } return []byte(cfg.AuthJwtHmacSecret), nil - }) + } + + opts := parserOptionsWithAudience(cfg) + return jwt.Parse(jwtString, keyFunc, opts...) } func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string { diff --git a/service/internal/auth/otjwt/jwt_test.go b/service/internal/auth/otjwt/jwt_test.go index d3f48a8..ed4e3af 100644 --- a/service/internal/auth/otjwt/jwt_test.go +++ b/service/internal/auth/otjwt/jwt_test.go @@ -66,12 +66,19 @@ func newMux() *http.ServeMux { } func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string { + return createJWTTokenWithExpirationAndAudience(t, privateKey, expire, "") +} + +func createJWTTokenWithExpirationAndAudience(t *testing.T, privateKey *rsa.PrivateKey, expire int64, audience string) string { token := jwt.New(jwt.SigningMethodRS256) claims := token.Claims.(jwt.MapClaims) claims["nbf"] = time.Now().Unix() - 1000 claims["exp"] = time.Now().Unix() + expire claims["sub"] = "test" claims["olivetinGroup"] = "test" + if audience != "" { + claims["aud"] = audience + } tokenStr, err := token.SignedString(privateKey) if err != nil { @@ -108,6 +115,10 @@ func verifyJWTResponse(t *testing.T, res *http.Response, expectCode int) { } func testJwkValidation(t *testing.T, expire int64, expectCode int) { + testJwkValidationWithAudience(t, expire, expectCode, "", "") +} + +func testJwkValidationWithAudience(t *testing.T, expire int64, expectCode int, configAudience, tokenAudience string) { privateKey, publicKeyPath := createKeys(t) defer os.Remove(publicKeyPath) @@ -116,8 +127,9 @@ func testJwkValidation(t *testing.T, expire int64, expectCode int) { cfg.AuthJwtClaimUsername = "sub" cfg.AuthJwtClaimUserGroup = "olivetinGroup" cfg.AuthJwtHeader = "Authorization" + cfg.AuthJwtAud = configAudience - tokenStr := createJWTTokenWithExpiration(t, privateKey, expire) + tokenStr := createJWTTokenWithExpirationAndAudience(t, privateKey, expire, tokenAudience) handler := setupJWTTestHandler(t, cfg) srv := httptest.NewServer(handler) @@ -135,6 +147,14 @@ func TestJWTSignatureVerificationFails(t *testing.T) { testJwkValidation(t, -500, 403) } +func TestJWTAudienceValidationRejectsWrongAudience(t *testing.T) { + testJwkValidationWithAudience(t, 1000, 403, "expected-audience", "wrong-audience") +} + +func TestJWTAudienceValidationAcceptsCorrectAudience(t *testing.T) { + testJwkValidationWithAudience(t, 1000, 200, "expected-audience", "expected-audience") +} + func createJWTTokenWithGroups(t *testing.T, privateKey *rsa.PrivateKey, groups interface{}) string { token := jwt.New(jwt.SigningMethodRS256) claims := token.Claims.(jwt.MapClaims) From d6a0abc3755d43107be1939567c52953bcbec3d5 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 4 Mar 2026 23:31:15 +0000 Subject: [PATCH 044/148] security: GHSA-gq2m-77hf-vwgh (MODERATE) Session Fixation: Logout Fails to Invalidate Server-Side Session --- service/internal/api/api.go | 2 + .../auth/otoauth2/restapi_auth_oauth2.go | 6 +++ service/internal/auth/sessions.go | 37 ++++++++++++++++++- service/internal/httpservers/frontend.go | 1 + 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 629d039..f2c81da 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -392,6 +392,8 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) { user := auth.UserFromApiCall(ctx, req, api.cfg) + auth.RevokeSessionForProvider(api.cfg, user.Provider, user.SID) + log.WithFields(log.Fields{ "username": user.Username, "provider": user.Provider, diff --git a/service/internal/auth/otoauth2/restapi_auth_oauth2.go b/service/internal/auth/otoauth2/restapi_auth_oauth2.go index a84e1f6..351a13b 100644 --- a/service/internal/auth/otoauth2/restapi_auth_oauth2.go +++ b/service/internal/auth/otoauth2/restapi_auth_oauth2.go @@ -391,6 +391,12 @@ func (h *OAuth2Handler) lookupOAuth2UserByState(state string) (*authTypes.Authen return user, true } +func (h *OAuth2Handler) RevokeSession(sid string) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.registeredStates, sid) +} + func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser { cookie, err := context.Request.Cookie("olivetin-sid-oauth") if err != nil || cookie.Value == "" { diff --git a/service/internal/auth/sessions.go b/service/internal/auth/sessions.go index f11fc8a..c8d7416 100644 --- a/service/internal/auth/sessions.go +++ b/service/internal/auth/sessions.go @@ -25,8 +25,9 @@ type SessionStorage struct { } var ( - sessionStorage *SessionStorage - sessionStorageMutex sync.RWMutex + sessionStorage *SessionStorage + sessionStorageMutex sync.RWMutex + oauth2SessionRevoker func(sid string) ) func init() { @@ -58,6 +59,38 @@ func RegisterUserSession(cfg *config.Config, provider string, sid string, userna saveUserSessions(cfg) } +// RegisterOAuth2SessionRevoker registers a callback to revoke OAuth2 sessions on logout. +// OAuth2 uses its own session storage; the API calls this when provider is oauth2. +func RegisterOAuth2SessionRevoker(fn func(sid string)) { + oauth2SessionRevoker = fn +} + +// RevokeSessionForProvider invalidates the session for the given provider and SID (e.g. on logout). +// Local auth uses shared SessionStorage; OAuth2 uses a separate storage and revoker. +func RevokeSessionForProvider(cfg *config.Config, provider string, sid string) { + if sid == "" { + return + } + if provider == "oauth2" && oauth2SessionRevoker != nil { + oauth2SessionRevoker(sid) + return + } + RevokeUserSession(cfg, provider, sid) +} + +// RevokeUserSession removes a session from storage so it can no longer be used (e.g. on logout). +func RevokeUserSession(cfg *config.Config, provider string, sid string) { + sessionStorageMutex.Lock() + defer sessionStorageMutex.Unlock() + + if sessionStorage.Providers[provider] != nil { + delete(sessionStorage.Providers[provider].Sessions, sid) + if cfg != nil { + saveUserSessions(cfg) + } + } +} + // GetUserSession retrieves a user session func GetUserSession(provider string, sid string) *UserSession { sessionStorageMutex.Lock() diff --git a/service/internal/httpservers/frontend.go b/service/internal/httpservers/frontend.go index c1c1a1f..399c6fd 100644 --- a/service/internal/httpservers/frontend.go +++ b/service/internal/httpservers/frontend.go @@ -101,6 +101,7 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) { oauth2handler := otoauth2.NewOAuth2Handler(cfg) auth.AddAuthChainFunction(oauth2handler.CheckUserFromOAuth2Cookie) + auth.RegisterOAuth2SessionRevoker(oauth2handler.RevokeSession) mux.HandleFunc("/oauth/login", oauth2handler.HandleOAuthLogin) mux.HandleFunc("/oauth/callback", oauth2handler.HandleOAuthCallback) From cb46a597b2465235839ed58cf034b5e7b70ef911 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 5 Mar 2026 00:04:58 +0000 Subject: [PATCH 045/148] security: GHSA-p443-p7w5-2f7f (MODERATE) RestartAction always runs actions as guest --- service/internal/api/api.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 629d039..4e0b1db 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -1271,8 +1271,6 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv ExecutionTrackingId: req.Msg.ExecutionTrackingId, } - var execReqLogEntry *executor.InternalLogEntry - execReqLogEntry, found := api.executor.GetLog(req.Msg.ExecutionTrackingId) if !found { @@ -1289,12 +1287,21 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv return connect.NewResponse(ret), nil } - return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{ - Msg: &apiv1.StartActionRequest{ - BindingId: execReqLogEntry.GetBindingId(), - UniqueTrackingId: req.Msg.ExecutionTrackingId, - }, - }) + authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg) + + // TrackingID is deliberately not passed to the executor, so that it generates a new one for the restarted execution. + // This is because the old execution (identified by the old TrackingID) is already used. + execReq := executor.ExecutionRequest{ + Binding: execReqLogEntry.Binding, + Arguments: make(map[string]string), + AuthenticatedUser: authenticatedUser, + Cfg: api.cfg, + } + + api.executor.ExecRequest(&execReq) + + ret.ExecutionTrackingId = execReq.TrackingID + return connect.NewResponse(ret), nil } func newServer(ex *executor.Executor) *oliveTinAPI { From 9d55d4a178573749ba93106748adf4c33269d10f Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 5 Mar 2026 00:24:00 +0000 Subject: [PATCH 046/148] docs: Policy change, 2k will receive security updates much slower --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 49fcf13..9b1ec7f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,8 @@ The following branches are currently being supported with security updates: | Version | Supported | | ------- | ------------------ | -| `main` (3k release branch) | :white_check_mark: | -| `release/2k` (2k release branch) | :white_check_mark: | +| `main` (3k release branch) | :white_check_mark: - advisories will be published when patched in this branch | +| `release/2k` (2k release branch) | :white_check_mark: - receives security updates, but much slower | To understand more about 2k vs 3k, see the following docs; https://docs.olivetin.app/upgrade/2k3k.html From 10f5ba62a261190efa472651b409cccd79b08d98 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 5 Mar 2026 08:07:56 +0000 Subject: [PATCH 047/148] docs: typos in SECURITY.md --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 9b1ec7f..f4268df 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -47,9 +47,9 @@ Once a vulnerability is reported, the process is; * Accept or reject the report, and communicate with the reporter about next steps. * If accepted, patch using a temporary branch, and code review will be requested from the original reporter if they are interested. -* The severity of the vulnerability will be assessed using CVSS, and the patch will be prioritized accordingly. +* The severity of the vulnerability will be assessed using CVSS, and the patch will be prioritised accordingly. * Once the patch is ready, it will be queued for a release onto the `next` branch (3k) or `release/2k` branch (2k) -* The reporter will be credited in the advistory and the release notes, but not the commit message. +* The reporter will be credited in the advisory and the release notes, but not the commit message. * The commit message will contain a reference to the CVSS score (eg: MED) and the advisory ID. From 9080577f2b7921add35dee70df8b6c24cfd6a184 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 5 Mar 2026 08:10:56 +0000 Subject: [PATCH 048/148] chore: potential crash in unit tests --- service/internal/executor/executor_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index 16f491d..a36fc10 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -469,7 +469,10 @@ func TestTriggerUnknownActionTitleSkipsWithoutPanic(t *testing.T) { case <-time.After(500 * time.Millisecond): } assert.Len(t, got, 1, "only the triggering action runs; unknown trigger is skipped") - assert.Equal(t, "Action with bad trigger", got[0]) + + if len(got) > 0 { + assert.Equal(t, "Action with bad trigger", got[0]) + } } type executionFinishedCollector struct { From d7962710e7c46f6bdda4188b5b0cdbde4be665a0 Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 5 Mar 2026 08:20:02 +0000 Subject: [PATCH 049/148] security: GHSA-jf73-858c-54pg (MODERATE) View permission not being checked when returning dashboards --- service/internal/api/api.go | 163 ++++++++++++++++++++--------- service/internal/api/apiActions.go | 53 ++++++---- service/internal/api/api_test.go | 115 ++++++++++++++++++++ service/internal/api/dashboards.go | 10 +- 4 files changed, 272 insertions(+), 69 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 629d039..a9e8c8a 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -70,20 +70,21 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.K execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId) if !ret.Found { - log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + if execReqLogEntry.Binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId)) + } action := execReqLogEntry.Binding.Action if action == nil { - log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) - ret.Killed = false - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } + log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + user := auth.UserFromApiCall(ctx, req, api.cfg) api.killActionByTrackingId(user, action, execReqLogEntry, ret) @@ -205,42 +206,58 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api return response, nil } -func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) { - args := make(map[string]string) - - for _, arg := range req.Msg.Arguments { - args[arg.Name] = arg.Value - } - - user := auth.UserFromApiCall(ctx, req, api.cfg) - +func (api *oliveTinAPI) startActionAndWaitRun(binding *executor.ActionBinding, args map[string]string, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, bool) { execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: user, Cfg: api.cfg, } - wg, _ := api.executor.ExecRequest(&execReq) wg.Wait() + return api.executor.GetLog(execReq.TrackingID) +} - internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID) - - if ok { - return connect.NewResponse(&apiv1.StartActionAndWaitResponse{ - LogEntry: api.internalLogEntryToPb(internalLogEntry, user), - }), nil - } else { - return nil, fmt.Errorf("execution not found") +func (api *oliveTinAPI) findBindingOrNotFound(actionId string) (*executor.ActionBinding, error) { + binding := api.executor.FindBindingByID(actionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", actionId)) } + return binding, nil +} + +func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) { + binding, err := api.findBindingOrNotFound(req.Msg.ActionId) + if err != nil { + return nil, err + } + + args := make(map[string]string) + for _, arg := range req.Msg.Arguments { + args[arg.Name] = arg.Value + } + user := auth.UserFromApiCall(ctx, req, api.cfg) + + internalLogEntry, ok := api.startActionAndWaitRun(binding, args, user) + if !ok { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) + } + return connect.NewResponse(&apiv1.StartActionAndWaitResponse{ + LogEntry: api.internalLogEntryToPb(internalLogEntry, user), + }), nil } func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) { + binding := api.executor.FindBindingByID(req.Msg.ActionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId)) + } + args := make(map[string]string) execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: auth.UserFromApiCall(ctx, req, api.cfg), @@ -255,12 +272,17 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a } func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) { + binding := api.executor.FindBindingByID(req.Msg.ActionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId)) + } + args := make(map[string]string) user := auth.UserFromApiCall(ctx, req, api.cfg) execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: user, @@ -276,9 +298,8 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{ LogEntry: api.internalLogEntryToPb(internalLogEntry, user), }), nil - } else { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string { @@ -392,6 +413,8 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) { user := auth.UserFromApiCall(ctx, req, api.cfg) + auth.RevokeSessionForProvider(api.cfg, user.Provider, user.SID) + log.WithFields(log.Fields{ "username": user.Username, "provider": user.Provider, @@ -434,19 +457,38 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a return nil, err } - binding := api.executor.FindBindingByID(req.Msg.BindingId) - - if binding == nil { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId)) + resp, err := api.getActionBindingResponse(user, req.Msg.BindingId) + if err != nil { + return nil, err } + return connect.NewResponse(resp), nil +} - return connect.NewResponse(&apiv1.GetActionBindingResponse{ +func (api *oliveTinAPI) getActionBindingResponse(user *authpublic.AuthenticatedUser, bindingId string) (*apiv1.GetActionBindingResponse, error) { + binding := api.executor.FindBindingByID(bindingId) + if binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", bindingId)) + } + if binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", bindingId)) + } + if !api.userCanViewAction(user, binding.Action) { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied")) + } + return &apiv1.GetActionBindingResponse{ Action: buildAction(binding, &DashboardRenderRequest{ cfg: api.cfg, AuthenticatedUser: user, ex: api.executor, }), - }), nil + }, nil +} + +func (api *oliveTinAPI) userCanViewAction(user *authpublic.AuthenticatedUser, action *config.Action) bool { + if user == nil { + return true + } + return acl.IsAllowedView(api.cfg, user, action) } func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) { @@ -646,7 +688,16 @@ error messages more quickly before starting the action. It uses the same validation logic as the executor, including mangling argument values (e.g., datetime formatting, checkbox title-to-value conversion). */ +func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumentTypeRequest) bool { + arg, _ := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName) + return arg == nil && (msg.BindingId != "" || msg.ArgumentName != "") +} + func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) { + if api.argumentNotFoundForValidation(req.Msg) { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action or argument not found for binding ID %s", req.Msg.BindingId)) + } + err := api.validateArgumentTypeInternal(req.Msg) desc := "" if err != nil { @@ -747,6 +798,13 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum return connect.NewResponse(res), nil } +func debugBindingActionTitle(binding *executor.ActionBinding) string { + if binding == nil || binding.Action == nil { + return "" + } + return binding.Action.Title +} + func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) { res := &apiv1.DumpPublicIdActionMapResponse{} res.Contents = make(map[string]*apiv1.DebugBinding) @@ -761,7 +819,7 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Requ for k, v := range api.executor.MapActionBindings { res.Contents[k] = &apiv1.DebugBinding{ - ActionTitle: v.Action.Title, + ActionTitle: debugBindingActionTitle(v), } } @@ -1271,30 +1329,37 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv ExecutionTrackingId: req.Msg.ExecutionTrackingId, } - var execReqLogEntry *executor.InternalLogEntry - execReqLogEntry, found := api.executor.GetLog(req.Msg.ExecutionTrackingId) if !found { - log.Warnf("Restarting execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - log.Warnf("Restarting execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + if execReqLogEntry.Binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId)) + } action := execReqLogEntry.Binding.Action if action == nil { - log.Warnf("Restarting execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{ - Msg: &apiv1.StartActionRequest{ - BindingId: execReqLogEntry.GetBindingId(), - UniqueTrackingId: req.Msg.ExecutionTrackingId, - }, - }) + authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg) + + // TrackingID is deliberately not passed to the executor, so that it generates a new one for the restarted execution. + // This is because the old execution (identified by the old TrackingID) is already used. + execReq := executor.ExecutionRequest{ + Binding: execReqLogEntry.Binding, + Arguments: make(map[string]string), + AuthenticatedUser: authenticatedUser, + Cfg: api.cfg, + } + + api.executor.ExecRequest(&execReq) + + ret.ExecutionTrackingId = execReq.TrackingID + return connect.NewResponse(ret), nil } func newServer(ex *executor.Executor) *oliveTinAPI { diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 355f179..4a23125 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -28,18 +28,22 @@ func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action { return rr.findActionForEntity(title, nil) } +func bindingMatchesTitleAndEntity(binding *executor.ActionBinding, title string, entity *entities.Entity) bool { + return binding != nil && binding.Action != nil && binding.Action.Title == title && matchesEntity(binding, entity) +} + func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action { rr.ex.MapActionBindingsLock.RLock() defer rr.ex.MapActionBindingsLock.RUnlock() for _, binding := range rr.ex.MapActionBindings { - if binding.Action.Title != title { + if !bindingMatchesTitleAndEntity(binding, title, entity) { continue } - - if matchesEntity(binding, entity) { - return buildAction(binding, rr) + if !acl.IsAllowedView(rr.cfg, rr.AuthenticatedUser, binding.Action) { + return nil } + return buildAction(binding, rr) } return nil @@ -117,26 +121,37 @@ func getDefaultArgumentValue(cfgArg config.ActionArgument, entity *entities.Enti return defaultValue } +func formatRateLimitExpiry(expiryUnix int64) string { + if expiryUnix <= 0 { + return "" + } + return time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05") +} + +func actionFromBinding(actionBinding *executor.ActionBinding) (*executor.ActionBinding, *config.Action) { + if actionBinding == nil || actionBinding.Action == nil { + return nil, nil + } + return actionBinding, actionBinding.Action +} + func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action { - action := actionBinding.Action - - aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action) - enabledExprCanExec := evaluateEnabledExpression(action, actionBinding.Entity) - - // Calculate rate limit expiry time - expiryUnix := rr.ex.GetTimeUntilAvailable(actionBinding) - datetimeRateLimitExpires := "" - if expiryUnix > 0 { - datetimeRateLimitExpires = time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05") + binding, action := actionFromBinding(actionBinding) + if binding == nil { + return nil } + aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action) + enabledExprCanExec := evaluateEnabledExpression(action, binding.Entity) + datetimeRateLimitExpires := formatRateLimitExpiry(rr.ex.GetTimeUntilAvailable(binding)) + btn := apiv1.Action{ - BindingId: actionBinding.ID, - Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, actionBinding.Entity), - Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, actionBinding.Entity), + BindingId: binding.ID, + Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, binding.Entity), + Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, binding.Entity), CanExec: aclCanExec && enabledExprCanExec, PopupOnStart: action.PopupOnStart, - Order: int32(actionBinding.ConfigOrder), + Order: int32(binding.ConfigOrder), Timeout: int32(action.Timeout), DatetimeRateLimitExpires: datetimeRateLimitExpires, } @@ -147,7 +162,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque Title: cfgArg.Title, Type: cfgArg.Type, Description: cfgArg.Description, - DefaultValue: getDefaultArgumentValue(cfgArg, actionBinding.Entity), + DefaultValue: getDefaultArgumentValue(cfgArg, binding.Entity), Choices: buildChoices(cfgArg), Suggestions: cfgArg.Suggestions, SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey, diff --git a/service/internal/api/api_test.go b/service/internal/api/api_test.go index 790e315..6e24d33 100644 --- a/service/internal/api/api_test.go +++ b/service/internal/api/api_test.go @@ -6,6 +6,7 @@ import ( "connectrpc.com/connect" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" log "github.com/sirupsen/logrus" @@ -335,3 +336,117 @@ func testWithEntity(t *testing.T, binding *executor.ActionBinding, rr *Dashboard actionResult := buildAction(binding, rr) assert.Equal(t, expectedCanExec, actionResult.CanExec, message) } + +// buildViewPermissionTestConfig returns config and users for GHSA view-permission tests: +// one action "secret_action", ACL "restricted" (view:false) for user "low", ACL "full" (view:true) for user "admin". +func buildViewPermissionTestConfig(t *testing.T) (*config.Config, *authpublic.AuthenticatedUser, *authpublic.AuthenticatedUser) { + t.Helper() + cfg := config.DefaultConfig() + cfg.DefaultPermissions.View = false + cfg.DefaultPermissions.Exec = false + + cfg.Actions = append(cfg.Actions, &config.Action{ + ID: "secret_action", + Title: "Secret Action", + Shell: "echo sensitive", + Icon: "🔒", + }) + + cfg.AccessControlLists = append(cfg.AccessControlLists, + &config.AccessControlList{ + Name: "restricted", + MatchUsernames: []string{"low"}, + AddToEveryAction: true, + Permissions: config.PermissionsList{View: false, Exec: false, Logs: false, Kill: false}, + }, + &config.AccessControlList{ + Name: "full", + MatchUsernames: []string{"admin"}, + AddToEveryAction: true, + Permissions: config.PermissionsList{View: true, Exec: true, Logs: true, Kill: true}, + }, + ) + + lowUser := &authpublic.AuthenticatedUser{Username: "low"} + lowUser.BuildUserAcls(cfg) + adminUser := &authpublic.AuthenticatedUser{Username: "admin"} + adminUser.BuildUserAcls(cfg) + return cfg, lowUser, adminUser +} + +// TestViewPermissionExcludedFromDashboard (GHSA: view permission) asserts that when a user has view: false, +// the default dashboard must not include that action. Covers GetDashboard not leaking action metadata. +func TestViewPermissionExcludedFromDashboard(t *testing.T) { + cfg, lowUser, _ := buildViewPermissionTestConfig(t) + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + + rr := &DashboardRenderRequest{ + AuthenticatedUser: lowUser, + cfg: cfg, + ex: ex, + } + db := buildDefaultDashboard(rr) + + bindingIdsInDashboard := bindingIdsInDashboardContents(db.Contents) + assert.NotContains(t, bindingIdsInDashboard, "secret_action", + "user with view:false must not see action in dashboard; got bindingIds: %v", bindingIdsInDashboard) +} + +// TestGetActionBindingDeniedWhenNoViewPermission (GHSA: view permission) asserts that GetActionBinding +// returns permission denied for a user with view: false. Covers GetActionBinding not exposing action details. +func TestGetActionBindingDeniedWhenNoViewPermission(t *testing.T) { + cfg, lowUser, _ := buildViewPermissionTestConfig(t) + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + api := newServer(ex) + + _, err := api.getActionBindingResponse(lowUser, "secret_action") + require.Error(t, err) + assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err), + "user with view:false must get permission denied from GetActionBinding") +} + +// TestViewPermissionAllowedSeesAction (GHSA: view permission) asserts that a user with view: true +// still sees the action in the dashboard and can fetch it via GetActionBinding. +func TestViewPermissionAllowedSeesAction(t *testing.T) { + cfg, _, adminUser := buildViewPermissionTestConfig(t) + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + api := newServer(ex) + + rr := &DashboardRenderRequest{ + AuthenticatedUser: adminUser, + cfg: cfg, + ex: ex, + } + db := buildDefaultDashboard(rr) + bindingIdsInDashboard := bindingIdsInDashboardContents(db.Contents) + assert.Contains(t, bindingIdsInDashboard, "secret_action", + "user with view:true must see action in dashboard; got bindingIds: %v", bindingIdsInDashboard) + + resp, err := api.getActionBindingResponse(adminUser, "secret_action") + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Action) + assert.Equal(t, "secret_action", resp.Action.BindingId) +} + +func bindingIdsInDashboardContents(contents []*apiv1.DashboardComponent) []string { + var ids []string + for _, c := range contents { + ids = append(ids, bindingIdsFromComponent(c)...) + } + return ids +} + +func bindingIdsFromComponent(c *apiv1.DashboardComponent) []string { + if c == nil { + return nil + } + var ids []string + if c.Action != nil && c.Action.BindingId != "" { + ids = append(ids, c.Action.BindingId) + } + return append(ids, bindingIdsInDashboardContents(c.Contents)...) +} diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 11c6cf3..575994c 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -4,6 +4,7 @@ import ( "sort" apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" + acl "github.com/OliveTin/OliveTin/internal/acl" config "github.com/OliveTin/OliveTin/internal/config" entities "github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/tpl" @@ -130,7 +131,7 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { } for _, binding := range rr.ex.MapActionBindings { - if binding.Action.Hidden { + if binding == nil || binding.Action == nil || binding.Action.Hidden { continue } @@ -138,7 +139,14 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { continue } + if !acl.IsAllowedView(rr.cfg, rr.AuthenticatedUser, binding.Action) { + continue + } + action := buildAction(binding, rr) + if action == nil { + continue + } fieldset.Contents = append(fieldset.Contents, &apiv1.DashboardComponent{ Type: "link", From b032ae5e5e3fc5f9a0d3c8f66b9dce7bd276c9ef Mon Sep 17 00:00:00 2001 From: jamesread Date: Thu, 5 Mar 2026 08:29:30 +0000 Subject: [PATCH 050/148] chore: fix regression on arguments not being found --- service/internal/api/api.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 20e41dd..83465aa 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -466,15 +466,15 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a func (api *oliveTinAPI) getActionBindingResponse(user *authpublic.AuthenticatedUser, bindingId string) (*apiv1.GetActionBindingResponse, error) { binding := api.executor.FindBindingByID(bindingId) - + if binding == nil || binding.Action == nil { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", bindingId)) } - + if !api.userCanViewAction(user, binding.Action) { return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied")) } - + return &apiv1.GetActionBindingResponse{ Action: buildAction(binding, &DashboardRenderRequest{ cfg: api.cfg, @@ -689,8 +689,13 @@ It uses the same validation logic as the executor, including mangling argument values (e.g., datetime formatting, checkbox title-to-value conversion). */ func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumentTypeRequest) bool { + if msg.BindingId == "" || msg.ArgumentName == "" { + return false + } + arg, _ := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName) - return arg == nil && (msg.BindingId != "" || msg.ArgumentName != "") + + return arg == nil } func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) { From 3f46007281769e09704c339231fc260fd4a41e72 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 8 Mar 2026 20:46:52 +0000 Subject: [PATCH 051/148] fix: Relax default CSP to allow iconify to work --- service/internal/config/config.go | 2 +- service/internal/config/sanitize.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 82c466f..742f6bf 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -281,7 +281,7 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.Prometheus.Enabled = false config.Prometheus.DefaultGoMetrics = false config.Security.HeaderContentSecurityPolicy = true - config.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'" + config.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'" config.Security.HeaderXContentTypeOptions = true config.Security.HeaderXFrameOptions = true config.Security.XFrameOptions = "DENY" diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index ddeac5b..f612edb 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -194,7 +194,7 @@ func (cfg *Config) sanitizeSecurityHeadersCSP() { if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" { return } - cfg.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'" + cfg.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'" } func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() { From 0fee24089fba815a219a158d178a7428c53f64e9 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 8 Mar 2026 21:11:27 +0000 Subject: [PATCH 052/148] fix: Restart action button was not working --- frontend/resources/vue/views/ExecutionView.vue | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/resources/vue/views/ExecutionView.vue b/frontend/resources/vue/views/ExecutionView.vue index 169572d..a8de151 100644 --- a/frontend/resources/vue/views/ExecutionView.vue +++ b/frontend/resources/vue/views/ExecutionView.vue @@ -169,14 +169,15 @@ function show(actionButton) { } async function rerunAction() { - if (!logEntry.value || !logEntry.value.actionId) { + const bindingId = logEntry.value?.bindingId + if (!logEntry.value || !bindingId) { console.error('Cannot rerun: no action ID available') return } try { const startActionArgs = { - "bindingId": logEntry.value.actionId, + "bindingId": bindingId, "arguments": [] } @@ -281,13 +282,13 @@ async function renderExecutionResult(res) { } executionTrackingId.value = res.logEntry.executionTrackingId - canRerun.value = res.logEntry.executionFinished + canRerun.value = res.logEntry.executionFinished && !!res.logEntry.bindingId canKill.value = res.logEntry.canKill icon.value = res.logEntry.actionIcon title.value = res.logEntry.actionTitle - titleTooltip.value = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId - actionId.value = res.logEntry.actionId + titleTooltip.value = 'Action ID: ' + res.logEntry.bindingId + '\nExecution ID: ' + res.logEntry.executionTrackingId + actionId.value = res.logEntry.bindingId updateDuration(res.logEntry) From 5ff6b5d08083ba8f362964d9a6793a58e5327bce Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 8 Mar 2026 22:16:24 +0000 Subject: [PATCH 053/148] fix: Entity ordering (#886, #762) --- AGENTS.md | 6 +- .../entityFilesWithLongIntsUseStandardForm.js | 10 +-- service/internal/api/apiActions.go | 4 +- service/internal/api/api_test.go | 26 ++++++ service/internal/api/dashboard_entities.go | 7 +- service/internal/api/dashboards.go | 80 +++++++++++++++++-- service/internal/entities/entities_test.go | 45 ++++++++++- service/internal/entities/storage.go | 45 +++++++++++ service/internal/executor/executor_actions.go | 2 +- specs/dashboard-component-ordering.md | 52 ++++++++++++ 10 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 specs/dashboard-component-ordering.md diff --git a/AGENTS.md b/AGENTS.md index db2412f..40dd89a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - **Frontend (Vue 3)**: `frontend/` (served by the service) - **Integration tests**: `integration-tests/` - **Protos/Generated**: `proto/`, `service/gen/...` +- **Specs**: `specs/` — Markdown specs that define how code should behave in human-readable form. When changing behavior in a spec-covered area, keep implementation and tests aligned with the spec; do not reference code or symbols in specs (English only). ### How to Run - Run the server (dev): @@ -62,11 +63,10 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. ### Contributing Checklist - Review the contributing guidelines at `CONTRIBUTING.adoc`. - Review the AI guidance in `AI.md`. -- Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.md`. +- Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.md`. +- When changing behaviour covered by a spec in `specs/`, ensure implementation and tests match the spec. ### Troubleshooting - API tests failing with content-type errors: ensure Connect handler is served under `/api/` and the client targets that base URL. - Executor panics: check for nil `Binding/Action` and add guards in step functions. - Integration timeouts: wait for `loaded-dashboard` and use selectors matching the Vue UI. - - diff --git a/integration-tests/tests/entityFilesWithLongIntsUseStandardForm/entityFilesWithLongIntsUseStandardForm.js b/integration-tests/tests/entityFilesWithLongIntsUseStandardForm/entityFilesWithLongIntsUseStandardForm.js index eb92a07..cc3a320 100644 --- a/integration-tests/tests/entityFilesWithLongIntsUseStandardForm/entityFilesWithLongIntsUseStandardForm.js +++ b/integration-tests/tests/entityFilesWithLongIntsUseStandardForm/entityFilesWithLongIntsUseStandardForm.js @@ -2,8 +2,8 @@ import { describe, it, before, after } from 'mocha' import { expect } from 'chai' import { By, until, Condition } from 'selenium-webdriver' -import { - getRootAndWait, +import { + getRootAndWait, getActionButtons, takeScreenshotOnFailure, } from '../../lib/elements.js' @@ -29,8 +29,8 @@ describe('config: entities', function () { expect(buttons).to.not.be.null expect(buttons).to.have.length(5) - // Test INT with 10 numbers - const buttonInt10 = await buttons[2] + // Entity buttons are in numeric key order (0,1,2,3,4); first row is "INT with 10 numbers" + const buttonInt10 = await buttons[0] expect(await buttonInt10.getAttribute('title')).to.be.equal('Test me INT with 10 numbers') await buttonInt10.click() @@ -49,7 +49,7 @@ describe('config: entities', function () { // Check that the execution completed successfully by looking at the status const statusElement = await webdriver.findElement(By.id('execution-dialog-status')) const statusText = await statusElement.getText() - + // The status should indicate success (not "Executing..." or "Failed") expect(statusText).to.not.include('Executing') expect(statusText).to.not.include('Failed') diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 4a23125..45fcad1 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -185,9 +185,7 @@ func buildChoices(arg config.ActionArgument) []*apiv1.ActionArgumentChoice { func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle string) []*apiv1.ActionArgumentChoice { ret := []*apiv1.ActionArgumentChoice{} - entList := entities.GetEntityInstances(entityTitle) - - for _, ent := range entList { + for _, ent := range entities.GetEntityInstancesOrdered(entityTitle) { ret = append(ret, &apiv1.ActionArgumentChoice{ Value: tpl.ParseTemplateOfActionBeforeExec(firstChoice.Value, ent), Title: tpl.ParseTemplateOfActionBeforeExec(firstChoice.Title, ent), diff --git a/service/internal/api/api_test.go b/service/internal/api/api_test.go index 6e24d33..7bca057 100644 --- a/service/internal/api/api_test.go +++ b/service/internal/api/api_test.go @@ -450,3 +450,29 @@ func bindingIdsFromComponent(c *apiv1.DashboardComponent) []string { } return append(ids, bindingIdsInDashboardContents(c.Contents)...) } + +func TestOrderTopLevelDashboardComponents_RegularFieldsetsPreserveConfigOrder(t *testing.T) { + zebra := &apiv1.DashboardComponent{Title: "Zebra", Type: "fieldset", EntityType: ""} + alpha := &apiv1.DashboardComponent{Title: "Alpha", Type: "fieldset", EntityType: ""} + root := &apiv1.DashboardComponent{Title: "Actions", Type: "fieldset", EntityType: ""} + components := []*apiv1.DashboardComponent{zebra, alpha, root} + + out := orderTopLevelDashboardComponents(components) + + require.Len(t, out, 3) + assert.Same(t, zebra, out[0], "first must be Zebra (config order)") + assert.Same(t, alpha, out[1], "second must be Alpha (config order)") + assert.Same(t, root, out[2], "third must be root Actions fieldset") +} + +func TestOrderTopLevelDashboardComponents_SortablesSorted(t *testing.T) { + entityBeta := &apiv1.DashboardComponent{Title: "Beta", Type: "fieldset", EntityType: "server"} + entityAlpha := &apiv1.DashboardComponent{Title: "Alpha", Type: "fieldset", EntityType: "server"} + components := []*apiv1.DashboardComponent{entityBeta, entityAlpha} + + out := orderTopLevelDashboardComponents(components) + + require.Len(t, out, 2) + assert.Equal(t, "Alpha", out[0].Title, "sortables ordered by title") + assert.Equal(t, "Beta", out[1].Title) +} diff --git a/service/internal/api/dashboard_entities.go b/service/internal/api/dashboard_entities.go index 2ddf964..d9a12d6 100644 --- a/service/internal/api/dashboard_entities.go +++ b/service/internal/api/dashboard_entities.go @@ -11,9 +11,8 @@ import ( func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr *DashboardRenderRequest) []*apiv1.DashboardComponent { ret := make([]*apiv1.DashboardComponent, 0) - entities := entities.GetEntityInstances(entityTitle) - - for _, ent := range entities { + orderedEntities := entities.GetEntityInstancesOrdered(entityTitle) + for _, ent := range orderedEntities { fs := buildEntityFieldset(tpl, ent, rr) if len(fs.Contents) > 0 { @@ -30,7 +29,7 @@ func buildEntityFieldset(component *config.DashboardComponent, ent *entities.Ent Type: "fieldset", Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(component.Contents, ent, component.Entity, rr)), CssClass: tpl.ParseTemplateOfActionBeforeExec(component.CssClass, ent), - Action: rr.findAction(component.Title), + Action: rr.findActionForEntity(component.Title, ent), EntityType: component.Entity, EntityKey: ent.UniqueKey, } diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 575994c..4e98fde 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -2,6 +2,7 @@ package api import ( "sort" + "strconv" apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" acl "github.com/OliveTin/OliveTin/internal/acl" @@ -113,7 +114,7 @@ func buildDashboardFromConfig(dashboard *config.DashboardComponent, rr *Dashboar func buildDashboardFromConfigWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.Dashboard { return &apiv1.Dashboard{ Title: dashboard.Title, - Contents: sortActions(removeNulls(getDashboardComponentContentsWithEntity(dashboard, rr, entity))), + Contents: orderTopLevelDashboardComponents(removeNulls(getDashboardComponentContentsWithEntity(dashboard, rr, entity))), } } @@ -148,33 +149,49 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { continue } - fieldset.Contents = append(fieldset.Contents, &apiv1.DashboardComponent{ + comp := &apiv1.DashboardComponent{ Type: "link", Title: action.Title, Icon: action.Icon, Action: action, - }) + } + if binding.Entity != nil { + comp.EntityKey = binding.Entity.UniqueKey + } + fieldset.Contents = append(fieldset.Contents, comp) } if len(fieldset.Contents) > 0 { - fieldset.Contents = sortActions(fieldset.Contents) + fieldset.Contents = sortDashboardComponents(fieldset.Contents) db.Contents = append(db.Contents, fieldset) } return db } -func sortActions(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent { +func entityKeyLess(a, b string) bool { + ai, errA := strconv.ParseInt(a, 10, 64) + bi, errB := strconv.ParseInt(b, 10, 64) + if errA == nil && errB == nil { + return ai < bi + } + return a < b +} + +//gocyclo:ignore +func sortDashboardComponents(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent { sort.Slice(components, func(i, j int) bool { if components[i].Action == nil || components[j].Action == nil { return components[i].Title < components[j].Title } - if components[i].Action.Order == components[j].Action.Order { - return components[i].Action.Title < components[j].Action.Title - } else { + if components[i].Action.Order != components[j].Action.Order { return components[i].Action.Order < components[j].Action.Order } + if components[i].EntityKey != components[j].EntityKey { + return entityKeyLess(components[i].EntityKey, components[j].EntityKey) + } + return components[i].Action.Title < components[j].Action.Title }) return components @@ -194,6 +211,53 @@ func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardCompo return ret } +func isRegularFieldset(component *apiv1.DashboardComponent, index int, totalLen int) bool { + if component == nil || component.Type != "fieldset" || component.EntityType != "" { + return false + } + return index != totalLen-1 +} + +func partitionTopLevelComponents(components []*apiv1.DashboardComponent) (regular, sortables []*apiv1.DashboardComponent, isRegular []bool) { + regular = make([]*apiv1.DashboardComponent, 0) + sortables = make([]*apiv1.DashboardComponent, 0) + isRegular = make([]bool, len(components)) + for i, c := range components { + anchor := isRegularFieldset(c, i, len(components)) + isRegular[i] = anchor + if anchor { + regular = append(regular, c) + } else { + sortables = append(sortables, c) + } + } + return regular, sortables, isRegular +} + +func mergeOrderedTopLevelComponents(regular, sortables []*apiv1.DashboardComponent, isRegular []bool) []*apiv1.DashboardComponent { + out := make([]*apiv1.DashboardComponent, 0, len(isRegular)) + regIdx, sortIdx := 0, 0 + for _, anchor := range isRegular { + if anchor { + out = append(out, regular[regIdx]) + regIdx++ + } else { + out = append(out, sortables[sortIdx]) + sortIdx++ + } + } + return out +} + +func orderTopLevelDashboardComponents(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent { + if len(components) == 0 { + return components + } + regular, sortables, isRegular := partitionTopLevelComponents(components) + sortDashboardComponents(sortables) + return mergeOrderedTopLevelComponents(regular, sortables, isRegular) +} + func getDashboardComponentContentsWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) []*apiv1.DashboardComponent { ret := make([]*apiv1.DashboardComponent, 0) rootFieldset := createRootFieldset() diff --git a/service/internal/entities/entities_test.go b/service/internal/entities/entities_test.go index ab135d3..b13fd08 100644 --- a/service/internal/entities/entities_test.go +++ b/service/internal/entities/entities_test.go @@ -1,8 +1,10 @@ package entities import ( - // "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLoadObjectPerLineJsonFile(t *testing.T) { @@ -16,3 +18,44 @@ func TestLoadObjectPerLineJsonFile(t *testing.T) { assert.Equal(t, "1234567890", GetEntity("testrow", "0"), "Value should match expected value") */ } + +func TestGetEntityInstancesOrdered_numericKeys(t *testing.T) { + ClearEntitiesOfType("order_test") + defer ClearEntitiesOfType("order_test") + + AddEntity("order_test", "2", map[string]any{"title": "Second"}) + AddEntity("order_test", "0", map[string]any{"title": "Zeroth"}) + AddEntity("order_test", "10", map[string]any{"title": "Tenth"}) + AddEntity("order_test", "1", map[string]any{"title": "First"}) + + ordered := GetEntityInstancesOrdered("order_test") + require.Len(t, ordered, 4, "should return 4 entities") + assert.Equal(t, "0", ordered[0].UniqueKey, "first key should be 0") + assert.Equal(t, "1", ordered[1].UniqueKey, "second key should be 1") + assert.Equal(t, "2", ordered[2].UniqueKey, "third key should be 2") + assert.Equal(t, "10", ordered[3].UniqueKey, "fourth key should be 10 (numeric order)") +} + +func TestGetEntityInstancesOrdered_lexicographicKeys(t *testing.T) { + ClearEntitiesOfType("order_test_lex") + defer ClearEntitiesOfType("order_test_lex") + + AddEntity("order_test_lex", "zebra", map[string]any{"title": "Z"}) + AddEntity("order_test_lex", "alpha", map[string]any{"title": "A"}) + AddEntity("order_test_lex", "beta", map[string]any{"title": "B"}) + + ordered := GetEntityInstancesOrdered("order_test_lex") + require.Len(t, ordered, 3, "should return 3 entities") + assert.Equal(t, "alpha", ordered[0].UniqueKey) + assert.Equal(t, "beta", ordered[1].UniqueKey) + assert.Equal(t, "zebra", ordered[2].UniqueKey) +} + +func TestGetEntityInstancesOrdered_emptyOrMissing(t *testing.T) { + ordered := GetEntityInstancesOrdered("nonexistent_type") + assert.Nil(t, ordered) + + ClearEntitiesOfType("empty_test") + ordered = GetEntityInstancesOrdered("empty_test") + assert.Nil(t, ordered) +} diff --git a/service/internal/entities/storage.go b/service/internal/entities/storage.go index e1fac78..bd61718 100644 --- a/service/internal/entities/storage.go +++ b/service/internal/entities/storage.go @@ -10,6 +10,8 @@ package entities */ import ( + "sort" + "strconv" "strings" "sync" ) @@ -64,6 +66,49 @@ func GetEntityInstances(entityName string) entityInstancesByKey { return make(entityInstancesByKey, 0) } +func GetEntityInstancesOrdered(entityName string) []*Entity { + instances := GetEntityInstances(entityName) + if len(instances) == 0 { + return nil + } + + keys := make([]string, 0, len(instances)) + for key := range instances { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return compareEntityKeys(keys[i], keys[j]) < 0 + }) + + result := make([]*Entity, 0, len(keys)) + for _, key := range keys { + result = append(result, instances[key]) + } + return result +} + +//gocyclo:ignore +func compareEntityKeys(a, b string) int { + ai, errA := strconv.ParseInt(a, 10, 64) + bi, errB := strconv.ParseInt(b, 10, 64) + if errA == nil && errB == nil { + if ai < bi { + return -1 + } + if ai > bi { + return 1 + } + return 0 + } + if a < b { + return -1 + } + if a > b { + return 1 + } + return 0 +} + func AddEntity(entityName string, entityKey string, data any) { rwmutex.Lock() diff --git a/service/internal/executor/executor_actions.go b/service/internal/executor/executor_actions.go index c55f114..706d16c 100644 --- a/service/internal/executor/executor_actions.go +++ b/service/internal/executor/executor_actions.go @@ -146,7 +146,7 @@ func registerAction(e *Executor, configOrder int, action *config.Action, req *Re } func registerActionsFromEntities(e *Executor, configOrder int, entityTitle string, tpl *config.Action, req *RebuildActionMapRequest) { - for _, ent := range entities.GetEntityInstances(entityTitle) { + for _, ent := range entities.GetEntityInstancesOrdered(entityTitle) { registerActionFromEntity(e, configOrder, tpl, ent, req) } } diff --git a/specs/dashboard-component-ordering.md b/specs/dashboard-component-ordering.md new file mode 100644 index 0000000..6723fb5 --- /dev/null +++ b/specs/dashboard-component-ordering.md @@ -0,0 +1,52 @@ +# Spec: Dashboard component ordering + +This spec describes how dashboard components (fieldsets, entity fieldsets, actions, and other elements) are ordered in OliveTin. It documents the current behaviour so that it can be reasoned about and kept consistent. + +--- + +## 1. Implementation + +### 1.1 Two ways dashboards are built + +Dashboards are built in two ways: + +- **Default dashboard:** Used when there is no dashboard configuration. A single fieldset titled "Actions" is created and filled with actions that are not already on a configured dashboard. +- **Config dashboard:** Built from the dashboard configuration (e.g. under dashboards or dashboards.d). The structure is derived by walking the config tree, which produces a mix of fieldsets and a special root fieldset titled "Actions" that holds any loose items. + +Ordering rules differ slightly between these two cases. + +### 1.2 Top-level dashboard contents (config dashboards) + +**Fieldsets without entities:** Fieldsets that are not tied to an entity type appear at the top level in **config order**. Their position in the dashboard matches the order in which they are defined in the config. + +**Other top-level components:** All other top-level components (including the root "Actions" fieldset and entity fieldset groups) are **sorted** before being shown. Sort order: + +1. If a component has no linked action, it is ordered by its title (alphabetically). +2. Otherwise, components are ordered first by the action's order value (lower values first). +3. If order values are equal, components are ordered by entity key: if both keys are whole numbers they are compared numerically; otherwise they are compared alphabetically. +4. If still equal, components are ordered by the action's title (alphabetically). + +The root "Actions" fieldset is the single fieldset created by the build to hold loose items; it is identified by reference (not by position). When present it is added last to the list, then the sort is applied among that fieldset and entity-related components. So that fieldset can appear anywhere among those according to the rules above. When there are no loose items the root is not present, and the last component in the list is not treated as the root—so a fieldset without entities in the last position keeps config order. Regular fieldsets stay in config order and are not reordered. + +### 1.3 Entity fieldsets (order of fieldsets per entity type) + +When a fieldset in the config is tied to an entity type (e.g. "Server" or "Project"), one fieldset is built per entity instance. Those fieldsets are shown in **entity key order**. + +**Entity key order:** + +- If both keys are whole numbers: **numeric** order (e.g. 2 before 10). +- Otherwise: **alphabetical** (lexicographic) order. + +So the order of entity fieldsets (e.g. one per server, one per project) is determined by this entity key order, not by config or insertion order. + +### 1.4 Contents inside fieldsets + +**Default dashboard:** The single "Actions" fieldset's contents are sorted. The same rules as for top-level components apply: order value first, then entity key (numeric then alphabetical), then action title. + +**Config dashboards:** For all fieldsets (the root "Actions" fieldset, entity fieldsets, and regular fieldsets), the contents are **not** sorted. They keep the order from the config: + +- **Root "Actions" fieldset:** Items appear in the order they are listed in the config (loose items that are not inside a fieldset). +- **Entity fieldset contents:** The order comes from the template's contents in the config. +- **Regular (non-entity) fieldset contents:** The order comes from the config, including for nested structure. + +So within any config-defined fieldset, the order of actions and other child components is the **config order**. From 71bb999950ad01014d1482e166c273c83b54b8af Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 8 Mar 2026 22:45:46 +0000 Subject: [PATCH 054/148] security: Actions that people didnt have permission to view were being returned (#921) --- service/internal/api/api_test.go | 67 ++++++++++++++++++++++ service/internal/api/dashboard_entities.go | 8 ++- service/internal/api/dashboards.go | 19 +++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/service/internal/api/api_test.go b/service/internal/api/api_test.go index 7bca057..57abddd 100644 --- a/service/internal/api/api_test.go +++ b/service/internal/api/api_test.go @@ -432,6 +432,73 @@ func TestViewPermissionAllowedSeesAction(t *testing.T) { assert.Equal(t, "secret_action", resp.Action.BindingId) } +// TestViewPermissionExcludedFromCustomDashboard (issue #921) asserts that when a custom dashboard +// lists an action by title, users without view permission do not see that action (title or icon). +func TestViewPermissionExcludedFromCustomDashboard(t *testing.T) { + cfg, lowUser, _ := buildViewPermissionTestConfig(t) + cfg.Dashboards = []*config.DashboardComponent{ + { + Title: "Custom", + Contents: []*config.DashboardComponent{ + {Title: "Secret Action"}, + }, + }, + } + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + + rr := &DashboardRenderRequest{ + AuthenticatedUser: lowUser, + cfg: cfg, + ex: ex, + } + dashboard := findDashboardByTitle(rr, "Custom") + require.NotNil(t, dashboard) + db := buildDashboardFromConfig(dashboard, rr) + require.NotNil(t, db) + + bindingIdsInDashboard := bindingIdsInDashboardContents(db.Contents) + assert.NotContains(t, bindingIdsInDashboard, "secret_action", + "user with view:false must not see action on custom dashboard; got bindingIds: %v", bindingIdsInDashboard) +} + +// TestViewPermissionExcludedFromEntityDashboard (GHSA: view permission) asserts that when a dashboard +// has an entity fieldset listing an action, users without view permission do not see that action. +func TestViewPermissionExcludedFromEntityDashboard(t *testing.T) { + entities.ClearEntitiesOfType("vp_entity_test") + defer entities.ClearEntitiesOfType("vp_entity_test") + entities.AddEntity("vp_entity_test", "1", map[string]any{"title": "Test Entity"}) + + cfg, lowUser, _ := buildViewPermissionTestConfig(t) + cfg.Dashboards = []*config.DashboardComponent{ + { + Title: "WithEntity", + Contents: []*config.DashboardComponent{ + { + Title: "Servers", Type: "fieldset", Entity: "vp_entity_test", + Contents: []*config.DashboardComponent{{Title: "Secret Action"}}, + }, + }, + }, + } + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + + rr := &DashboardRenderRequest{ + AuthenticatedUser: lowUser, + cfg: cfg, + ex: ex, + } + dashboard := findDashboardByTitle(rr, "WithEntity") + require.NotNil(t, dashboard) + db := buildDashboardFromConfig(dashboard, rr) + require.NotNil(t, db) + + bindingIdsInDashboard := bindingIdsInDashboardContents(db.Contents) + assert.NotContains(t, bindingIdsInDashboard, "secret_action", + "user with view:false must not see action in entity fieldset; got bindingIds: %v", bindingIdsInDashboard) +} + func bindingIdsInDashboardContents(contents []*apiv1.DashboardComponent) []string { var ids []string for _, c := range contents { diff --git a/service/internal/api/dashboard_entities.go b/service/internal/api/dashboard_entities.go index d9a12d6..d588a57 100644 --- a/service/internal/api/dashboard_entities.go +++ b/service/internal/api/dashboard_entities.go @@ -82,8 +82,6 @@ func isLinkType(itemType string) bool { } func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { - clone.Type = "link" - clone.Title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, ent) // Prefer an entity-specific action when available, but fall back to a // non-entity-scoped action with the same title. This allows inline actions // defined inside entity dashboards to work without requiring an explicit @@ -92,7 +90,11 @@ func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clo if action == nil { action = rr.findAction(subitem.Title) } - + if action == nil { + return nil + } + clone.Type = "link" + clone.Title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, ent) clone.Action = action return clone } diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 4e98fde..44e2244 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -277,16 +277,31 @@ func createRootFieldset() *apiv1.DashboardComponent { } } +func appendComponentIfNotNil(components *[]*apiv1.DashboardComponent, comp *apiv1.DashboardComponent) { + if comp != nil { + *components = append(*components, comp) + } +} + +func getDashboardComponentOrNil(subitem *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.DashboardComponent { + if len(subitem.Contents) == 0 && rr.findActionForEntity(subitem.Title, entity) == nil { + if !isAllowedType(subitem.Type) { + return nil + } + } + return buildDashboardComponentSimpleWithEntity(subitem, rr, entity) +} + func processDashboardSubitemWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, ret *[]*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent, entity *entities.Entity) { if subitem.Type != "fieldset" { - rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimpleWithEntity(subitem, rr, entity)) + appendComponentIfNotNil(&rootFieldset.Contents, getDashboardComponentOrNil(subitem, rr, entity)) return } if subitem.Entity != "" { *ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...) } else { - *ret = append(*ret, buildDashboardComponentSimpleWithEntity(subitem, rr, entity)) + appendComponentIfNotNil(ret, getDashboardComponentOrNil(subitem, rr, entity)) } } From 24e8b48dc9406afec953c8c0c303795dea69c850 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 8 Mar 2026 23:09:23 +0000 Subject: [PATCH 055/148] fix: Websocket reconnection logic (#802, 889, #884) --- frontend/js/websocket.js | 14 +++++++++++++- frontend/resources/vue/App.vue | 17 ++++++++++++++--- .../resources/vue/stores/connectionState.js | 6 ++++++ lang/combined_output.json | 12 +++++++++++- lang/de-DE.yaml | 4 +++- lang/en.yaml | 4 +++- lang/es-ES.yaml | 4 +++- lang/it-IT.yaml | 4 +++- lang/zh-Hans-CN.yaml | 4 +++- 9 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 frontend/resources/vue/stores/connectionState.js diff --git a/frontend/js/websocket.js b/frontend/js/websocket.js index 8c0200f..dbabe67 100644 --- a/frontend/js/websocket.js +++ b/frontend/js/websocket.js @@ -1,5 +1,8 @@ import { buttonResults } from '../resources/vue/stores/buttonResults.js' import { rateLimits } from '../resources/vue/stores/rateLimits.js' +import { connectionState } from '../resources/vue/stores/connectionState.js' + +const RECONNECT_DELAY_MS = 3000 export function initWebsocket () { window.addEventListener('EventOutputChunk', onOutputChunk) @@ -16,9 +19,14 @@ async function reconnectWebsocket () { return } + connectionState.reconnecting = true + connectionState.connected = false + try { window.websocketAvailable = true for await (const e of window.client.eventStream()) { + connectionState.connected = true + connectionState.reconnecting = false handleEvent(e) } } catch (err) { @@ -26,7 +34,11 @@ async function reconnectWebsocket () { } window.websocketAvailable = false - console.log('Reconnecting websocket...') + connectionState.connected = false + console.log('Reconnecting websocket in ' + RECONNECT_DELAY_MS + 'ms...') + setTimeout(() => { + reconnectWebsocket() + }, RECONNECT_DELAY_MS) } function handleEvent (msg) { diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 7dfae4a..82830d7 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -50,7 +50,7 @@ {{ currentThemeName }} - {{ t('connected') }} + {{ connectionStatusLabel }}

@@ -107,6 +107,7 @@ import { DashboardSquare01Icon } from '@hugeicons/core-free-icons' import logoUrl from '../../OliveTinLogo.png'; import { useI18n } from 'vue-i18n'; import combinedTranslations from '../../../lang/combined_output.json'; +import { connectionState } from './stores/connectionState.js'; const { t, locale } = useI18n(); @@ -116,7 +117,6 @@ const sidebar = ref(null); const navigation = ref(null); const username = ref('notset'); const isLoggedIn = ref(false); -const serverConnection = ref(true); const currentVersion = ref('?'); const pageTitle = ref('OliveTin'); const bannerMessage = ref(''); @@ -130,6 +130,18 @@ const showVersionNumber = ref(true) const showLoginLink = ref(true) const sectionNavigationStyle = ref('sidebar') +const connectionStatusLabel = computed(() => { + if (connectionState.connected) { + return t('connected') + } + if (connectionState.reconnecting) { + return t('reconnecting') + } + return t('disconnected') +}) + +const connectionStatusTitle = computed(() => connectionStatusLabel.value) + const languageDialog = ref(null) const browserLanguages = ref([]) @@ -404,7 +416,6 @@ function handleThemeDialogClick(event) { window.updateHeaderFromInit = updateHeaderFromInit onMounted(() => { - serverConnection.value = true; updateHeaderFromInit() // Initialize selected language from stored preference diff --git a/frontend/resources/vue/stores/connectionState.js b/frontend/resources/vue/stores/connectionState.js new file mode 100644 index 0000000..c8a4606 --- /dev/null +++ b/frontend/resources/vue/stores/connectionState.js @@ -0,0 +1,6 @@ +import { reactive } from 'vue' + +export const connectionState = reactive({ + connected: false, + reconnecting: false +}) diff --git a/lang/combined_output.json b/lang/combined_output.json index 3fda01a..9e4514b 100644 --- a/lang/combined_output.json +++ b/lang/combined_output.json @@ -20,6 +20,7 @@ "diagnostics.unknown": "Unbekannt", "diagnostics.useragent-data-error": "Fehler beim Abrufen von userAgentData", "diagnostics.where-to-find-help": "Wo Sie Hilfe finden", + "disconnected": "Getrennt", "docs": "Dokumentation", "language-dialog.browser-languages": "Browser-Sprachen", "language-dialog.close": "Schließen", @@ -47,6 +48,7 @@ "nav.entities": "Entitäten", "nav.logs": "Protokolle", "raise-issue": "Ein Problem melden auf GitHub", + "reconnecting": "Verbinde erneut…", "return-to-index": "Zurück zur Startseite", "search-filter": "Filter aktuelle Seite", "theme-dialog.close": "Schließen", @@ -73,6 +75,7 @@ "diagnostics.unknown": "Unknown", "diagnostics.useragent-data-error": "Error retrieving userAgentData", "diagnostics.where-to-find-help": "Where to find help", + "disconnected": "Disconnected", "docs": "Documentation", "language-dialog.browser-languages": "Browser languages", "language-dialog.close": "Close", @@ -100,6 +103,7 @@ "nav.entities": "Entities", "nav.logs": "Logs", "raise-issue": "Raise an issue on GitHub", + "reconnecting": "Reconnecting…", "return-to-index": "Return to index", "search-filter": "Filter current page", "theme-dialog.close": "Close", @@ -126,6 +130,7 @@ "diagnostics.unknown": "Desconocido", "diagnostics.useragent-data-error": "Error al recuperar userAgentData", "diagnostics.where-to-find-help": "Dónde encontrar ayuda", + "disconnected": "Desconectado", "docs": "Documentación", "language-dialog.browser-languages": "Idiomas del navegador", "language-dialog.close": "Cerrar", @@ -153,6 +158,7 @@ "nav.entities": "Entidades", "nav.logs": "Registros", "raise-issue": "Reportar un problema en GitHub", + "reconnecting": "Reconectando…", "return-to-index": "Volver a la página principal", "search-filter": "Filtrar página actual", "theme-dialog.close": "Cerrar", @@ -179,6 +185,7 @@ "diagnostics.unknown": "Sconosciuto", "diagnostics.useragent-data-error": "Errore nel recupero di userAgentData", "diagnostics.where-to-find-help": "Dove trovare aiuto", + "disconnected": "Disconnesso", "docs": "Documentazione", "language-dialog.browser-languages": "Lingue del browser", "language-dialog.close": "Chiudi", @@ -206,6 +213,7 @@ "nav.entities": "Entità", "nav.logs": "Registri", "raise-issue": "Segnala un problema su GitHub", + "reconnecting": "Riconnessione…", "return-to-index": "Torna alla pagina principale", "search-filter": "Filtra la pagina corrente", "theme-dialog.close": "Chiudi", @@ -232,6 +240,7 @@ "diagnostics.unknown": "未知", "diagnostics.useragent-data-error": "检索 userAgentData 时出错", "diagnostics.where-to-find-help": "在哪里找到帮助", + "disconnected": "已断开连接", "docs": "文档", "language-dialog.browser-languages": "浏览器语言", "language-dialog.close": "关闭", @@ -259,6 +268,7 @@ "nav.entities": "实体", "nav.logs": "日志", "raise-issue": "在 GitHub 上报告问题", + "reconnecting": "正在重新连接…", "return-to-index": "返回首页", "search-filter": "过滤当前页面", "theme-dialog.close": "关闭", @@ -267,4 +277,4 @@ "welcome": "欢迎使用 OliveTin" } } -} \ No newline at end of file +} diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml index 2e5f6c7..e56b45a 100644 --- a/lang/de-DE.yaml +++ b/lang/de-DE.yaml @@ -6,6 +6,8 @@ translations: nav.entities: Entitäten nav.diagnostics: Diagnostik connected: Verbunden + disconnected: Getrennt + reconnecting: Verbinde erneut… login-button: Login raise-issue: Ein Problem melden auf GitHub docs: Dokumentation @@ -50,4 +52,4 @@ translations: language-dialog.close: Schließen theme-dialog.title: Design auswählen theme-dialog.default: Standard-Design - theme-dialog.close: Schließen \ No newline at end of file + theme-dialog.close: Schließen diff --git a/lang/en.yaml b/lang/en.yaml index 8cf5c25..4205514 100644 --- a/lang/en.yaml +++ b/lang/en.yaml @@ -8,6 +8,8 @@ translations: nav.entities: Entities nav.diagnostics: Diagnostics connected: Connected + disconnected: Disconnected + reconnecting: Reconnecting… login-button: Login logs.title: Logs logs.page-description: This is a list of logs from actions that have been executed. You can filter the list by action title. @@ -50,4 +52,4 @@ translations: language-dialog.close: Close theme-dialog.title: Select Theme theme-dialog.default: Default Theme - theme-dialog.close: Close \ No newline at end of file + theme-dialog.close: Close diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml index 974eaf6..21a4bff 100644 --- a/lang/es-ES.yaml +++ b/lang/es-ES.yaml @@ -6,6 +6,8 @@ translations: nav.entities: Entidades nav.diagnostics: Diagnósticos connected: Conectado + disconnected: Desconectado + reconnecting: Reconectando… login-button: Iniciar sesión raise-issue: Reportar un problema en GitHub docs: Documentación @@ -50,4 +52,4 @@ translations: language-dialog.close: Cerrar theme-dialog.title: Seleccionar tema theme-dialog.default: Tema Predeterminado - theme-dialog.close: Cerrar \ No newline at end of file + theme-dialog.close: Cerrar diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml index 4bc9c66..b56f498 100644 --- a/lang/it-IT.yaml +++ b/lang/it-IT.yaml @@ -7,6 +7,8 @@ translations: nav.diagnostics: Diagnostica docs: Documentazione connected: Connesso + disconnected: Disconnesso + reconnecting: Riconnessione… login-button: Login raise-issue: Segnala un problema su GitHub logs.title: Registri @@ -50,4 +52,4 @@ translations: language-dialog.close: Chiudi theme-dialog.title: Seleziona tema theme-dialog.default: Tema Predefinito - theme-dialog.close: Chiudi \ No newline at end of file + theme-dialog.close: Chiudi diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml index ad84897..73f7810 100644 --- a/lang/zh-Hans-CN.yaml +++ b/lang/zh-Hans-CN.yaml @@ -6,6 +6,8 @@ translations: nav.entities: 实体 nav.diagnostics: 诊断 connected: 已连接 + disconnected: 已断开连接 + reconnecting: 正在重新连接… login-button: 登录 raise-issue: 在 GitHub 上报告问题 docs: 文档 @@ -50,4 +52,4 @@ translations: diagnostics.copy-to-clipboard: 复制到剪贴板 diagnostics.copied: 已复制! diagnostics.unknown: 未知 - diagnostics.useragent-data-error: 检索 userAgentData 时出错 \ No newline at end of file + diagnostics.useragent-data-error: 检索 userAgentData 时出错 From 2f77000de44f65690f257e3cf8e2c8462b0e74c7 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 8 Mar 2026 23:29:00 +0000 Subject: [PATCH 056/148] security: GHSA-364q-w7vh-vhpc (HIGH) Unsafe parsing of UniqueTrackingId can be used to write files --- service/internal/executor/executor.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index f6807cf..670aeea 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -20,6 +20,7 @@ import ( "os" "os/exec" "path" + "regexp" "strings" "sync" "time" @@ -30,6 +31,14 @@ const ( MaxTriggerDepth = 10 ) +var validTrackingIDPattern = regexp.MustCompile(`^[a-fA-F0-9\-]+$`) + +func isValidTrackingID(id string) bool { + const MaxTrackingIDLength = 36 + + return id != "" && len(id) <= MaxTrackingIDLength && validTrackingIDPattern.MatchString(id) +} + var ( metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{ Name: "olivetin_actions_requested_count", @@ -506,8 +515,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) } _, isDuplicate := e.GetLog(req.TrackingID) - - if isDuplicate || req.TrackingID == "" { + if isDuplicate || !isValidTrackingID(req.TrackingID) { req.TrackingID = uuid.NewString() } From a5c102dbf16f72b4d97abec9790393862d6a51f2 Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 9 Mar 2026 00:27:53 +0000 Subject: [PATCH 057/148] fix: Much more helpful reconnection banner --- frontend/js/websocket.js | 15 ++-- frontend/resources/vue/App.vue | 18 +---- .../vue/components/ConnectionBanner.vue | 68 +++++++++++++++++++ .../resources/vue/stores/connectionState.js | 4 +- lang/combined_output.json | 10 +++ lang/de-DE.yaml | 2 + lang/en.yaml | 2 + lang/es-ES.yaml | 2 + lang/it-IT.yaml | 2 + lang/zh-Hans-CN.yaml | 2 + 10 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 frontend/resources/vue/components/ConnectionBanner.vue diff --git a/frontend/js/websocket.js b/frontend/js/websocket.js index dbabe67..d453a70 100644 --- a/frontend/js/websocket.js +++ b/frontend/js/websocket.js @@ -2,7 +2,7 @@ import { buttonResults } from '../resources/vue/stores/buttonResults.js' import { rateLimits } from '../resources/vue/stores/rateLimits.js' import { connectionState } from '../resources/vue/stores/connectionState.js' -const RECONNECT_DELAY_MS = 3000 +const RECONNECT_DELAY_MS = 10000 export function initWebsocket () { window.addEventListener('EventOutputChunk', onOutputChunk) @@ -21,12 +21,17 @@ async function reconnectWebsocket () { connectionState.reconnecting = true connectionState.connected = false + connectionState.disconnectedAt = Date.now() + connectionState.nextReconnectAt = null try { window.websocketAvailable = true - for await (const e of window.client.eventStream()) { - connectionState.connected = true - connectionState.reconnecting = false + const stream = window.client.eventStream() + connectionState.connected = true + connectionState.reconnecting = false + connectionState.disconnectedAt = null + connectionState.nextReconnectAt = null + for await (const e of stream) { handleEvent(e) } } catch (err) { @@ -35,6 +40,8 @@ async function reconnectWebsocket () { window.websocketAvailable = false connectionState.connected = false + connectionState.disconnectedAt = connectionState.disconnectedAt ?? Date.now() + connectionState.nextReconnectAt = Date.now() + RECONNECT_DELAY_MS console.log('Reconnecting websocket in ' + RECONNECT_DELAY_MS + 'ms...') setTimeout(() => { reconnectWebsocket() diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 82830d7..dccdaa0 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -7,6 +7,7 @@ diff --git a/frontend/resources/vue/views/ActionExecConditionsView.vue b/frontend/resources/vue/views/ActionExecConditionsView.vue new file mode 100644 index 0000000..534d1c4 --- /dev/null +++ b/frontend/resources/vue/views/ActionExecConditionsView.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index 6c12ebe..0ba61ce 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -392,6 +392,11 @@ async function startAction(actionArgs) { async function handleSubmit(event) { event.preventDefault() + if (popupOnStart.value === 'history') { + router.push(`/action/${props.bindingId}`) + return + } + // Set custom validity for required fields for (const arg of actionArguments.value) { const value = argValues.value[arg.name] @@ -422,8 +427,6 @@ async function handleSubmit(event) { const response = await startAction(argvs) if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) { router.push(`/logs/${response.executionTrackingId}`) - } else if (popupOnStart.value === 'history') { - router.push(`/action/${props.bindingId}`) } else { router.back() } diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index baf71e2..50c7936 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -14,6 +14,17 @@ message Action { int32 order = 7; int32 timeout = 8; string datetime_rate_limit_expires = 9; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05" + bool exec_on_startup = 10; + repeated string exec_on_cron = 11; + repeated string exec_on_file_created_in_dir = 12; + repeated string exec_on_file_changed_in_dir = 13; + string exec_on_calendar_file = 14; + repeated ActionWebhookExecHint exec_on_webhooks = 15; +} + +message ActionWebhookExecHint { + string template = 1; + string match_path = 2; } message ActionArgument { diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index 182827b..448e351 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -22,16 +22,22 @@ const ( ) type Action struct { - state protoimpl.MessageState `protogen:"open.v1"` - BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"` - CanExec bool `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"` - Arguments []*ActionArgument `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"` - PopupOnStart string `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"` - Order int32 `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"` - Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"` - DatetimeRateLimitExpires string `protobuf:"bytes,9,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05" + state protoimpl.MessageState `protogen:"open.v1"` + BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"` + CanExec bool `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"` + Arguments []*ActionArgument `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"` + PopupOnStart string `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"` + Order int32 `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"` + Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"` + DatetimeRateLimitExpires string `protobuf:"bytes,9,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05" + ExecOnStartup bool `protobuf:"varint,10,opt,name=exec_on_startup,json=execOnStartup,proto3" json:"exec_on_startup,omitempty"` + ExecOnCron []string `protobuf:"bytes,11,rep,name=exec_on_cron,json=execOnCron,proto3" json:"exec_on_cron,omitempty"` + ExecOnFileCreatedInDir []string `protobuf:"bytes,12,rep,name=exec_on_file_created_in_dir,json=execOnFileCreatedInDir,proto3" json:"exec_on_file_created_in_dir,omitempty"` + ExecOnFileChangedInDir []string `protobuf:"bytes,13,rep,name=exec_on_file_changed_in_dir,json=execOnFileChangedInDir,proto3" json:"exec_on_file_changed_in_dir,omitempty"` + ExecOnCalendarFile string `protobuf:"bytes,14,opt,name=exec_on_calendar_file,json=execOnCalendarFile,proto3" json:"exec_on_calendar_file,omitempty"` + ExecOnWebhooks []*ActionWebhookExecHint `protobuf:"bytes,15,rep,name=exec_on_webhooks,json=execOnWebhooks,proto3" json:"exec_on_webhooks,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -129,6 +135,100 @@ func (x *Action) GetDatetimeRateLimitExpires() string { return "" } +func (x *Action) GetExecOnStartup() bool { + if x != nil { + return x.ExecOnStartup + } + return false +} + +func (x *Action) GetExecOnCron() []string { + if x != nil { + return x.ExecOnCron + } + return nil +} + +func (x *Action) GetExecOnFileCreatedInDir() []string { + if x != nil { + return x.ExecOnFileCreatedInDir + } + return nil +} + +func (x *Action) GetExecOnFileChangedInDir() []string { + if x != nil { + return x.ExecOnFileChangedInDir + } + return nil +} + +func (x *Action) GetExecOnCalendarFile() string { + if x != nil { + return x.ExecOnCalendarFile + } + return "" +} + +func (x *Action) GetExecOnWebhooks() []*ActionWebhookExecHint { + if x != nil { + return x.ExecOnWebhooks + } + return nil +} + +type ActionWebhookExecHint struct { + state protoimpl.MessageState `protogen:"open.v1"` + Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + MatchPath string `protobuf:"bytes,2,opt,name=match_path,json=matchPath,proto3" json:"match_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActionWebhookExecHint) Reset() { + *x = ActionWebhookExecHint{} + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActionWebhookExecHint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActionWebhookExecHint) ProtoMessage() {} + +func (x *ActionWebhookExecHint) ProtoReflect() protoreflect.Message { + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActionWebhookExecHint.ProtoReflect.Descriptor instead. +func (*ActionWebhookExecHint) Descriptor() ([]byte, []int) { + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{1} +} + +func (x *ActionWebhookExecHint) GetTemplate() string { + if x != nil { + return x.Template + } + return "" +} + +func (x *ActionWebhookExecHint) GetMatchPath() string { + if x != nil { + return x.MatchPath + } + return "" +} + type ActionArgument struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -145,7 +245,7 @@ type ActionArgument struct { func (x *ActionArgument) Reset() { *x = ActionArgument{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[1] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -157,7 +257,7 @@ func (x *ActionArgument) String() string { func (*ActionArgument) ProtoMessage() {} func (x *ActionArgument) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[1] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -170,7 +270,7 @@ func (x *ActionArgument) ProtoReflect() protoreflect.Message { // Deprecated: Use ActionArgument.ProtoReflect.Descriptor instead. func (*ActionArgument) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{1} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{2} } func (x *ActionArgument) GetName() string { @@ -239,7 +339,7 @@ type ActionArgumentChoice struct { func (x *ActionArgumentChoice) Reset() { *x = ActionArgumentChoice{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[2] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -251,7 +351,7 @@ func (x *ActionArgumentChoice) String() string { func (*ActionArgumentChoice) ProtoMessage() {} func (x *ActionArgumentChoice) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[2] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -264,7 +364,7 @@ func (x *ActionArgumentChoice) ProtoReflect() protoreflect.Message { // Deprecated: Use ActionArgumentChoice.ProtoReflect.Descriptor instead. func (*ActionArgumentChoice) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{2} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{3} } func (x *ActionArgumentChoice) GetValue() string { @@ -294,7 +394,7 @@ type Entity struct { func (x *Entity) Reset() { *x = Entity{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[3] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -306,7 +406,7 @@ func (x *Entity) String() string { func (*Entity) ProtoMessage() {} func (x *Entity) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[3] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -319,7 +419,7 @@ func (x *Entity) ProtoReflect() protoreflect.Message { // Deprecated: Use Entity.ProtoReflect.Descriptor instead. func (*Entity) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{3} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{4} } func (x *Entity) GetTitle() string { @@ -367,7 +467,7 @@ type GetDashboardResponse struct { func (x *GetDashboardResponse) Reset() { *x = GetDashboardResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[4] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -379,7 +479,7 @@ func (x *GetDashboardResponse) String() string { func (*GetDashboardResponse) ProtoMessage() {} func (x *GetDashboardResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[4] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -392,7 +492,7 @@ func (x *GetDashboardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDashboardResponse.ProtoReflect.Descriptor instead. func (*GetDashboardResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{4} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{5} } func (x *GetDashboardResponse) GetTitle() string { @@ -420,7 +520,7 @@ type EffectivePolicy struct { func (x *EffectivePolicy) Reset() { *x = EffectivePolicy{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[5] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -432,7 +532,7 @@ func (x *EffectivePolicy) String() string { func (*EffectivePolicy) ProtoMessage() {} func (x *EffectivePolicy) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[5] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -445,7 +545,7 @@ func (x *EffectivePolicy) ProtoReflect() protoreflect.Message { // Deprecated: Use EffectivePolicy.ProtoReflect.Descriptor instead. func (*EffectivePolicy) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{5} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{6} } func (x *EffectivePolicy) GetShowDiagnostics() bool { @@ -480,7 +580,7 @@ type GetDashboardRequest struct { func (x *GetDashboardRequest) Reset() { *x = GetDashboardRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[6] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -492,7 +592,7 @@ func (x *GetDashboardRequest) String() string { func (*GetDashboardRequest) ProtoMessage() {} func (x *GetDashboardRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[6] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -505,7 +605,7 @@ func (x *GetDashboardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDashboardRequest.ProtoReflect.Descriptor instead. func (*GetDashboardRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{6} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{7} } func (x *GetDashboardRequest) GetTitle() string { @@ -539,7 +639,7 @@ type Dashboard struct { func (x *Dashboard) Reset() { *x = Dashboard{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[7] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -551,7 +651,7 @@ func (x *Dashboard) String() string { func (*Dashboard) ProtoMessage() {} func (x *Dashboard) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[7] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -564,7 +664,7 @@ func (x *Dashboard) ProtoReflect() protoreflect.Message { // Deprecated: Use Dashboard.ProtoReflect.Descriptor instead. func (*Dashboard) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{7} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{8} } func (x *Dashboard) GetTitle() string { @@ -597,7 +697,7 @@ type DashboardComponent struct { func (x *DashboardComponent) Reset() { *x = DashboardComponent{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[8] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -609,7 +709,7 @@ func (x *DashboardComponent) String() string { func (*DashboardComponent) ProtoMessage() {} func (x *DashboardComponent) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[8] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -622,7 +722,7 @@ func (x *DashboardComponent) ProtoReflect() protoreflect.Message { // Deprecated: Use DashboardComponent.ProtoReflect.Descriptor instead. func (*DashboardComponent) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{8} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{9} } func (x *DashboardComponent) GetTitle() string { @@ -692,7 +792,7 @@ type StartActionRequest struct { func (x *StartActionRequest) Reset() { *x = StartActionRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[9] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -704,7 +804,7 @@ func (x *StartActionRequest) String() string { func (*StartActionRequest) ProtoMessage() {} func (x *StartActionRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[9] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -717,7 +817,7 @@ func (x *StartActionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionRequest.ProtoReflect.Descriptor instead. func (*StartActionRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{9} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{10} } func (x *StartActionRequest) GetBindingId() string { @@ -751,7 +851,7 @@ type StartActionArgument struct { func (x *StartActionArgument) Reset() { *x = StartActionArgument{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[10] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -763,7 +863,7 @@ func (x *StartActionArgument) String() string { func (*StartActionArgument) ProtoMessage() {} func (x *StartActionArgument) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[10] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -776,7 +876,7 @@ func (x *StartActionArgument) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionArgument.ProtoReflect.Descriptor instead. func (*StartActionArgument) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{10} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{11} } func (x *StartActionArgument) GetName() string { @@ -802,7 +902,7 @@ type StartActionResponse struct { func (x *StartActionResponse) Reset() { *x = StartActionResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[11] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -814,7 +914,7 @@ func (x *StartActionResponse) String() string { func (*StartActionResponse) ProtoMessage() {} func (x *StartActionResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[11] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -827,7 +927,7 @@ func (x *StartActionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionResponse.ProtoReflect.Descriptor instead. func (*StartActionResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{11} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{12} } func (x *StartActionResponse) GetExecutionTrackingId() string { @@ -847,7 +947,7 @@ type StartActionAndWaitRequest struct { func (x *StartActionAndWaitRequest) Reset() { *x = StartActionAndWaitRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[12] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -859,7 +959,7 @@ func (x *StartActionAndWaitRequest) String() string { func (*StartActionAndWaitRequest) ProtoMessage() {} func (x *StartActionAndWaitRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[12] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -872,7 +972,7 @@ func (x *StartActionAndWaitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionAndWaitRequest.ProtoReflect.Descriptor instead. func (*StartActionAndWaitRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{12} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{13} } func (x *StartActionAndWaitRequest) GetActionId() string { @@ -898,7 +998,7 @@ type StartActionAndWaitResponse struct { func (x *StartActionAndWaitResponse) Reset() { *x = StartActionAndWaitResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[13] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -910,7 +1010,7 @@ func (x *StartActionAndWaitResponse) String() string { func (*StartActionAndWaitResponse) ProtoMessage() {} func (x *StartActionAndWaitResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[13] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -923,7 +1023,7 @@ func (x *StartActionAndWaitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionAndWaitResponse.ProtoReflect.Descriptor instead. func (*StartActionAndWaitResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{13} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{14} } func (x *StartActionAndWaitResponse) GetLogEntry() *LogEntry { @@ -942,7 +1042,7 @@ type StartActionByGetRequest struct { func (x *StartActionByGetRequest) Reset() { *x = StartActionByGetRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[14] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -954,7 +1054,7 @@ func (x *StartActionByGetRequest) String() string { func (*StartActionByGetRequest) ProtoMessage() {} func (x *StartActionByGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[14] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -967,7 +1067,7 @@ func (x *StartActionByGetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionByGetRequest.ProtoReflect.Descriptor instead. func (*StartActionByGetRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{14} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{15} } func (x *StartActionByGetRequest) GetActionId() string { @@ -986,7 +1086,7 @@ type StartActionByGetResponse struct { func (x *StartActionByGetResponse) Reset() { *x = StartActionByGetResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[15] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -998,7 +1098,7 @@ func (x *StartActionByGetResponse) String() string { func (*StartActionByGetResponse) ProtoMessage() {} func (x *StartActionByGetResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[15] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1011,7 +1111,7 @@ func (x *StartActionByGetResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionByGetResponse.ProtoReflect.Descriptor instead. func (*StartActionByGetResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{15} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{16} } func (x *StartActionByGetResponse) GetExecutionTrackingId() string { @@ -1030,7 +1130,7 @@ type StartActionByGetAndWaitRequest struct { func (x *StartActionByGetAndWaitRequest) Reset() { *x = StartActionByGetAndWaitRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[16] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1042,7 +1142,7 @@ func (x *StartActionByGetAndWaitRequest) String() string { func (*StartActionByGetAndWaitRequest) ProtoMessage() {} func (x *StartActionByGetAndWaitRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[16] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1055,7 +1155,7 @@ func (x *StartActionByGetAndWaitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionByGetAndWaitRequest.ProtoReflect.Descriptor instead. func (*StartActionByGetAndWaitRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{16} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{17} } func (x *StartActionByGetAndWaitRequest) GetActionId() string { @@ -1074,7 +1174,7 @@ type StartActionByGetAndWaitResponse struct { func (x *StartActionByGetAndWaitResponse) Reset() { *x = StartActionByGetAndWaitResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[17] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1086,7 +1186,7 @@ func (x *StartActionByGetAndWaitResponse) String() string { func (*StartActionByGetAndWaitResponse) ProtoMessage() {} func (x *StartActionByGetAndWaitResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[17] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1099,7 +1199,7 @@ func (x *StartActionByGetAndWaitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartActionByGetAndWaitResponse.ProtoReflect.Descriptor instead. func (*StartActionByGetAndWaitResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{17} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{18} } func (x *StartActionByGetAndWaitResponse) GetLogEntry() *LogEntry { @@ -1120,7 +1220,7 @@ type GetLogsRequest struct { func (x *GetLogsRequest) Reset() { *x = GetLogsRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[18] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1132,7 +1232,7 @@ func (x *GetLogsRequest) String() string { func (*GetLogsRequest) ProtoMessage() {} func (x *GetLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[18] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1145,7 +1245,7 @@ func (x *GetLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogsRequest.ProtoReflect.Descriptor instead. func (*GetLogsRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{18} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{19} } func (x *GetLogsRequest) GetStartOffset() int64 { @@ -1195,7 +1295,7 @@ type LogEntry struct { func (x *LogEntry) Reset() { *x = LogEntry{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[19] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1207,7 +1307,7 @@ func (x *LogEntry) String() string { func (*LogEntry) ProtoMessage() {} func (x *LogEntry) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[19] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1220,7 +1320,7 @@ func (x *LogEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use LogEntry.ProtoReflect.Descriptor instead. func (*LogEntry) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{19} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{20} } func (x *LogEntry) GetDatetimeStarted() string { @@ -1362,7 +1462,7 @@ type GetLogsResponse struct { func (x *GetLogsResponse) Reset() { *x = GetLogsResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[20] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1374,7 +1474,7 @@ func (x *GetLogsResponse) String() string { func (*GetLogsResponse) ProtoMessage() {} func (x *GetLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[20] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1387,7 +1487,7 @@ func (x *GetLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogsResponse.ProtoReflect.Descriptor instead. func (*GetLogsResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{20} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{21} } func (x *GetLogsResponse) GetLogs() []*LogEntry { @@ -1435,7 +1535,7 @@ type GetActionLogsRequest struct { func (x *GetActionLogsRequest) Reset() { *x = GetActionLogsRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[21] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1447,7 +1547,7 @@ func (x *GetActionLogsRequest) String() string { func (*GetActionLogsRequest) ProtoMessage() {} func (x *GetActionLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[21] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1460,7 +1560,7 @@ func (x *GetActionLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionLogsRequest.ProtoReflect.Descriptor instead. func (*GetActionLogsRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{21} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{22} } func (x *GetActionLogsRequest) GetActionId() string { @@ -1490,7 +1590,7 @@ type GetActionLogsResponse struct { func (x *GetActionLogsResponse) Reset() { *x = GetActionLogsResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[22] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1502,7 +1602,7 @@ func (x *GetActionLogsResponse) String() string { func (*GetActionLogsResponse) ProtoMessage() {} func (x *GetActionLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[22] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1515,7 +1615,7 @@ func (x *GetActionLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionLogsResponse.ProtoReflect.Descriptor instead. func (*GetActionLogsResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{22} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{23} } func (x *GetActionLogsResponse) GetLogs() []*LogEntry { @@ -1565,7 +1665,7 @@ type ValidateArgumentTypeRequest struct { func (x *ValidateArgumentTypeRequest) Reset() { *x = ValidateArgumentTypeRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[23] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1577,7 +1677,7 @@ func (x *ValidateArgumentTypeRequest) String() string { func (*ValidateArgumentTypeRequest) ProtoMessage() {} func (x *ValidateArgumentTypeRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[23] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1590,7 +1690,7 @@ func (x *ValidateArgumentTypeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateArgumentTypeRequest.ProtoReflect.Descriptor instead. func (*ValidateArgumentTypeRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{23} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{24} } func (x *ValidateArgumentTypeRequest) GetValue() string { @@ -1631,7 +1731,7 @@ type ValidateArgumentTypeResponse struct { func (x *ValidateArgumentTypeResponse) Reset() { *x = ValidateArgumentTypeResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[24] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1643,7 +1743,7 @@ func (x *ValidateArgumentTypeResponse) String() string { func (*ValidateArgumentTypeResponse) ProtoMessage() {} func (x *ValidateArgumentTypeResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[24] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1656,7 +1756,7 @@ func (x *ValidateArgumentTypeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateArgumentTypeResponse.ProtoReflect.Descriptor instead. func (*ValidateArgumentTypeResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{24} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{25} } func (x *ValidateArgumentTypeResponse) GetValid() bool { @@ -1682,7 +1782,7 @@ type WatchExecutionRequest struct { func (x *WatchExecutionRequest) Reset() { *x = WatchExecutionRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[25] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1694,7 +1794,7 @@ func (x *WatchExecutionRequest) String() string { func (*WatchExecutionRequest) ProtoMessage() {} func (x *WatchExecutionRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[25] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1707,7 +1807,7 @@ func (x *WatchExecutionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchExecutionRequest.ProtoReflect.Descriptor instead. func (*WatchExecutionRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{25} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{26} } func (x *WatchExecutionRequest) GetExecutionTrackingId() string { @@ -1726,7 +1826,7 @@ type WatchExecutionUpdate struct { func (x *WatchExecutionUpdate) Reset() { *x = WatchExecutionUpdate{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[26] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1738,7 +1838,7 @@ func (x *WatchExecutionUpdate) String() string { func (*WatchExecutionUpdate) ProtoMessage() {} func (x *WatchExecutionUpdate) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[26] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1751,7 +1851,7 @@ func (x *WatchExecutionUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchExecutionUpdate.ProtoReflect.Descriptor instead. func (*WatchExecutionUpdate) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{26} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{27} } func (x *WatchExecutionUpdate) GetUpdate() string { @@ -1771,7 +1871,7 @@ type ExecutionStatusRequest struct { func (x *ExecutionStatusRequest) Reset() { *x = ExecutionStatusRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[27] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1783,7 +1883,7 @@ func (x *ExecutionStatusRequest) String() string { func (*ExecutionStatusRequest) ProtoMessage() {} func (x *ExecutionStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[27] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1796,7 +1896,7 @@ func (x *ExecutionStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutionStatusRequest.ProtoReflect.Descriptor instead. func (*ExecutionStatusRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{27} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{28} } func (x *ExecutionStatusRequest) GetExecutionTrackingId() string { @@ -1822,7 +1922,7 @@ type ExecutionStatusResponse struct { func (x *ExecutionStatusResponse) Reset() { *x = ExecutionStatusResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[28] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1834,7 +1934,7 @@ func (x *ExecutionStatusResponse) String() string { func (*ExecutionStatusResponse) ProtoMessage() {} func (x *ExecutionStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[28] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1847,7 +1947,7 @@ func (x *ExecutionStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutionStatusResponse.ProtoReflect.Descriptor instead. func (*ExecutionStatusResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{28} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{29} } func (x *ExecutionStatusResponse) GetLogEntry() *LogEntry { @@ -1865,7 +1965,7 @@ type WhoAmIRequest struct { func (x *WhoAmIRequest) Reset() { *x = WhoAmIRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[29] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1877,7 +1977,7 @@ func (x *WhoAmIRequest) String() string { func (*WhoAmIRequest) ProtoMessage() {} func (x *WhoAmIRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[29] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1890,7 +1990,7 @@ func (x *WhoAmIRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WhoAmIRequest.ProtoReflect.Descriptor instead. func (*WhoAmIRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{29} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{30} } type WhoAmIResponse struct { @@ -1906,7 +2006,7 @@ type WhoAmIResponse struct { func (x *WhoAmIResponse) Reset() { *x = WhoAmIResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[30] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1918,7 +2018,7 @@ func (x *WhoAmIResponse) String() string { func (*WhoAmIResponse) ProtoMessage() {} func (x *WhoAmIResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[30] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1931,7 +2031,7 @@ func (x *WhoAmIResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WhoAmIResponse.ProtoReflect.Descriptor instead. func (*WhoAmIResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{30} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{31} } func (x *WhoAmIResponse) GetAuthenticatedUser() string { @@ -1977,7 +2077,7 @@ type SosReportRequest struct { func (x *SosReportRequest) Reset() { *x = SosReportRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[31] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1989,7 +2089,7 @@ func (x *SosReportRequest) String() string { func (*SosReportRequest) ProtoMessage() {} func (x *SosReportRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[31] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2002,7 +2102,7 @@ func (x *SosReportRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SosReportRequest.ProtoReflect.Descriptor instead. func (*SosReportRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{31} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{32} } type SosReportResponse struct { @@ -2014,7 +2114,7 @@ type SosReportResponse struct { func (x *SosReportResponse) Reset() { *x = SosReportResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[32] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2026,7 +2126,7 @@ func (x *SosReportResponse) String() string { func (*SosReportResponse) ProtoMessage() {} func (x *SosReportResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[32] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2039,7 +2139,7 @@ func (x *SosReportResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SosReportResponse.ProtoReflect.Descriptor instead. func (*SosReportResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{32} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{33} } func (x *SosReportResponse) GetAlert() string { @@ -2057,7 +2157,7 @@ type DumpVarsRequest struct { func (x *DumpVarsRequest) Reset() { *x = DumpVarsRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[33] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2069,7 +2169,7 @@ func (x *DumpVarsRequest) String() string { func (*DumpVarsRequest) ProtoMessage() {} func (x *DumpVarsRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[33] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2082,7 +2182,7 @@ func (x *DumpVarsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DumpVarsRequest.ProtoReflect.Descriptor instead. func (*DumpVarsRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{33} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{34} } type DumpVarsResponse struct { @@ -2095,7 +2195,7 @@ type DumpVarsResponse struct { func (x *DumpVarsResponse) Reset() { *x = DumpVarsResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[34] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2107,7 +2207,7 @@ func (x *DumpVarsResponse) String() string { func (*DumpVarsResponse) ProtoMessage() {} func (x *DumpVarsResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[34] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2120,7 +2220,7 @@ func (x *DumpVarsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DumpVarsResponse.ProtoReflect.Descriptor instead. func (*DumpVarsResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{34} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{35} } func (x *DumpVarsResponse) GetAlert() string { @@ -2147,7 +2247,7 @@ type DebugBinding struct { func (x *DebugBinding) Reset() { *x = DebugBinding{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2159,7 +2259,7 @@ func (x *DebugBinding) String() string { func (*DebugBinding) ProtoMessage() {} func (x *DebugBinding) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2172,7 +2272,7 @@ func (x *DebugBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugBinding.ProtoReflect.Descriptor instead. func (*DebugBinding) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{35} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{36} } func (x *DebugBinding) GetActionTitle() string { @@ -2197,7 +2297,7 @@ type DumpPublicIdActionMapRequest struct { func (x *DumpPublicIdActionMapRequest) Reset() { *x = DumpPublicIdActionMapRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[36] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2209,7 +2309,7 @@ func (x *DumpPublicIdActionMapRequest) String() string { func (*DumpPublicIdActionMapRequest) ProtoMessage() {} func (x *DumpPublicIdActionMapRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[36] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2222,7 +2322,7 @@ func (x *DumpPublicIdActionMapRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DumpPublicIdActionMapRequest.ProtoReflect.Descriptor instead. func (*DumpPublicIdActionMapRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{36} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{37} } type DumpPublicIdActionMapResponse struct { @@ -2235,7 +2335,7 @@ type DumpPublicIdActionMapResponse struct { func (x *DumpPublicIdActionMapResponse) Reset() { *x = DumpPublicIdActionMapResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[37] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2247,7 +2347,7 @@ func (x *DumpPublicIdActionMapResponse) String() string { func (*DumpPublicIdActionMapResponse) ProtoMessage() {} func (x *DumpPublicIdActionMapResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[37] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2260,7 +2360,7 @@ func (x *DumpPublicIdActionMapResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DumpPublicIdActionMapResponse.ProtoReflect.Descriptor instead. func (*DumpPublicIdActionMapResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{37} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{38} } func (x *DumpPublicIdActionMapResponse) GetAlert() string { @@ -2285,7 +2385,7 @@ type GetReadyzRequest struct { func (x *GetReadyzRequest) Reset() { *x = GetReadyzRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[38] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2297,7 +2397,7 @@ func (x *GetReadyzRequest) String() string { func (*GetReadyzRequest) ProtoMessage() {} func (x *GetReadyzRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[38] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2310,7 +2410,7 @@ func (x *GetReadyzRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetReadyzRequest.ProtoReflect.Descriptor instead. func (*GetReadyzRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{38} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{39} } type GetReadyzResponse struct { @@ -2322,7 +2422,7 @@ type GetReadyzResponse struct { func (x *GetReadyzResponse) Reset() { *x = GetReadyzResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[39] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2334,7 +2434,7 @@ func (x *GetReadyzResponse) String() string { func (*GetReadyzResponse) ProtoMessage() {} func (x *GetReadyzResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[39] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2347,7 +2447,7 @@ func (x *GetReadyzResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetReadyzResponse.ProtoReflect.Descriptor instead. func (*GetReadyzResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{39} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{40} } func (x *GetReadyzResponse) GetStatus() string { @@ -2365,7 +2465,7 @@ type EventStreamRequest struct { func (x *EventStreamRequest) Reset() { *x = EventStreamRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[40] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2377,7 +2477,7 @@ func (x *EventStreamRequest) String() string { func (*EventStreamRequest) ProtoMessage() {} func (x *EventStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[40] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2390,7 +2490,7 @@ func (x *EventStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EventStreamRequest.ProtoReflect.Descriptor instead. func (*EventStreamRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{40} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{41} } type EventStreamResponse struct { @@ -2409,7 +2509,7 @@ type EventStreamResponse struct { func (x *EventStreamResponse) Reset() { *x = EventStreamResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[41] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2421,7 +2521,7 @@ func (x *EventStreamResponse) String() string { func (*EventStreamResponse) ProtoMessage() {} func (x *EventStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[41] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2434,7 +2534,7 @@ func (x *EventStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EventStreamResponse.ProtoReflect.Descriptor instead. func (*EventStreamResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{41} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{42} } func (x *EventStreamResponse) GetEvent() isEventStreamResponse_Event { @@ -2533,7 +2633,7 @@ type EventOutputChunk struct { func (x *EventOutputChunk) Reset() { *x = EventOutputChunk{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[42] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2545,7 +2645,7 @@ func (x *EventOutputChunk) String() string { func (*EventOutputChunk) ProtoMessage() {} func (x *EventOutputChunk) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[42] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2558,7 +2658,7 @@ func (x *EventOutputChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use EventOutputChunk.ProtoReflect.Descriptor instead. func (*EventOutputChunk) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{42} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{43} } func (x *EventOutputChunk) GetExecutionTrackingId() string { @@ -2583,7 +2683,7 @@ type EventEntityChanged struct { func (x *EventEntityChanged) Reset() { *x = EventEntityChanged{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[43] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2595,7 +2695,7 @@ func (x *EventEntityChanged) String() string { func (*EventEntityChanged) ProtoMessage() {} func (x *EventEntityChanged) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[43] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2608,7 +2708,7 @@ func (x *EventEntityChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use EventEntityChanged.ProtoReflect.Descriptor instead. func (*EventEntityChanged) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{43} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{44} } type EventConfigChanged struct { @@ -2619,7 +2719,7 @@ type EventConfigChanged struct { func (x *EventConfigChanged) Reset() { *x = EventConfigChanged{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[44] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2631,7 +2731,7 @@ func (x *EventConfigChanged) String() string { func (*EventConfigChanged) ProtoMessage() {} func (x *EventConfigChanged) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[44] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2644,7 +2744,7 @@ func (x *EventConfigChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use EventConfigChanged.ProtoReflect.Descriptor instead. func (*EventConfigChanged) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{44} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{45} } type EventExecutionFinished struct { @@ -2656,7 +2756,7 @@ type EventExecutionFinished struct { func (x *EventExecutionFinished) Reset() { *x = EventExecutionFinished{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[45] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2668,7 +2768,7 @@ func (x *EventExecutionFinished) String() string { func (*EventExecutionFinished) ProtoMessage() {} func (x *EventExecutionFinished) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[45] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2681,7 +2781,7 @@ func (x *EventExecutionFinished) ProtoReflect() protoreflect.Message { // Deprecated: Use EventExecutionFinished.ProtoReflect.Descriptor instead. func (*EventExecutionFinished) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{45} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{46} } func (x *EventExecutionFinished) GetLogEntry() *LogEntry { @@ -2700,7 +2800,7 @@ type EventExecutionStarted struct { func (x *EventExecutionStarted) Reset() { *x = EventExecutionStarted{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[46] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2712,7 +2812,7 @@ func (x *EventExecutionStarted) String() string { func (*EventExecutionStarted) ProtoMessage() {} func (x *EventExecutionStarted) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[46] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2725,7 +2825,7 @@ func (x *EventExecutionStarted) ProtoReflect() protoreflect.Message { // Deprecated: Use EventExecutionStarted.ProtoReflect.Descriptor instead. func (*EventExecutionStarted) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{46} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{47} } func (x *EventExecutionStarted) GetLogEntry() *LogEntry { @@ -2744,7 +2844,7 @@ type KillActionRequest struct { func (x *KillActionRequest) Reset() { *x = KillActionRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[47] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2756,7 +2856,7 @@ func (x *KillActionRequest) String() string { func (*KillActionRequest) ProtoMessage() {} func (x *KillActionRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[47] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2769,7 +2869,7 @@ func (x *KillActionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use KillActionRequest.ProtoReflect.Descriptor instead. func (*KillActionRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{47} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{48} } func (x *KillActionRequest) GetExecutionTrackingId() string { @@ -2791,7 +2891,7 @@ type KillActionResponse struct { func (x *KillActionResponse) Reset() { *x = KillActionResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[48] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2803,7 +2903,7 @@ func (x *KillActionResponse) String() string { func (*KillActionResponse) ProtoMessage() {} func (x *KillActionResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[48] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2816,7 +2916,7 @@ func (x *KillActionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use KillActionResponse.ProtoReflect.Descriptor instead. func (*KillActionResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{48} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{49} } func (x *KillActionResponse) GetExecutionTrackingId() string { @@ -2857,7 +2957,7 @@ type LocalUserLoginRequest struct { func (x *LocalUserLoginRequest) Reset() { *x = LocalUserLoginRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[49] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2869,7 +2969,7 @@ func (x *LocalUserLoginRequest) String() string { func (*LocalUserLoginRequest) ProtoMessage() {} func (x *LocalUserLoginRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[49] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2882,7 +2982,7 @@ func (x *LocalUserLoginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LocalUserLoginRequest.ProtoReflect.Descriptor instead. func (*LocalUserLoginRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{49} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{50} } func (x *LocalUserLoginRequest) GetUsername() string { @@ -2908,7 +3008,7 @@ type LocalUserLoginResponse struct { func (x *LocalUserLoginResponse) Reset() { *x = LocalUserLoginResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[50] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2920,7 +3020,7 @@ func (x *LocalUserLoginResponse) String() string { func (*LocalUserLoginResponse) ProtoMessage() {} func (x *LocalUserLoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[50] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2933,7 +3033,7 @@ func (x *LocalUserLoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LocalUserLoginResponse.ProtoReflect.Descriptor instead. func (*LocalUserLoginResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{50} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{51} } func (x *LocalUserLoginResponse) GetSuccess() bool { @@ -2952,7 +3052,7 @@ type PasswordHashRequest struct { func (x *PasswordHashRequest) Reset() { *x = PasswordHashRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[51] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2964,7 +3064,7 @@ func (x *PasswordHashRequest) String() string { func (*PasswordHashRequest) ProtoMessage() {} func (x *PasswordHashRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[51] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2977,7 +3077,7 @@ func (x *PasswordHashRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PasswordHashRequest.ProtoReflect.Descriptor instead. func (*PasswordHashRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{51} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{52} } func (x *PasswordHashRequest) GetPassword() string { @@ -2996,7 +3096,7 @@ type PasswordHashResponse struct { func (x *PasswordHashResponse) Reset() { *x = PasswordHashResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[52] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3008,7 +3108,7 @@ func (x *PasswordHashResponse) String() string { func (*PasswordHashResponse) ProtoMessage() {} func (x *PasswordHashResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[52] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3021,7 +3121,7 @@ func (x *PasswordHashResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PasswordHashResponse.ProtoReflect.Descriptor instead. func (*PasswordHashResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{52} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{53} } func (x *PasswordHashResponse) GetHash() string { @@ -3039,7 +3139,7 @@ type LogoutRequest struct { func (x *LogoutRequest) Reset() { *x = LogoutRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[53] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3051,7 +3151,7 @@ func (x *LogoutRequest) String() string { func (*LogoutRequest) ProtoMessage() {} func (x *LogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[53] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3064,7 +3164,7 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. func (*LogoutRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{53} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{54} } type LogoutResponse struct { @@ -3075,7 +3175,7 @@ type LogoutResponse struct { func (x *LogoutResponse) Reset() { *x = LogoutResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[54] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3087,7 +3187,7 @@ func (x *LogoutResponse) String() string { func (*LogoutResponse) ProtoMessage() {} func (x *LogoutResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[54] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3100,7 +3200,7 @@ func (x *LogoutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. func (*LogoutResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{54} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{55} } type GetDiagnosticsRequest struct { @@ -3111,7 +3211,7 @@ type GetDiagnosticsRequest struct { func (x *GetDiagnosticsRequest) Reset() { *x = GetDiagnosticsRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[55] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3123,7 +3223,7 @@ func (x *GetDiagnosticsRequest) String() string { func (*GetDiagnosticsRequest) ProtoMessage() {} func (x *GetDiagnosticsRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[55] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3136,7 +3236,7 @@ func (x *GetDiagnosticsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiagnosticsRequest.ProtoReflect.Descriptor instead. func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{55} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{56} } type GetDiagnosticsResponse struct { @@ -3149,7 +3249,7 @@ type GetDiagnosticsResponse struct { func (x *GetDiagnosticsResponse) Reset() { *x = GetDiagnosticsResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[56] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3161,7 +3261,7 @@ func (x *GetDiagnosticsResponse) String() string { func (*GetDiagnosticsResponse) ProtoMessage() {} func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[56] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3174,7 +3274,7 @@ func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead. func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{56} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{57} } func (x *GetDiagnosticsResponse) GetSshFoundKey() string { @@ -3199,7 +3299,7 @@ type InitRequest struct { func (x *InitRequest) Reset() { *x = InitRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[57] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3211,7 +3311,7 @@ func (x *InitRequest) String() string { func (*InitRequest) ProtoMessage() {} func (x *InitRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[57] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3224,7 +3324,7 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. func (*InitRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{57} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{58} } type InitResponse struct { @@ -3260,7 +3360,7 @@ type InitResponse struct { func (x *InitResponse) Reset() { *x = InitResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[58] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3272,7 +3372,7 @@ func (x *InitResponse) String() string { func (*InitResponse) ProtoMessage() {} func (x *InitResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[58] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3285,7 +3385,7 @@ func (x *InitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InitResponse.ProtoReflect.Descriptor instead. func (*InitResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{58} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{59} } func (x *InitResponse) GetShowFooter() bool { @@ -3473,7 +3573,7 @@ type AdditionalLink struct { func (x *AdditionalLink) Reset() { *x = AdditionalLink{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[59] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3485,7 +3585,7 @@ func (x *AdditionalLink) String() string { func (*AdditionalLink) ProtoMessage() {} func (x *AdditionalLink) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[59] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3498,7 +3598,7 @@ func (x *AdditionalLink) ProtoReflect() protoreflect.Message { // Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead. func (*AdditionalLink) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{59} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{60} } func (x *AdditionalLink) GetTitle() string { @@ -3526,7 +3626,7 @@ type OAuth2Provider struct { func (x *OAuth2Provider) Reset() { *x = OAuth2Provider{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[60] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3538,7 +3638,7 @@ func (x *OAuth2Provider) String() string { func (*OAuth2Provider) ProtoMessage() {} func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[60] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3551,7 +3651,7 @@ func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead. func (*OAuth2Provider) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{60} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{61} } func (x *OAuth2Provider) GetTitle() string { @@ -3584,7 +3684,7 @@ type GetActionBindingRequest struct { func (x *GetActionBindingRequest) Reset() { *x = GetActionBindingRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[61] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3596,7 +3696,7 @@ func (x *GetActionBindingRequest) String() string { func (*GetActionBindingRequest) ProtoMessage() {} func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[61] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3609,7 +3709,7 @@ func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead. func (*GetActionBindingRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{61} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{62} } func (x *GetActionBindingRequest) GetBindingId() string { @@ -3628,7 +3728,7 @@ type GetActionBindingResponse struct { func (x *GetActionBindingResponse) Reset() { *x = GetActionBindingResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[62] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3640,7 +3740,7 @@ func (x *GetActionBindingResponse) String() string { func (*GetActionBindingResponse) ProtoMessage() {} func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[62] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3653,7 +3753,7 @@ func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead. func (*GetActionBindingResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{62} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{63} } func (x *GetActionBindingResponse) GetAction() *Action { @@ -3671,7 +3771,7 @@ type GetEntitiesRequest struct { func (x *GetEntitiesRequest) Reset() { *x = GetEntitiesRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[63] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3683,7 +3783,7 @@ func (x *GetEntitiesRequest) String() string { func (*GetEntitiesRequest) ProtoMessage() {} func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[63] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3696,7 +3796,7 @@ func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead. func (*GetEntitiesRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{63} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64} } type GetEntitiesResponse struct { @@ -3708,7 +3808,7 @@ type GetEntitiesResponse struct { func (x *GetEntitiesResponse) Reset() { *x = GetEntitiesResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[64] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3720,7 +3820,7 @@ func (x *GetEntitiesResponse) String() string { func (*GetEntitiesResponse) ProtoMessage() {} func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[64] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3733,7 +3833,7 @@ func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead. func (*GetEntitiesResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65} } func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition { @@ -3754,7 +3854,7 @@ type EntityDefinition struct { func (x *EntityDefinition) Reset() { *x = EntityDefinition{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3766,7 +3866,7 @@ func (x *EntityDefinition) String() string { func (*EntityDefinition) ProtoMessage() {} func (x *EntityDefinition) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3779,7 +3879,7 @@ func (x *EntityDefinition) ProtoReflect() protoreflect.Message { // Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead. func (*EntityDefinition) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66} } func (x *EntityDefinition) GetTitle() string { @@ -3813,7 +3913,7 @@ type GetEntityRequest struct { func (x *GetEntityRequest) Reset() { *x = GetEntityRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3825,7 +3925,7 @@ func (x *GetEntityRequest) String() string { func (*GetEntityRequest) ProtoMessage() {} func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3838,7 +3938,7 @@ func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead. func (*GetEntityRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67} } func (x *GetEntityRequest) GetUniqueKey() string { @@ -3864,7 +3964,7 @@ type RestartActionRequest struct { func (x *RestartActionRequest) Reset() { *x = RestartActionRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3876,7 +3976,7 @@ func (x *RestartActionRequest) String() string { func (*RestartActionRequest) ProtoMessage() {} func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3889,7 +3989,7 @@ func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead. func (*RestartActionRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68} } func (x *RestartActionRequest) GetExecutionTrackingId() string { @@ -3903,7 +4003,7 @@ var File_olivetin_api_v1_olivetin_proto protoreflect.FileDescriptor const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\n" + - "\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\xc0\x02\n" + + "\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\x89\x05\n" + "\x06Action\x12\x1d\n" + "\n" + "binding_id\x18\x01 \x01(\tR\tbindingId\x12\x14\n" + @@ -3914,7 +4014,19 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x0epopup_on_start\x18\x06 \x01(\tR\fpopupOnStart\x12\x14\n" + "\x05order\x18\a \x01(\x05R\x05order\x12\x18\n" + "\atimeout\x18\b \x01(\x05R\atimeout\x12=\n" + - "\x1bdatetime_rate_limit_expires\x18\t \x01(\tR\x18datetimeRateLimitExpires\"\xa2\x03\n" + + "\x1bdatetime_rate_limit_expires\x18\t \x01(\tR\x18datetimeRateLimitExpires\x12&\n" + + "\x0fexec_on_startup\x18\n" + + " \x01(\bR\rexecOnStartup\x12 \n" + + "\fexec_on_cron\x18\v \x03(\tR\n" + + "execOnCron\x12;\n" + + "\x1bexec_on_file_created_in_dir\x18\f \x03(\tR\x16execOnFileCreatedInDir\x12;\n" + + "\x1bexec_on_file_changed_in_dir\x18\r \x03(\tR\x16execOnFileChangedInDir\x121\n" + + "\x15exec_on_calendar_file\x18\x0e \x01(\tR\x12execOnCalendarFile\x12P\n" + + "\x10exec_on_webhooks\x18\x0f \x03(\v2&.olivetin.api.v1.ActionWebhookExecHintR\x0eexecOnWebhooks\"R\n" + + "\x15ActionWebhookExecHint\x12\x1a\n" + + "\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1d\n" + + "\n" + + "match_path\x18\x02 \x01(\tR\tmatchPath\"\xa2\x03\n" + "\x0eActionArgument\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" + @@ -4218,168 +4330,170 @@ func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte { return file_olivetin_api_v1_olivetin_proto_rawDescData } -var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 72) +var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 73) var file_olivetin_api_v1_olivetin_proto_goTypes = []any{ (*Action)(nil), // 0: olivetin.api.v1.Action - (*ActionArgument)(nil), // 1: olivetin.api.v1.ActionArgument - (*ActionArgumentChoice)(nil), // 2: olivetin.api.v1.ActionArgumentChoice - (*Entity)(nil), // 3: olivetin.api.v1.Entity - (*GetDashboardResponse)(nil), // 4: olivetin.api.v1.GetDashboardResponse - (*EffectivePolicy)(nil), // 5: olivetin.api.v1.EffectivePolicy - (*GetDashboardRequest)(nil), // 6: olivetin.api.v1.GetDashboardRequest - (*Dashboard)(nil), // 7: olivetin.api.v1.Dashboard - (*DashboardComponent)(nil), // 8: olivetin.api.v1.DashboardComponent - (*StartActionRequest)(nil), // 9: olivetin.api.v1.StartActionRequest - (*StartActionArgument)(nil), // 10: olivetin.api.v1.StartActionArgument - (*StartActionResponse)(nil), // 11: olivetin.api.v1.StartActionResponse - (*StartActionAndWaitRequest)(nil), // 12: olivetin.api.v1.StartActionAndWaitRequest - (*StartActionAndWaitResponse)(nil), // 13: olivetin.api.v1.StartActionAndWaitResponse - (*StartActionByGetRequest)(nil), // 14: olivetin.api.v1.StartActionByGetRequest - (*StartActionByGetResponse)(nil), // 15: olivetin.api.v1.StartActionByGetResponse - (*StartActionByGetAndWaitRequest)(nil), // 16: olivetin.api.v1.StartActionByGetAndWaitRequest - (*StartActionByGetAndWaitResponse)(nil), // 17: olivetin.api.v1.StartActionByGetAndWaitResponse - (*GetLogsRequest)(nil), // 18: olivetin.api.v1.GetLogsRequest - (*LogEntry)(nil), // 19: olivetin.api.v1.LogEntry - (*GetLogsResponse)(nil), // 20: olivetin.api.v1.GetLogsResponse - (*GetActionLogsRequest)(nil), // 21: olivetin.api.v1.GetActionLogsRequest - (*GetActionLogsResponse)(nil), // 22: olivetin.api.v1.GetActionLogsResponse - (*ValidateArgumentTypeRequest)(nil), // 23: olivetin.api.v1.ValidateArgumentTypeRequest - (*ValidateArgumentTypeResponse)(nil), // 24: olivetin.api.v1.ValidateArgumentTypeResponse - (*WatchExecutionRequest)(nil), // 25: olivetin.api.v1.WatchExecutionRequest - (*WatchExecutionUpdate)(nil), // 26: olivetin.api.v1.WatchExecutionUpdate - (*ExecutionStatusRequest)(nil), // 27: olivetin.api.v1.ExecutionStatusRequest - (*ExecutionStatusResponse)(nil), // 28: olivetin.api.v1.ExecutionStatusResponse - (*WhoAmIRequest)(nil), // 29: olivetin.api.v1.WhoAmIRequest - (*WhoAmIResponse)(nil), // 30: olivetin.api.v1.WhoAmIResponse - (*SosReportRequest)(nil), // 31: olivetin.api.v1.SosReportRequest - (*SosReportResponse)(nil), // 32: olivetin.api.v1.SosReportResponse - (*DumpVarsRequest)(nil), // 33: olivetin.api.v1.DumpVarsRequest - (*DumpVarsResponse)(nil), // 34: olivetin.api.v1.DumpVarsResponse - (*DebugBinding)(nil), // 35: olivetin.api.v1.DebugBinding - (*DumpPublicIdActionMapRequest)(nil), // 36: olivetin.api.v1.DumpPublicIdActionMapRequest - (*DumpPublicIdActionMapResponse)(nil), // 37: olivetin.api.v1.DumpPublicIdActionMapResponse - (*GetReadyzRequest)(nil), // 38: olivetin.api.v1.GetReadyzRequest - (*GetReadyzResponse)(nil), // 39: olivetin.api.v1.GetReadyzResponse - (*EventStreamRequest)(nil), // 40: olivetin.api.v1.EventStreamRequest - (*EventStreamResponse)(nil), // 41: olivetin.api.v1.EventStreamResponse - (*EventOutputChunk)(nil), // 42: olivetin.api.v1.EventOutputChunk - (*EventEntityChanged)(nil), // 43: olivetin.api.v1.EventEntityChanged - (*EventConfigChanged)(nil), // 44: olivetin.api.v1.EventConfigChanged - (*EventExecutionFinished)(nil), // 45: olivetin.api.v1.EventExecutionFinished - (*EventExecutionStarted)(nil), // 46: olivetin.api.v1.EventExecutionStarted - (*KillActionRequest)(nil), // 47: olivetin.api.v1.KillActionRequest - (*KillActionResponse)(nil), // 48: olivetin.api.v1.KillActionResponse - (*LocalUserLoginRequest)(nil), // 49: olivetin.api.v1.LocalUserLoginRequest - (*LocalUserLoginResponse)(nil), // 50: olivetin.api.v1.LocalUserLoginResponse - (*PasswordHashRequest)(nil), // 51: olivetin.api.v1.PasswordHashRequest - (*PasswordHashResponse)(nil), // 52: olivetin.api.v1.PasswordHashResponse - (*LogoutRequest)(nil), // 53: olivetin.api.v1.LogoutRequest - (*LogoutResponse)(nil), // 54: olivetin.api.v1.LogoutResponse - (*GetDiagnosticsRequest)(nil), // 55: olivetin.api.v1.GetDiagnosticsRequest - (*GetDiagnosticsResponse)(nil), // 56: olivetin.api.v1.GetDiagnosticsResponse - (*InitRequest)(nil), // 57: olivetin.api.v1.InitRequest - (*InitResponse)(nil), // 58: olivetin.api.v1.InitResponse - (*AdditionalLink)(nil), // 59: olivetin.api.v1.AdditionalLink - (*OAuth2Provider)(nil), // 60: olivetin.api.v1.OAuth2Provider - (*GetActionBindingRequest)(nil), // 61: olivetin.api.v1.GetActionBindingRequest - (*GetActionBindingResponse)(nil), // 62: olivetin.api.v1.GetActionBindingResponse - (*GetEntitiesRequest)(nil), // 63: olivetin.api.v1.GetEntitiesRequest - (*GetEntitiesResponse)(nil), // 64: olivetin.api.v1.GetEntitiesResponse - (*EntityDefinition)(nil), // 65: olivetin.api.v1.EntityDefinition - (*GetEntityRequest)(nil), // 66: olivetin.api.v1.GetEntityRequest - (*RestartActionRequest)(nil), // 67: olivetin.api.v1.RestartActionRequest - nil, // 68: olivetin.api.v1.ActionArgument.SuggestionsEntry - nil, // 69: olivetin.api.v1.Entity.FieldsEntry - nil, // 70: olivetin.api.v1.DumpVarsResponse.ContentsEntry - nil, // 71: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + (*ActionWebhookExecHint)(nil), // 1: olivetin.api.v1.ActionWebhookExecHint + (*ActionArgument)(nil), // 2: olivetin.api.v1.ActionArgument + (*ActionArgumentChoice)(nil), // 3: olivetin.api.v1.ActionArgumentChoice + (*Entity)(nil), // 4: olivetin.api.v1.Entity + (*GetDashboardResponse)(nil), // 5: olivetin.api.v1.GetDashboardResponse + (*EffectivePolicy)(nil), // 6: olivetin.api.v1.EffectivePolicy + (*GetDashboardRequest)(nil), // 7: olivetin.api.v1.GetDashboardRequest + (*Dashboard)(nil), // 8: olivetin.api.v1.Dashboard + (*DashboardComponent)(nil), // 9: olivetin.api.v1.DashboardComponent + (*StartActionRequest)(nil), // 10: olivetin.api.v1.StartActionRequest + (*StartActionArgument)(nil), // 11: olivetin.api.v1.StartActionArgument + (*StartActionResponse)(nil), // 12: olivetin.api.v1.StartActionResponse + (*StartActionAndWaitRequest)(nil), // 13: olivetin.api.v1.StartActionAndWaitRequest + (*StartActionAndWaitResponse)(nil), // 14: olivetin.api.v1.StartActionAndWaitResponse + (*StartActionByGetRequest)(nil), // 15: olivetin.api.v1.StartActionByGetRequest + (*StartActionByGetResponse)(nil), // 16: olivetin.api.v1.StartActionByGetResponse + (*StartActionByGetAndWaitRequest)(nil), // 17: olivetin.api.v1.StartActionByGetAndWaitRequest + (*StartActionByGetAndWaitResponse)(nil), // 18: olivetin.api.v1.StartActionByGetAndWaitResponse + (*GetLogsRequest)(nil), // 19: olivetin.api.v1.GetLogsRequest + (*LogEntry)(nil), // 20: olivetin.api.v1.LogEntry + (*GetLogsResponse)(nil), // 21: olivetin.api.v1.GetLogsResponse + (*GetActionLogsRequest)(nil), // 22: olivetin.api.v1.GetActionLogsRequest + (*GetActionLogsResponse)(nil), // 23: olivetin.api.v1.GetActionLogsResponse + (*ValidateArgumentTypeRequest)(nil), // 24: olivetin.api.v1.ValidateArgumentTypeRequest + (*ValidateArgumentTypeResponse)(nil), // 25: olivetin.api.v1.ValidateArgumentTypeResponse + (*WatchExecutionRequest)(nil), // 26: olivetin.api.v1.WatchExecutionRequest + (*WatchExecutionUpdate)(nil), // 27: olivetin.api.v1.WatchExecutionUpdate + (*ExecutionStatusRequest)(nil), // 28: olivetin.api.v1.ExecutionStatusRequest + (*ExecutionStatusResponse)(nil), // 29: olivetin.api.v1.ExecutionStatusResponse + (*WhoAmIRequest)(nil), // 30: olivetin.api.v1.WhoAmIRequest + (*WhoAmIResponse)(nil), // 31: olivetin.api.v1.WhoAmIResponse + (*SosReportRequest)(nil), // 32: olivetin.api.v1.SosReportRequest + (*SosReportResponse)(nil), // 33: olivetin.api.v1.SosReportResponse + (*DumpVarsRequest)(nil), // 34: olivetin.api.v1.DumpVarsRequest + (*DumpVarsResponse)(nil), // 35: olivetin.api.v1.DumpVarsResponse + (*DebugBinding)(nil), // 36: olivetin.api.v1.DebugBinding + (*DumpPublicIdActionMapRequest)(nil), // 37: olivetin.api.v1.DumpPublicIdActionMapRequest + (*DumpPublicIdActionMapResponse)(nil), // 38: olivetin.api.v1.DumpPublicIdActionMapResponse + (*GetReadyzRequest)(nil), // 39: olivetin.api.v1.GetReadyzRequest + (*GetReadyzResponse)(nil), // 40: olivetin.api.v1.GetReadyzResponse + (*EventStreamRequest)(nil), // 41: olivetin.api.v1.EventStreamRequest + (*EventStreamResponse)(nil), // 42: olivetin.api.v1.EventStreamResponse + (*EventOutputChunk)(nil), // 43: olivetin.api.v1.EventOutputChunk + (*EventEntityChanged)(nil), // 44: olivetin.api.v1.EventEntityChanged + (*EventConfigChanged)(nil), // 45: olivetin.api.v1.EventConfigChanged + (*EventExecutionFinished)(nil), // 46: olivetin.api.v1.EventExecutionFinished + (*EventExecutionStarted)(nil), // 47: olivetin.api.v1.EventExecutionStarted + (*KillActionRequest)(nil), // 48: olivetin.api.v1.KillActionRequest + (*KillActionResponse)(nil), // 49: olivetin.api.v1.KillActionResponse + (*LocalUserLoginRequest)(nil), // 50: olivetin.api.v1.LocalUserLoginRequest + (*LocalUserLoginResponse)(nil), // 51: olivetin.api.v1.LocalUserLoginResponse + (*PasswordHashRequest)(nil), // 52: olivetin.api.v1.PasswordHashRequest + (*PasswordHashResponse)(nil), // 53: olivetin.api.v1.PasswordHashResponse + (*LogoutRequest)(nil), // 54: olivetin.api.v1.LogoutRequest + (*LogoutResponse)(nil), // 55: olivetin.api.v1.LogoutResponse + (*GetDiagnosticsRequest)(nil), // 56: olivetin.api.v1.GetDiagnosticsRequest + (*GetDiagnosticsResponse)(nil), // 57: olivetin.api.v1.GetDiagnosticsResponse + (*InitRequest)(nil), // 58: olivetin.api.v1.InitRequest + (*InitResponse)(nil), // 59: olivetin.api.v1.InitResponse + (*AdditionalLink)(nil), // 60: olivetin.api.v1.AdditionalLink + (*OAuth2Provider)(nil), // 61: olivetin.api.v1.OAuth2Provider + (*GetActionBindingRequest)(nil), // 62: olivetin.api.v1.GetActionBindingRequest + (*GetActionBindingResponse)(nil), // 63: olivetin.api.v1.GetActionBindingResponse + (*GetEntitiesRequest)(nil), // 64: olivetin.api.v1.GetEntitiesRequest + (*GetEntitiesResponse)(nil), // 65: olivetin.api.v1.GetEntitiesResponse + (*EntityDefinition)(nil), // 66: olivetin.api.v1.EntityDefinition + (*GetEntityRequest)(nil), // 67: olivetin.api.v1.GetEntityRequest + (*RestartActionRequest)(nil), // 68: olivetin.api.v1.RestartActionRequest + nil, // 69: olivetin.api.v1.ActionArgument.SuggestionsEntry + nil, // 70: olivetin.api.v1.Entity.FieldsEntry + nil, // 71: olivetin.api.v1.DumpVarsResponse.ContentsEntry + nil, // 72: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry } var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ - 1, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument - 2, // 1: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice - 68, // 2: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry - 69, // 3: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry - 7, // 4: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard - 8, // 5: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent - 8, // 6: olivetin.api.v1.DashboardComponent.contents:type_name -> olivetin.api.v1.DashboardComponent - 0, // 7: olivetin.api.v1.DashboardComponent.action:type_name -> olivetin.api.v1.Action - 10, // 8: olivetin.api.v1.StartActionRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument - 10, // 9: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument - 19, // 10: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry - 19, // 11: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry - 19, // 12: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry - 19, // 13: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry - 19, // 14: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry - 70, // 15: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry - 71, // 16: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry - 43, // 17: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged - 44, // 18: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged - 45, // 19: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished - 46, // 20: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted - 42, // 21: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk - 19, // 22: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry - 19, // 23: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry - 60, // 24: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider - 59, // 25: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink - 5, // 26: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy - 0, // 27: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action - 65, // 28: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition - 3, // 29: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity - 35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding - 6, // 31: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest - 9, // 32: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest - 12, // 33: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest - 14, // 34: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest - 16, // 35: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest - 67, // 36: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest - 47, // 37: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest - 27, // 38: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest - 18, // 39: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest - 21, // 40: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest - 23, // 41: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest - 29, // 42: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest - 31, // 43: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest - 33, // 44: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest - 36, // 45: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest - 38, // 46: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest - 49, // 47: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest - 51, // 48: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest - 53, // 49: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest - 40, // 50: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest - 55, // 51: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest - 57, // 52: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest - 61, // 53: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest - 63, // 54: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest - 66, // 55: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest - 4, // 56: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse - 11, // 57: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse - 13, // 58: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse - 15, // 59: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse - 17, // 60: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse - 11, // 61: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse - 48, // 62: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse - 28, // 63: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse - 20, // 64: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse - 22, // 65: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse - 24, // 66: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse - 30, // 67: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse - 32, // 68: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse - 34, // 69: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse - 37, // 70: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse - 39, // 71: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse - 50, // 72: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse - 52, // 73: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse - 54, // 74: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse - 41, // 75: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse - 56, // 76: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse - 58, // 77: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse - 62, // 78: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse - 64, // 79: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse - 3, // 80: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity - 56, // [56:81] is the sub-list for method output_type - 31, // [31:56] is the sub-list for method input_type - 31, // [31:31] is the sub-list for extension type_name - 31, // [31:31] is the sub-list for extension extendee - 0, // [0:31] is the sub-list for field type_name + 2, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument + 1, // 1: olivetin.api.v1.Action.exec_on_webhooks:type_name -> olivetin.api.v1.ActionWebhookExecHint + 3, // 2: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice + 69, // 3: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry + 70, // 4: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry + 8, // 5: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard + 9, // 6: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent + 9, // 7: olivetin.api.v1.DashboardComponent.contents:type_name -> olivetin.api.v1.DashboardComponent + 0, // 8: olivetin.api.v1.DashboardComponent.action:type_name -> olivetin.api.v1.Action + 11, // 9: olivetin.api.v1.StartActionRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument + 11, // 10: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument + 20, // 11: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry + 20, // 12: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry + 20, // 13: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry + 20, // 14: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry + 20, // 15: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry + 71, // 16: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry + 72, // 17: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + 44, // 18: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged + 45, // 19: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged + 46, // 20: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished + 47, // 21: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted + 43, // 22: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk + 20, // 23: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry + 20, // 24: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry + 61, // 25: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider + 60, // 26: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink + 6, // 27: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy + 0, // 28: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action + 66, // 29: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition + 4, // 30: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity + 36, // 31: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding + 7, // 32: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest + 10, // 33: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest + 13, // 34: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest + 15, // 35: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest + 17, // 36: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest + 68, // 37: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest + 48, // 38: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest + 28, // 39: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest + 19, // 40: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest + 22, // 41: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest + 24, // 42: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest + 30, // 43: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest + 32, // 44: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest + 34, // 45: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest + 37, // 46: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest + 39, // 47: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest + 50, // 48: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest + 52, // 49: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest + 54, // 50: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest + 41, // 51: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest + 56, // 52: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest + 58, // 53: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest + 62, // 54: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest + 64, // 55: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest + 67, // 56: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest + 5, // 57: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse + 12, // 58: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse + 14, // 59: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse + 16, // 60: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse + 18, // 61: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse + 12, // 62: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse + 49, // 63: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse + 29, // 64: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse + 21, // 65: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse + 23, // 66: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse + 25, // 67: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse + 31, // 68: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse + 33, // 69: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse + 35, // 70: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse + 38, // 71: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse + 40, // 72: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse + 51, // 73: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse + 53, // 74: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse + 55, // 75: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse + 42, // 76: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse + 57, // 77: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse + 59, // 78: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse + 63, // 79: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse + 65, // 80: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse + 4, // 81: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity + 57, // [57:82] is the sub-list for method output_type + 32, // [32:57] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_olivetin_api_v1_olivetin_proto_init() } @@ -4387,7 +4501,7 @@ func file_olivetin_api_v1_olivetin_proto_init() { if File_olivetin_api_v1_olivetin_proto != nil { return } - file_olivetin_api_v1_olivetin_proto_msgTypes[41].OneofWrappers = []any{ + file_olivetin_api_v1_olivetin_proto_msgTypes[42].OneofWrappers = []any{ (*EventStreamResponse_EntityChanged)(nil), (*EventStreamResponse_ConfigChanged)(nil), (*EventStreamResponse_ExecutionFinished)(nil), @@ -4400,7 +4514,7 @@ func file_olivetin_api_v1_olivetin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)), NumEnums: 0, - NumMessages: 72, + NumMessages: 73, NumExtensions: 0, NumServices: 1, }, diff --git a/service/internal/api/apiActionExecTriggers.go b/service/internal/api/apiActionExecTriggers.go new file mode 100644 index 0000000..c80bf46 --- /dev/null +++ b/service/internal/api/apiActionExecTriggers.go @@ -0,0 +1,25 @@ +package api + +import ( + apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" + config "github.com/OliveTin/OliveTin/internal/config" +) + +func applyActionExecTriggers(pb *apiv1.Action, cfg *config.Action) { + if cfg == nil { + return + } + + pb.ExecOnStartup = cfg.ExecOnStartup + pb.ExecOnCron = append([]string(nil), cfg.ExecOnCron...) + pb.ExecOnFileCreatedInDir = append([]string(nil), cfg.ExecOnFileCreatedInDir...) + pb.ExecOnFileChangedInDir = append([]string(nil), cfg.ExecOnFileChangedInDir...) + pb.ExecOnCalendarFile = cfg.ExecOnCalendarFile + + for _, wh := range cfg.ExecOnWebhook { + pb.ExecOnWebhooks = append(pb.ExecOnWebhooks, &apiv1.ActionWebhookExecHint{ + Template: wh.Template, + MatchPath: wh.MatchPath, + }) + } +} diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 45fcad1..88659aa 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -156,6 +156,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque DatetimeRateLimitExpires: datetimeRateLimitExpires, } + applyActionExecTriggers(&btn, action) + for _, cfgArg := range action.Arguments { pbArg := apiv1.ActionArgument{ Name: cfgArg.Name, diff --git a/service/internal/onfileindir/fileindir.go b/service/internal/onfileindir/fileindir.go index 40bd19c..73a7ac8 100644 --- a/service/internal/onfileindir/fileindir.go +++ b/service/internal/onfileindir/fileindir.go @@ -13,6 +13,13 @@ import ( func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) { for _, action := range cfg.Actions { + for _, dirname := range action.ExecOnFileCreatedInDir { + go func(act *config.Action, dir string) { + filehelper.WatchDirectoryCreate(dir, func(filename string) { + scheduleExec(act, cfg, ex, filename) + }) + }(action, dirname) + } for _, dirname := range action.ExecOnFileChangedInDir { // Pass values into anonymous function because of this issue // https://github.com/OliveTin/OliveTin/issues/503 From b1c74c9e040e146795544c470d6505102c688944 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 23 May 2026 11:38:10 +0100 Subject: [PATCH 093/148] fmt: Cleanup coderabbit issues from action details change --- frontend/resources/vue/ActionButton.vue | 2 +- service/internal/onfileindir/fileindir.go | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/resources/vue/ActionButton.vue b/frontend/resources/vue/ActionButton.vue index 263ef31..a93d453 100644 --- a/frontend/resources/vue/ActionButton.vue +++ b/frontend/resources/vue/ActionButton.vue @@ -201,7 +201,7 @@ function openActionDetails() { async function handleClick() { if (popupOnStart.value === 'history') { - router.push(`/action/${props.actionData.bindingId}`) + openActionDetails() return } if (props.actionData.arguments && props.actionData.arguments.length > 0) { diff --git a/service/internal/onfileindir/fileindir.go b/service/internal/onfileindir/fileindir.go index 73a7ac8..b6085d1 100644 --- a/service/internal/onfileindir/fileindir.go +++ b/service/internal/onfileindir/fileindir.go @@ -29,12 +29,6 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) { scheduleExec(act, cfg, ex, filename) }) }(action, dirname) - - go func(act *config.Action, dir string) { - filehelper.WatchDirectoryCreate(dir, func(filename string) { - scheduleExec(act, cfg, ex, filename) - }) - }(action, dirname) } } } From 82f749a9cefb564f5ed0fde98a235756a7465b1a Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 25 May 2026 20:47:39 +0100 Subject: [PATCH 094/148] feat: Default icon is now a CLI HugeIcon instead of a smiley face --- .../pages/action_customization/icons.adoc | 33 ++++++--- docs/modules/ROOT/pages/config.adoc | 10 +-- frontend/resources/vue/ActionButton.vue | 23 ++----- .../vue/components/ActionIconGlyph.vue | 68 +++++++++++++++++++ .../resources/vue/views/ActionDetailsView.vue | 4 +- .../resources/vue/views/ExecutionView.vue | 3 +- frontend/resources/vue/views/LogsListView.vue | 7 +- service/internal/config/config.go | 2 +- service/internal/config/sanitize_test.go | 2 +- 9 files changed, 113 insertions(+), 39 deletions(-) create mode 100644 frontend/resources/vue/components/ActionIconGlyph.vue diff --git a/docs/modules/ROOT/pages/action_customization/icons.adoc b/docs/modules/ROOT/pages/action_customization/icons.adoc index 78ffff9..ed120b7 100644 --- a/docs/modules/ROOT/pages/action_customization/icons.adoc +++ b/docs/modules/ROOT/pages/action_customization/icons.adoc @@ -2,7 +2,7 @@ = Icons You can specify any HTML for an icon. It's a popular choice to use Unicode -icons because they are extremely fast to load and there are a lot of them, +icons because they are extremely fast to load and there are a lot of them, but OliveTin also support Iconify, and simple PNG, JPG, WEBP and similar images. .Examples of icons in OliveTin @@ -39,14 +39,31 @@ And you should get something that looks like this; image::../action-button-iconify.png[] +== HugeIcons icons (bundled) + +The OliveTin web UI ships with curated https://www.hugeicons.com/[HugeIcons] symbols. +Set `icon:` to `hugeicons:` followed by the icon export name, for example `hugeicons:NeutralIcon`. + +This is the neutral glyph OliveTin uses when no icon is configured for an action. + +.`config.yaml` +---- +actions: + - title: Action with the bundled CLI HugeIcon + icon: hugeicons:CommandLineIcon + shell: echo hello +---- + +Known `hugeicons:` names are registered in the web UI (`ActionIconGlyph` Vue component). + == Unicode icons ("emoji") -Using simple emoji (unicode) icons from your browser's font is extremely fast, and can look good on some platforms. However, the icons are platform specific, which mean's they'll look different between browsers and between operating systems. +Using simple emoji (unicode) icons from your browser's font is extremely fast, and can look good on some platforms. However, the icons are platform specific, which mean's they'll look different between browsers and between operating systems. There are great sites like link:https://symbl.cc/en/emoji/[symbl.cc - a list of -"Emoji" in unicode]. +"Emoji" in unicode]. -For example, if you find "link:https://symbl.cc/en/1F60E/[Smiling face with sunglasses]" you can click +For example, if you find "link:https://symbl.cc/en/1F60E/[Smiling face with sunglasses]" you can click on it to see it's "HTML-code". In OliveTin, you'd setup the icon like this; ---- @@ -56,16 +73,16 @@ actions: shell: echo "You are awesome" ---- -=== Unicode alises +=== Unicode alises OliveTin has hard-coded aliases for a few commonly used icons, so you don't have to type out the full unicode codes. A list of those hard coded icons is; .Alias'd unicode reference table [%header] |=== -| Alias | Rendered as +| Alias | Rendered as -| `poop` | 💩 +| `poop` | 💩 | `smile` | 😀 | `ping` | 📡 | `backup` | 💾 @@ -140,5 +157,3 @@ examples; shell: echo "I like purple" ---- //// - - diff --git a/docs/modules/ROOT/pages/config.adoc b/docs/modules/ROOT/pages/config.adoc index cd5de64..299426f 100644 --- a/docs/modules/ROOT/pages/config.adoc +++ b/docs/modules/ROOT/pages/config.adoc @@ -2,11 +2,11 @@ = Configuration OliveTin is controlled by a `config.yaml` file. On startup, it looks for this -file in the following locations; +file in the following locations; 1. The value specified by the `--configdir` argument, which defaults to the current working directory (`./`) 2. `/config/` - Mostly used for containers -3. `/etc/OliveTin/` - this is the recommended directory on Linux for your `config.yaml`. +3. `/etc/OliveTin/` - this is the recommended directory on Linux for your `config.yaml`. The most simple `config.yaml` would be something like this; @@ -18,9 +18,9 @@ actions: shell: echo 'Hello World!' ---- -The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax otherwise extremely simple. +The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax otherwise extremely simple. -For building up from here, look at the following resources; +For building up from here, look at the following resources; * See the xref:action_examples/intro.adoc[action examples] section for extra examples of what OliveTin could be configured to do. @@ -54,7 +54,7 @@ All configuration options are covered in the solution sections | `showNavigateOnStartIcons` | Show (or hide) the small icons on action buttons that indicate popup/argument/background behavior on start. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI]. | `sectionNavigationStyle` | The style of the section navigation. `sidebar`, `topbar` | `sidebar` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI]. | `defaultPopupOnStart` | The default popup to show on start. | `none` | Live reloadable | xref:action_customization/popuponstart.adoc[Popup On Start]. -| `defaultIconForActions` | The default icon to use for actions. | `smile` | Requires Restart | - +| `defaultIconForActions` | The default icon string for actions (Unicode aliases such as `smile`, `hugeicons:NeutralIcon`, HTML, Iconify snippets, images, etc.). See xref:action_customization/icons.adoc[Icons]. | `hugeicons:CommandLineIcon` | Requires Restart | - | `defaultIconForDirectories` | The default icon to use for directories. | `directory` | Requires Restart | - | `defaultIconForBack` | The default icon to use for back (from directories). | `«` | Requires Restart | - | `enableCustomJs` | Enable custom JavaScript. | `false` | Live Reloadable, but refreshing the web browser is required. | xref:advanced_configuration/webui.adoc[Custom JS]. diff --git a/frontend/resources/vue/ActionButton.vue b/frontend/resources/vue/ActionButton.vue index 8f81521..bf3ef51 100644 --- a/frontend/resources/vue/ActionButton.vue +++ b/frontend/resources/vue/ActionButton.vue @@ -15,7 +15,7 @@ - + {{ displayTitle }} {{ rateLimitMessage }} @@ -30,7 +30,9 @@ import { useRouter } from 'vue-router' import { HugeiconsIcon } from '@hugeicons/vue' import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons' -import { ref, watch, onMounted, onUnmounted, inject, computed } from 'vue' +import ActionIconGlyph from './components/ActionIconGlyph.vue' + +import { ref, watch, onMounted, onUnmounted, computed } from 'vue' const router = useRouter() const navigateOnStart = ref('') @@ -53,7 +55,6 @@ const canExec = ref(true) const popupOnStart = ref('') // Display properties -const unicodeIcon = ref('💩') const displayTitle = ref('') // State @@ -74,6 +75,8 @@ const showNavigateOnStartIcons = computed(() => { return window.initResponse?.showNavigateOnStartIcons ?? true }) +const actionGlyph = computed(() => props.actionData?.icon ?? '') + // Combined classes including custom cssClass const combinedClasses = computed(() => { const classes = [...buttonClasses.value] @@ -86,16 +89,6 @@ const combinedClasses = computed(() => { // Timestamps const updateIterationTimestamp = ref(0) -function getUnicodeIcon(icon) { - if (icon === '') { - console.log('icon not found ', icon) - - return '💩' - } else { - return unescape(icon) - } -} - function constructFromJson(json) { updateIterationTimestamp.value = 0 @@ -114,8 +107,6 @@ function constructFromJson(json) { isDisabled.value = !json.canExec displayTitle.value = title.value - unicodeIcon.value = getUnicodeIcon(json.icon) - // Initialize rate limit from action data (parse datetime string) if (json.datetimeRateLimitExpires) { const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) @@ -134,8 +125,6 @@ function updateFromJson(json) { // Fields that should not be updated // title - as the callback URL relies on it - unicodeIcon.value = getUnicodeIcon(json.icon) - // Update rate limiting if changed (parse datetime string) if (json.datetimeRateLimitExpires) { const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) diff --git a/frontend/resources/vue/components/ActionIconGlyph.vue b/frontend/resources/vue/components/ActionIconGlyph.vue new file mode 100644 index 0000000..e7290e6 --- /dev/null +++ b/frontend/resources/vue/components/ActionIconGlyph.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend/resources/vue/views/ActionDetailsView.vue b/frontend/resources/vue/views/ActionDetailsView.vue index 9d8b31e..59df188 100644 --- a/frontend/resources/vue/views/ActionDetailsView.vue +++ b/frontend/resources/vue/views/ActionDetailsView.vue @@ -22,7 +22,7 @@

- +
@@ -62,6 +62,7 @@ From 3e414564e544dbef358849ad6d0ec3af18dddece Mon Sep 17 00:00:00 2001 From: jamesread Date: Tue, 26 May 2026 00:28:52 +0100 Subject: [PATCH 096/148] fix: Dont rely on icon DOM text for ExecutionView --- frontend/resources/vue/ActionButton.vue | 9 +++++++++ frontend/resources/vue/views/ExecutionView.vue | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/resources/vue/ActionButton.vue b/frontend/resources/vue/ActionButton.vue index 6223469..f2d2f6a 100644 --- a/frontend/resources/vue/ActionButton.vue +++ b/frontend/resources/vue/ActionButton.vue @@ -79,6 +79,7 @@ const showNavigateOnStartIcons = computed(() => { }) const actionGlyph = computed(() => props.actionData?.icon ?? '') +const glyph = ref('') // Combined classes including custom cssClass const combinedClasses = computed(() => { @@ -112,6 +113,7 @@ function constructFromJson(json) { isDisabled.value = !json.canExec displayTitle.value = title.value + glyph.value = json.icon ?? '' // Initialize rate limit from action data (parse datetime string) if (json.datetimeRateLimitExpires) { const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) @@ -313,10 +315,17 @@ watch( () => props.actionData, (newData) => { updateFromJson(newData) + if (newData?.icon !== undefined) { + glyph.value = newData.icon ?? '' + } }, { deep: true } ) +defineExpose({ + glyph +}) + \ No newline at end of file + diff --git a/frontend/resources/vue/components/ConnectionBanner.vue b/frontend/resources/vue/components/ConnectionBanner.vue index ebf2928..926553c 100644 --- a/frontend/resources/vue/components/ConnectionBanner.vue +++ b/frontend/resources/vue/components/ConnectionBanner.vue @@ -1,5 +1,5 @@