Merge branch 'next' into feat-default-icon-cli-hugeicon

This commit is contained in:
James Read 2026-05-25 21:34:54 +01:00 committed by GitHub
commit ae928625a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1147 additions and 371 deletions

View File

@ -10,6 +10,8 @@ updates:
labels: labels:
- "3k" - "3k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# npm updates for frontend - targeting "release/2k" branch (security updates only) # npm updates for frontend - targeting "release/2k" branch (security updates only)
- package-ecosystem: "npm" - package-ecosystem: "npm"
@ -21,6 +23,8 @@ updates:
labels: labels:
- "2k" - "2k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# npm updates for integration-tests - targeting "next" branch # npm updates for integration-tests - targeting "next" branch
- package-ecosystem: "npm" - package-ecosystem: "npm"
@ -32,6 +36,8 @@ updates:
labels: labels:
- "3k" - "3k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# npm updates for integration-tests - targeting "release/2k" branch (security updates only) # npm updates for integration-tests - targeting "release/2k" branch (security updates only)
- package-ecosystem: "npm" - package-ecosystem: "npm"
@ -43,6 +49,8 @@ updates:
labels: labels:
- "2k" - "2k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# Go modules updates for service - targeting "next" branch # Go modules updates for service - targeting "next" branch
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
@ -54,6 +62,8 @@ updates:
labels: labels:
- "3k" - "3k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# Go modules updates for service - targeting "release/2k" branch (security updates only) # Go modules updates for service - targeting "release/2k" branch (security updates only)
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
@ -65,6 +75,8 @@ updates:
labels: labels:
- "2k" - "2k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# Go modules updates for lang - targeting "next" branch # Go modules updates for lang - targeting "next" branch
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
@ -76,6 +88,8 @@ updates:
labels: labels:
- "3k" - "3k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# Go modules updates for lang - targeting "release/2k" branch (security updates only) # Go modules updates for lang - targeting "release/2k" branch (security updates only)
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
@ -87,6 +101,8 @@ updates:
labels: labels:
- "2k" - "2k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# Docker updates - targeting "next" branch # Docker updates - targeting "next" branch
- package-ecosystem: "docker" - package-ecosystem: "docker"
@ -98,6 +114,8 @@ updates:
labels: labels:
- "3k" - "3k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7
# Docker updates - targeting "release/2k" branch (security updates only) # Docker updates - targeting "release/2k" branch (security updates only)
- package-ecosystem: "docker" - package-ecosystem: "docker"
@ -109,4 +127,6 @@ updates:
labels: labels:
- "2k" - "2k"
- "dependencies" - "dependencies"
cooldown:
default-days: 7

View File

@ -3,6 +3,16 @@ name: "Build & Release pipeline"
on: on:
pull_request: pull_request:
paths:
- '.github/workflows/build-and-release.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
workflow_dispatch: workflow_dispatch:
push: push:
tags: tags:
@ -11,31 +21,46 @@ on:
- main - main
- next - next
- beta - beta
paths:
- '.github/workflows/build-and-release.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
with: with:
image: tonistiigi/binfmt:latest image: tonistiigi/binfmt:latest
platforms: arm64,arm platforms: arm64,arm
- name: Setup node - name: Setup node (npm cache)
uses: actions/setup-node@v4 if: github.event_name != 'pull_request'
uses: actions/setup-node@v6.4.0
with: with:
cache: 'npm' cache: 'npm'
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
- name: Setup node
if: github.event_name == 'pull_request'
uses: actions/setup-node@v6.4.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: 'service/go.mod' go-version-file: 'service/go.mod'
cache: true cache: true
@ -45,7 +70,7 @@ jobs:
run: go version run: go version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_KEY }} password: ${{ secrets.DOCKERHUB_KEY }}
@ -74,7 +99,7 @@ jobs:
run: cd integration-tests && make -w run: cd integration-tests && make -w
- name: Archive integration tests - name: Archive integration tests
uses: actions/upload-artifact@v4.3.1 uses: actions/upload-artifact@v7
if: always() if: always()
with: with:
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}" name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"

View File

@ -15,13 +15,19 @@ name: "CodeQL"
on: on:
push: push:
paths: paths:
- 'cmd/**' - '.github/workflows/codeql-analysis.yml'
- 'internal/**' - 'frontend/**'
- 'webui.dev/**'
- 'integration-tests/**' - 'integration-tests/**'
- 'OliveTin.proto' - 'proto/**'
- 'service/**'
branches: [main] branches: [main]
pull_request: pull_request:
paths:
- '.github/workflows/codeql-analysis.yml'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
branches: [main] branches: [main]
schedule: schedule:
- cron: '25 10 * * 5' - cron: '25 10 * * 5'

View File

@ -4,11 +4,11 @@ name: "Codestyle checks"
on: on:
push: push:
paths: paths:
- 'cmd/**' - '.github/workflows/codestyle.yml'
- 'internal/**' - 'frontend/**'
- 'webui.dev/**'
- 'integration-tests/**' - 'integration-tests/**'
- 'OliveTin.proto' - 'proto/**'
- 'service/**'
jobs: jobs:

View File

@ -7,8 +7,28 @@ name: DevSkim
on: on:
push: push:
paths:
- '.github/workflows/devskim.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
branches: [ "main" ] branches: [ "main" ]
pull_request: pull_request:
paths:
- '.github/workflows/devskim.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
branches: [ "main" ] branches: [ "main" ]
schedule: schedule:
- cron: '34 21 * * 2' - cron: '34 21 * * 2'

View File

@ -86,6 +86,7 @@
** xref:dashboards/2-fieldsets.adoc[Fieldsets] ** xref:dashboards/2-fieldsets.adoc[Fieldsets]
** xref:dashboards/3-folders.adoc[Folders] ** xref:dashboards/3-folders.adoc[Folders]
** xref:dashboards/4-displays.adoc[Displays] ** xref:dashboards/4-displays.adoc[Displays]
** xref:dashboards/faq-display-hyperlinks.adoc[Hyperlinks (Displays)]
** xref:dashboards/5-output-views.adoc[Output Views] ** xref:dashboards/5-output-views.adoc[Output Views]
** xref:dashboards/entity-directories.adoc[Entity Directories] ** xref:dashboards/entity-directories.adoc[Entity Directories]
* xref:entities/intro.adoc[Entities] * xref:entities/intro.adoc[Entities]
@ -95,6 +96,7 @@
* xref:security/concepts.adoc[Security] * xref:security/concepts.adoc[Security]
** xref:security/acl.adoc[Access Control Lists] ** xref:security/acl.adoc[Access Control Lists]
** xref:security/local.adoc[Local Users Authorization] ** xref:security/local.adoc[Local Users Authorization]
** xref:security/api_keys.adoc[API Keys]
** xref:security/trusted_header.adoc[Trusted Header Authorization] ** xref:security/trusted_header.adoc[Trusted Header Authorization]
** xref:security/jwt.adoc[JWT Authorization] ** xref:security/jwt.adoc[JWT Authorization]
*** xref:security/jwt_keys.adoc[JWT with Keys] *** xref:security/jwt_keys.adoc[JWT with Keys]

View File

@ -68,4 +68,16 @@ actions:
image::../executionButtons.png[] image::../executionButtons.png[]
== Action execution history
The `history` option opens the action details page for that binding when the execution starts, so you can see past runs and status for the same action.
[source,yaml]
.`config.yaml`
----
actions:
- title: Long-running job
popupOnStart: history
----

View File

@ -44,6 +44,7 @@ image::defaultUiHideNav.png[]
When enabled (the default), each action button can show a small icon indicating what happens when the action is started: When enabled (the default), each action button can show a small icon indicating what happens when the action is started:
* **Popup dialog** — the action opens a popup (e.g. `popupOnStart: execution-dialog`) * **Popup dialog** — the action opens a popup (e.g. `popupOnStart: execution-dialog`)
* **Action history** — the action opens the action details page (e.g. `popupOnStart: history`)
* **Argument form** — the action opens an argument form on start * **Argument form** — the action opens an argument form on start
* **Run in background** — the action runs without opening a dialog * **Run in background** — the action runs without opening a dialog

View File

@ -0,0 +1,43 @@
[#faq-dashboard-display-hyperlinks]
= Hyperlinks in dashboards
This page explains how to add clickable links on dashboards using xref:dashboards/4-displays.adoc[display components] (`type: display`).
== How do I add a clickable link on a dashboard?
Use a `type: display` component and put normal HTML in its `title` field. OliveTin renders `title` as HTML, so use an anchor element, for example:
[source,html]
----
<a href="https://example.com">Documentation</a>
----
== Should I open external links in a new tab?
For links that leave OliveTin, `target="_blank"` is convenient. Combine it with `rel="noopener noreferrer"` so the new page cannot access your OliveTin tab and referrer details are limited. Example:
[source,yaml]
----
contents:
- type: display
title: |
<a href="https://example.com/docs" target="_blank" rel="noopener noreferrer">Open docs</a>
----
== Can I mix links with entity variables or plain text?
Yes. `title` can combine literal text, xref:entities/intro.adoc[entity template variables], and HTML (such as `<br />`, `<strong>`, or `<a>`). Use YAML's `|` block scalar when you need several lines.
== Does Markdown link syntax like `[text](url)` work?
No. Display `title` is not run through a Markdown parser; it is treated as HTML. Use `<a href="...">...</a>` (or other tags you need) explicitly.
== What should I avoid when embedding HTML?
Treat anything you put in `title` as trusted markup only you control; do not paste untrusted strings into `href` or other attributes without strict validation.
Some deployments use xref:security/content_security_policy.adoc[Content Security Policy] headers, which may block certain schemes or injected scripts—ordinary `https:` links are usually the safest choice.
== Where else is this documented?
See xref:dashboards/4-displays.adoc[Displays] for full display configuration (including xref:dashboards/css.adoc[CSS classes]).

View File

@ -89,6 +89,7 @@ Now that you understand dashboards, explore these related features:
* xref:dashboards/2-fieldsets.adoc[Learn about fieldsets] - Group actions visually on dashboards * xref:dashboards/2-fieldsets.adoc[Learn about fieldsets] - Group actions visually on dashboards
* xref:dashboards/3-folders.adoc[Organize with folders] - Create folder structures for better organization * xref:dashboards/3-folders.adoc[Organize with folders] - Create folder structures for better organization
* xref:dashboards/4-displays.adoc[Add displays] - Show information alongside actions * xref:dashboards/4-displays.adoc[Add displays] - Show information alongside actions
* xref:dashboards/faq-display-hyperlinks.adoc[FAQ: Hyperlinks in displays] - Clickable links in display components
* xref:dashboards/5-output-views.adoc[Configure output views] - Customize how action output is displayed * xref:dashboards/5-output-views.adoc[Configure output views] - Customize how action output is displayed
* xref:entities/intro.adoc[Use entities with dashboards] - Dynamically generate actions from entity files * xref:entities/intro.adoc[Use entities with dashboards] - Dynamically generate actions from entity files
* xref:dashboards/examples.adoc[View dashboard examples] - See complete dashboard configurations * xref:dashboards/examples.adoc[View dashboard examples] - See complete dashboard configurations

View File

@ -0,0 +1,66 @@
[#api-keys]
= API Keys
This page is for **developers** who want to call OliveTin's HTTP API (Connect RPC under `/api/`) using a **Bearer token**, without using the interactive web login.
API keys are configured on xref:security/local.adoc[local users] as an optional `apiKey` field. When present, clients can authenticate by sending:
----
Authorization: Bearer <your-api-key>
----
The prefix `Bearer ` (including the trailing space after `Bearer`) must match exactly.
== Configuration
include::partial$config-start.adoc[]
----
authLocalUsers:
enabled: true
users:
- username: automation
usergroup: bots
apiKey: "{{ .Env.OLIVETIN_AUTOMATION_KEY }}"
- username: alice
usergroup: admins
password: $argon2id$v=19$m=65536,t=4,p=6$...
apiKey: "{{ .Env.OLIVETIN_ALICE_API_KEY }}"
----
* Use a **long, random** API key (similar to any other bearer secret).
* Prefer loading the key from the environment with `{{ .Env.VAR }}` instead of committing the raw value to disk.
* **TLS**: send bearer tokens only over HTTPS in real deployments.
* **Interactive login**: if a user has **no** `password` configured, they **cannot** use the `/login` page; they can only authenticate with an API key (or another auth mechanism you configure separately).
Two local users **must not** share the same `apiKey` value. OliveTin will refuse to start if duplicate keys are detected.
== Authorization (permissions)
API key authentication uses the same **username** and **usergroup** as the matching local user. xref:security/acl.adoc[Access Control Lists] and `defaultPermissions` apply in the same way as for users who sign in via the web UI.
== Example: curl and Init
The OliveTin API is **Connect RPC**. Unary calls accept JSON bodies. The following example calls `Init` with an empty request object:
[source,bash]
----
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
"https://olivetin.example.com:1337/api/olivetin.api.v1.OliveTinApiService/Init" \
--data '{}'
----
Replace the host, port, and path prefix if your installation differs. Other RPCs use the same URL pattern with a different final segment (method name).
== Operational security notes
* **Reverse proxies**: if you use xref:security/trusted_header.adoc[Trusted Header Authorization], remember it is evaluated **before** bearer API keys. Do not expose OliveTin in a way that allows clients to spoof trusted identity headers.
* **Debug logging**: avoid enabling `logDebugOptions.singleFrontendRequestHeaders` in production. OliveTin redacts common sensitive headers (including `Authorization`) in debug output, but minimizing debug surface area is still recommended.
* **Brute force**: OliveTin does not ship per-IP rate limiting for failed bearer attempts. Consider rate limiting or WAF rules on `/api/` at your reverse proxy.
== See also
* xref:security/local.adoc[Local Users Authorization] (password hashing and local user basics)
* xref:security/acl.adoc[Access Control Lists]

View File

@ -3,6 +3,8 @@
OliveTin supports just basic users defined with a username and password in the config.yaml file. This can be used when you do not want to use a full authentication system like LDAP, OAuth2 or a Reverse Proxy. OliveTin supports just basic users defined with a username and password in the config.yaml file. This can be used when you do not want to use a full authentication system like LDAP, OAuth2 or a Reverse Proxy.
For programmatic access (scripts, integrations) using per-user bearer API keys, see xref:security/api_keys.adoc[API Keys].
== Define a user == Define a user
include::partial$config-start.adoc[] include::partial$config-start.adoc[]

View File

@ -11,9 +11,9 @@
"dependencies": { "dependencies": {
"@connectrpc/connect": "^2.1.1", "@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1", "@connectrpc/connect-web": "^2.1.1",
"@hugeicons/core-free-icons": "^4.1.3", "@hugeicons/core-free-icons": "^4.1.4",
"@hugeicons/vue": "^1.0.5", "@hugeicons/vue": "^1.0.5",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.7",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
@ -21,14 +21,14 @@
"picocrank": "^1.15.0", "picocrank": "^1.15.0",
"standard": "^17.1.2", "standard": "^17.1.2",
"unplugin-vue-components": "^32.0.0", "unplugin-vue-components": "^32.0.0",
"vite": "^8.0.12", "vite": "^8.0.13",
"vue": "^3.5.34", "vue": "^3.5.34",
"vue-i18n": "^11.4.2", "vue-i18n": "^11.4.4",
"vue-router": "^5.0.6" "vue-router": "^5.0.7"
}, },
"devDependencies": { "devDependencies": {
"process": "^0.11.10", "process": "^0.11.10",
"stylelint": "^17.11.0", "stylelint": "^17.11.1",
"stylelint-config-standard": "^40.0.0" "stylelint-config-standard": "^40.0.0"
} }
}, },
@ -48,19 +48,66 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.29.0", "version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz",
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.0", "@babel/parser": "^8.0.0-rc.5",
"@babel/types": "^7.29.0", "@babel/types": "^8.0.0-rc.5",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"@types/jsesc": "^2.5.0",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.5.tgz",
"integrity": "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.5.tgz",
"integrity": "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/parser": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.5.tgz",
"integrity": "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^8.0.0-rc.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/types": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.5.tgz",
"integrity": "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^8.0.0-rc.5",
"@babel/helper-validator-identifier": "^8.0.0-rc.5"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
@ -891,9 +938,9 @@
} }
}, },
"node_modules/@hugeicons/core-free-icons": { "node_modules/@hugeicons/core-free-icons": {
"version": "4.1.3", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.1.3.tgz", "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.1.4.tgz",
"integrity": "sha512-FWPrKnlYKpSaitUtlZhFlDQXDgHiayTPFJYWvyIKkW2RI6Vj5KBvjxI+lAnnFPu07SwgIMiDDj+Gttl0t+o/oQ==", "integrity": "sha512-vkMvlnW7Atqh7juhZCadYDvqABWcEeVAkavAK6HJBFbYkLCme5CN51Oc91D5j5ruDja+HQPjMfWkTxxulcwvAQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@hugeicons/vue": { "node_modules/@hugeicons/vue": {
@ -947,61 +994,61 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@intlify/core-base": { "node_modules/@intlify/core-base": {
"version": "11.4.2", "version": "11.4.4",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.4.tgz",
"integrity": "sha512-7fpuCcVmeLv2T9qHsARqGvh8xt+sV2fH+Q+gMHFwB/rPXzo85DpbJFKn7dBH1L5p0c2cSh2DW+2h/64EKrISmA==", "integrity": "sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/devtools-types": "11.4.2", "@intlify/devtools-types": "11.4.4",
"@intlify/message-compiler": "11.4.2", "@intlify/message-compiler": "11.4.4",
"@intlify/shared": "11.4.2" "@intlify/shared": "11.4.4"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://github.com/sponsors/kazupon"
} }
}, },
"node_modules/@intlify/devtools-types": { "node_modules/@intlify/devtools-types": {
"version": "11.4.2", "version": "11.4.4",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.4.tgz",
"integrity": "sha512-3u8EN1kB6EMSi96KXs5k7a8y2X2g4+h3X6iwVZU47cP4n+mTuq//WMjG588BzSp/2XQ/dTXo2BLUXX+XS+PNfA==", "integrity": "sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/core-base": "11.4.2", "@intlify/core-base": "11.4.4",
"@intlify/shared": "11.4.2" "@intlify/shared": "11.4.4"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://github.com/sponsors/kazupon"
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
"version": "11.4.2", "version": "11.4.4",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.4.tgz",
"integrity": "sha512-a6CDSGSMTGrg0BjD97x8TBYPf7qQMDlZipJ6UDfv/pd4OIym8TMlHu3MsH0bTNnRdAG2D6EFEykIgiQPqvtTkA==", "integrity": "sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/shared": "11.4.2", "@intlify/shared": "11.4.4",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://github.com/sponsors/kazupon"
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
"version": "11.4.2", "version": "11.4.4",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.4.tgz",
"integrity": "sha512-NzpHbguRCsOHDwxmlBa9qu/imc+/QWgsYUaK6FZeNC0wK8QfAbhqrktEp/haVzxU1aikH8IX4ytD+mfFEMi/9A==", "integrity": "sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 16" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://github.com/sponsors/kazupon"
@ -1110,18 +1157,18 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.129.0", "version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/Boshen" "url": "https://github.com/sponsors/Boshen"
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1135,9 +1182,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1151,9 +1198,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1167,9 +1214,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1183,9 +1230,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1199,9 +1246,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1215,9 +1262,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1231,9 +1278,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1247,9 +1294,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -1263,9 +1310,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1279,9 +1326,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1295,9 +1342,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1311,9 +1358,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@ -1329,9 +1376,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1345,9 +1392,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1361,9 +1408,9 @@
} }
}, },
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.13", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
@ -1395,6 +1442,12 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/jsesc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
"license": "MIT"
},
"node_modules/@types/json5": { "node_modules/@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -1408,12 +1461,12 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "6.0.6", "version": "6.0.7",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
"integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.13" "@rolldown/pluginutils": "^1.0.1"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
@ -1507,28 +1560,22 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-kit": { "node_modules/@vue/devtools-kit": {
"version": "8.0.6", "version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz",
"integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==", "integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-shared": "^8.0.6", "@vue/devtools-shared": "^8.1.2",
"birpc": "^2.6.1", "birpc": "^2.6.1",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"mitt": "^3.0.1", "perfect-debounce": "^2.0.0"
"perfect-debounce": "^2.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
} }
}, },
"node_modules/@vue/devtools-shared": { "node_modules/@vue/devtools-shared": {
"version": "8.0.6", "version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz",
"integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==", "integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==",
"license": "MIT", "license": "MIT"
"dependencies": {
"rfdc": "^1.4.1"
}
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.34", "version": "3.5.34",
@ -2086,21 +2133,6 @@
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
"license": "MIT",
"dependencies": {
"is-what": "^5.2.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
@ -4036,15 +4068,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@ -4181,18 +4204,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-what": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@ -4798,12 +4809,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mlly": { "node_modules/mlly": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
@ -5577,12 +5582,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -5600,13 +5599,13 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.129.0", "@oxc-project/types": "=0.130.0",
"@rolldown/pluginutils": "1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
@ -5615,29 +5614,23 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.0" "@rolldown/binding-win32-x64-msvc": "1.0.1"
} }
}, },
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
"license": "MIT"
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -5918,15 +5911,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/standard": { "node_modules/standard": {
"version": "17.1.2", "version": "17.1.2",
"resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz", "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz",
@ -6146,9 +6130,9 @@
} }
}, },
"node_modules/stylelint": { "node_modules/stylelint": {
"version": "17.11.0", "version": "17.11.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.1.tgz",
"integrity": "sha512-/3czzmbF9XdGWvReDF3Ex4R23Ajolo7j8RB2bFNEqk6Ht356nlpVV+G5bG2Qt8AW1ofJzXztBRDnAtd7cgowWA==", "integrity": "sha512-+smN/HqVTggUx3iuAzOi9fPh8SrH+cJWlZrYVldXoJ06orWBhZ4Ue/QEp64oei6pVrAh4w3tG+Y12Vw7MbCFRQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -6183,13 +6167,12 @@
"html-tags": "^5.1.0", "html-tags": "^5.1.0",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"import-meta-resolve": "^4.2.0", "import-meta-resolve": "^4.2.0",
"is-plain-object": "^5.0.0",
"mathml-tag-names": "^4.0.0", "mathml-tag-names": "^4.0.0",
"meow": "^14.1.0", "meow": "^14.1.0",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"postcss": "^8.5.13", "postcss": "^8.5.14",
"postcss-safe-parser": "^7.0.1", "postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.1.1", "postcss-selector-parser": "^7.1.1",
"postcss-value-parser": "^4.2.0", "postcss-value-parser": "^4.2.0",
@ -6333,18 +6316,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/superjson": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
"license": "MIT",
"dependencies": {
"copy-anything": "^4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -6798,15 +6769,15 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.12", "version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.14", "postcss": "^8.5.14",
"rolldown": "1.0.0", "rolldown": "1.0.1",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.16"
}, },
"bin": { "bin": {
@ -6908,18 +6879,18 @@
} }
}, },
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "11.4.2", "version": "11.4.4",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.2.tgz", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.4.tgz",
"integrity": "sha512-sADDeKXqAGsPX6tK3t3y2ZiMpbVWN12tG+MhTiJ06rVoh58eGtM4wFyw3uWGbVkXByVp9Ne/AP+nSSzI+J9OAQ==", "integrity": "sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/core-base": "11.4.2", "@intlify/core-base": "11.4.4",
"@intlify/devtools-types": "11.4.2", "@intlify/devtools-types": "11.4.4",
"@intlify/shared": "11.4.2", "@intlify/shared": "11.4.4",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://github.com/sponsors/kazupon"
@ -6929,14 +6900,14 @@
} }
}, },
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "5.0.6", "version": "5.0.7",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.6.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.7.tgz",
"integrity": "sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==", "integrity": "sha512-dqfk8kvRbCutmCOCj/XLDqDEYxc1wBdAOGLuVy5M93ifYMsBd5fIjfaPN4tQAbxr5IprdBDIox1gr4wYyOx/SA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/generator": "^7.28.6", "@babel/generator": "^8.0.0-rc.4",
"@vue-macros/common": "^3.1.1", "@vue-macros/common": "^3.1.1",
"@vue/devtools-api": "^8.0.6", "@vue/devtools-api": "^8.1.1",
"ast-walker-scope": "^0.8.3", "ast-walker-scope": "^0.8.3",
"chokidar": "^5.0.0", "chokidar": "^5.0.0",
"json5": "^2.2.3", "json5": "^2.2.3",
@ -6957,9 +6928,9 @@
}, },
"peerDependencies": { "peerDependencies": {
"@pinia/colada": ">=0.21.2", "@pinia/colada": ">=0.21.2",
"@vue/compiler-sfc": "^3.5.17", "@vue/compiler-sfc": "^3.5.34",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.0" "vue": "^3.5.34"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@pinia/colada": { "@pinia/colada": {
@ -6974,12 +6945,12 @@
} }
}, },
"node_modules/vue-router/node_modules/@vue/devtools-api": { "node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "8.0.6", "version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz",
"integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==", "integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-kit": "^8.0.6" "@vue/devtools-kit": "^8.1.2"
} }
}, },
"node_modules/vue-router/node_modules/json5": { "node_modules/vue-router/node_modules/json5": {

View File

@ -6,7 +6,7 @@
"source": "index.html", "source": "index.html",
"devDependencies": { "devDependencies": {
"process": "^0.11.10", "process": "^0.11.10",
"stylelint": "^17.11.0", "stylelint": "^17.11.1",
"stylelint-config-standard": "^40.0.0" "stylelint-config-standard": "^40.0.0"
}, },
"scripts": { "scripts": {
@ -24,9 +24,9 @@
"dependencies": { "dependencies": {
"@connectrpc/connect": "^2.1.1", "@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1", "@connectrpc/connect-web": "^2.1.1",
"@hugeicons/core-free-icons": "^4.1.3", "@hugeicons/core-free-icons": "^4.1.4",
"@hugeicons/vue": "^1.0.5", "@hugeicons/vue": "^1.0.5",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.7",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
@ -34,9 +34,9 @@
"picocrank": "^1.15.0", "picocrank": "^1.15.0",
"standard": "^17.1.2", "standard": "^17.1.2",
"unplugin-vue-components": "^32.0.0", "unplugin-vue-components": "^32.0.0",
"vite": "^8.0.12", "vite": "^8.0.13",
"vue": "^3.5.34", "vue": "^3.5.34",
"vue-i18n": "^11.4.2", "vue-i18n": "^11.4.4",
"vue-router": "^5.0.6" "vue-router": "^5.0.7"
} }
} }

View File

@ -10,6 +10,9 @@
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start"> <div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
<HugeiconsIcon :icon="TypeCursorIcon" /> <HugeiconsIcon :icon="TypeCursorIcon" />
</div> </div>
<div v-if="navigateOnStart == 'hist'" class="navigate-on-start" title="Opens action execution history on start">
<HugeiconsIcon :icon="WorkHistoryIcon" />
</div>
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background"> <div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
<HugeiconsIcon :icon="WorkoutRunIcon" /> <HugeiconsIcon :icon="WorkoutRunIcon" />
</div> </div>
@ -28,7 +31,7 @@ import { buttonResults } from './stores/buttonResults'
import { rateLimits } from './stores/rateLimits' import { rateLimits } from './stores/rateLimits'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue' import { HugeiconsIcon } from '@hugeicons/vue'
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons' import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon, WorkHistoryIcon } from '@hugeicons/core-free-icons'
import ActionIconGlyph from './components/ActionIconGlyph.vue' import ActionIconGlyph from './components/ActionIconGlyph.vue'
@ -101,6 +104,8 @@ function constructFromJson(json) {
if (popupOnStart.value.includes('execution-dialog')) { if (popupOnStart.value.includes('execution-dialog')) {
navigateOnStart.value = 'pop' navigateOnStart.value = 'pop'
} else if (popupOnStart.value === 'history') {
navigateOnStart.value = 'hist'
} else if (props.actionData.arguments.length > 0) { } else if (props.actionData.arguments.length > 0) {
navigateOnStart.value = 'arg' navigateOnStart.value = 'arg'
} }
@ -233,6 +238,8 @@ function onLogEntryChanged(logEntry) {
function onExecutionStarted(logEntry) { function onExecutionStarted(logEntry) {
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) { if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
router.push(`/logs/${logEntry.executionTrackingId}`) router.push(`/logs/${logEntry.executionTrackingId}`)
} else if (popupOnStart.value === 'history') {
router.push(`/action/${bindingId.value}`)
} }
isDisabled.value = true isDisabled.value = true

View File

@ -17,7 +17,7 @@
<dt>Timeout</dt> <dt>Timeout</dt>
<dd>{{ action.timeout }} seconds</dd> <dd>{{ action.timeout }} seconds</dd>
</dl> </dl>
<p v-if="action" class = "fg1"> <p class = "fg1">
Execution history for this action. You can filter by execution tracking ID. Execution history for this action. You can filter by execution tracking ID.
</p> </p>
</div> </div>
@ -47,6 +47,7 @@
<thead> <thead>
<tr> <tr>
<th>Timestamp</th> <th>Timestamp</th>
<th>Duration</th>
<th>Execution ID</th> <th>Execution ID</th>
<th>Metadata</th> <th>Metadata</th>
<th>Status</th> <th>Status</th>
@ -55,6 +56,7 @@
<tbody> <tbody>
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle"> <tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td> <td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
<td class="duration">{{ formatExecutionDuration(log) }}</td>
<td> <td>
<router-link :to="`/logs/${log.executionTrackingId}`"> <router-link :to="`/logs/${log.executionTrackingId}`">
{{ log.executionTrackingId }} {{ log.executionTrackingId }}
@ -70,9 +72,7 @@
</span> </span>
</td> </td>
<td class="exit-code"> <td class="exit-code">
<span :class="getStatusClass(log) + ' annotation'"> <ActionStatusDisplay :logEntry="log" />
{{ getStatusText(log) }}
</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -90,11 +90,12 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import Pagination from 'picocrank/vue/components/Pagination.vue' import Pagination from 'picocrank/vue/components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue' import Section from 'picocrank/vue/components/Section.vue'
import ActionIconGlyph from '../components/ActionIconGlyph.vue' import ActionIconGlyph from '../components/ActionIconGlyph.vue'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -106,6 +107,8 @@ const pageSize = ref(10)
const currentPage = ref(1) const currentPage = ref(1)
const loading = ref(false) const loading = ref(false)
const totalCount = ref(0) const totalCount = ref(0)
const durationClock = ref(Date.now())
let durationTicker = null
const filteredLogs = computed(() => { const filteredLogs = computed(() => {
if (!searchText.value) { if (!searchText.value) {
@ -138,6 +141,7 @@ async function fetchActionLogs() {
pageSize.value = serverPageSize pageSize.value = serverPageSize
} }
totalCount.value = Number(response.totalCount) || 0 totalCount.value = Number(response.totalCount) || 0
syncDurationTicker()
} catch (err) { } catch (err) {
console.error('Failed to fetch action logs:', err) console.error('Failed to fetch action logs:', err)
window.showBigError('fetch-action-logs', 'getting action logs', err, false) window.showBigError('fetch-action-logs', 'getting action logs', err, false)
@ -169,6 +173,7 @@ function resetState() {
currentPage.value = 1 currentPage.value = 1
searchText.value = '' searchText.value = ''
loading.value = true loading.value = true
syncDurationTicker()
} }
function clearSearch() { function clearSearch() {
@ -185,20 +190,78 @@ function formatTimestamp(timestamp) {
} }
} }
function getStatusClass(log) { function plural(n, singular, pluralForm) {
if (log.timedOut) return 'status-timeout' return n === 1 ? `1 ${singular}` : `${n} ${pluralForm}`
if (log.blocked) return 'status-blocked'
if (log.exitCode !== 0) return 'status-error'
return 'status-success'
} }
function getStatusText(log) { function formatDurationSimple(ms) {
if (log.timedOut) return 'Timed out' if (!Number.isFinite(ms) || ms < 0) {
if (log.blocked) return 'Blocked' return '—'
if (log.exitCode !== 0) return `Exit code ${log.exitCode}` }
return 'Completed' const totalSec = Math.round(ms / 1000)
if (totalSec === 0) {
return '0 seconds'
}
const days = Math.floor(totalSec / 86400)
const hours = Math.floor((totalSec % 86400) / 3600)
const minutes = Math.floor((totalSec % 3600) / 60)
const seconds = totalSec % 60
const parts = []
if (days > 0) parts.push(plural(days, 'day', 'days'))
if (hours > 0) parts.push(plural(hours, 'hour', 'hours'))
if (minutes > 0) parts.push(plural(minutes, 'minute', 'minutes'))
if (seconds > 0) parts.push(plural(seconds, 'second', 'seconds'))
return parts.join(' ')
} }
function formatExecutionDuration(log) {
// Reading durationClock keeps this column reactive while executions are in progress.
const clock = durationClock.value
if (!log?.datetimeStarted) {
return '—'
}
const started = new Date(log.datetimeStarted)
if (Number.isNaN(started.getTime())) {
return '—'
}
let endMs
if (log.executionFinished) {
const finished = new Date(log.datetimeFinished)
if (Number.isNaN(finished.getTime())) {
return '—'
}
endMs = finished.getTime()
} else {
endMs = clock
}
return formatDurationSimple(endMs - started.getTime())
}
function syncDurationTicker() {
if (durationTicker != null) {
clearInterval(durationTicker)
durationTicker = null
}
const hasRunning = logs.value.some(l => !l.executionFinished)
if (!hasRunning) {
return
}
durationTicker = window.setInterval(() => {
durationClock.value = Date.now()
}, 1000)
}
onUnmounted(() => {
if (durationTicker != null) {
clearInterval(durationTicker)
durationTicker = null
}
})
function handlePageChange(page) { function handlePageChange(page) {
currentPage.value = page currentPage.value = page
fetchActionLogs() fetchActionLogs()
@ -247,16 +310,6 @@ watch(
</script> </script>
<style scoped> <style scoped>
.action-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.action-header h2 {
margin: 0;
}
.icon { .icon {
font-size: 1.5rem; font-size: 1.5rem;
} }
@ -288,6 +341,12 @@ watch(
color: var(--text-secondary); color: var(--text-secondary);
} }
.duration {
font-size: 0.9rem;
color: var(--text-secondary);
white-space: nowrap;
}
.empty-state { .empty-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@ -367,22 +426,6 @@ watch(
font-size: 0.85rem; font-size: 0.85rem;
} }
.exit-code .status-success {
color: #28a745;
}
.exit-code .status-error {
color: #dc3545;
}
.exit-code .status-timeout {
color: #ffc107;
}
.exit-code .status-blocked {
color: #6c757d;
}
.padding { .padding {
padding: 1rem; padding: 1rem;
} }

View File

@ -422,6 +422,8 @@ async function handleSubmit(event) {
const response = await startAction(argvs) const response = await startAction(argvs)
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) { if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
router.push(`/logs/${response.executionTrackingId}`) router.push(`/logs/${response.executionTrackingId}`)
} else if (popupOnStart.value === 'history') {
router.push(`/action/${props.bindingId}`)
} else { } else {
router.back() router.back()
} }

View File

@ -1,51 +1,66 @@
<template> <template>
<Section class = "with-header-and-content" v-if="entityDefinitions.length === 0" title="Loading entity definitions..."> <Section v-if="!definitionsLoaded" title="Loading entity definitions..." />
<div class = "section-header"> <Section
<h2 class="loading-message"> v-else-if="totalInstances === 0"
Loading entity definitions... title="There are no entities to show yet."
</h2> >
</div> <p>
When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
</p>
</Section> </Section>
<template v-else> <template v-else>
<Section v-for="def in entityDefinitions" :key="def.title" :title="'Entity: ' + def.title "> <Section v-for="def in entityDefinitions" :key="def.title" :title="'Entity: ' + def.title ">
<div class = "section-content"> <p>{{ def.instances.length }} instances.</p>
<p>{{ def.instances.length }} instances.</p>
<ul> <ul>
<li v-for="inst in def.instances" :key="inst.uniqueKey"> <li v-for="inst in def.instances" :key="inst.uniqueKey">
<router-link :to="{ name: 'EntityDetails', params: { entityType: inst.type, entityKey: inst.uniqueKey } }"> <router-link :to="{ name: 'EntityDetails', params: { entityType: inst.type, entityKey: inst.uniqueKey } }">
{{ inst.title }} {{ inst.title }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
<h3>Used on Dashboards:</h3> <h3>Used on Dashboards:</h3>
<ul> <ul>
<li v-for="dash in filteredDashboards(def.usedOnDashboards)" :key="dash"> <li v-for="dash in filteredDashboards(def.usedOnDashboards)" :key="dash">
<template v-if="isEntityDirectory(dash)"> <template v-if="isEntityDirectory(dash)">
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span> {{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
</template> </template>
<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }"> <router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }">
{{ getDashboardTitle(dash) }} {{ getDashboardTitle(dash) }}
</router-link> </router-link>
<span v-else>{{ dash }}</span> <span v-else>{{ dash }}</span>
</li> </li>
</ul> </ul>
</div>
</Section> </Section>
</template> </template>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue' import Section from 'picocrank/vue/components/Section.vue'
const definitionsLoaded = ref(false)
const entityDefinitions = ref([]) const entityDefinitions = ref([])
async function fetchEntities() { const totalInstances = computed(() =>
const ret = await window.client.getEntities() entityDefinitions.value.reduce(
(sum, def) => sum + (def.instances?.length ?? 0),
0,
),
)
entityDefinitions.value = ret.entityDefinitions async function fetchEntities() {
try {
const ret = await window.client.getEntities()
entityDefinitions.value = ret.entityDefinitions ?? []
} catch (err) {
console.error('Failed to fetch entities:', err)
window.showBigError('fetch-entities', 'getting entities', err, false)
entityDefinitions.value = []
} finally {
definitionsLoaded.value = true
}
} }
function filteredDashboards(dashboards) { function filteredDashboards(dashboards) {

View File

@ -9,13 +9,13 @@
"version": "1.0.0", "version": "1.0.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"wait-on": "^9.0.6" "wait-on": "^9.0.10"
}, },
"devDependencies": { "devDependencies": {
"chai": "^6.2.2", "chai": "^6.2.2",
"eslint": "^10.3.0", "eslint": "^10.4.0",
"mocha": "^11.7.5", "mocha": "^11.7.5",
"selenium-webdriver": "^4.43.0" "selenium-webdriver": "^4.44.0"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -92,9 +92,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.5.5", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
"integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -783,16 +783,16 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "10.3.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
"integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.5", "@eslint/config-array": "^0.23.5",
"@eslint/config-helpers": "^0.5.5", "@eslint/config-helpers": "^0.6.0",
"@eslint/core": "^1.2.1", "@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.1", "@eslint/plugin-kit": "^0.7.1",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
@ -1866,9 +1866,9 @@
"dev": true "dev": true
}, },
"node_modules/selenium-webdriver": { "node_modules/selenium-webdriver": {
"version": "4.43.0", "version": "4.44.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.43.0.tgz", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.44.0.tgz",
"integrity": "sha512-dV4zBTT37or3Z3/8uD6rS8zvd4ZxPuG4EJVlqYIbZCGZCYttZm7xb9rlFLSk4rrsQHAeDYvudl7cquo0vWpHjg==", "integrity": "sha512-7RbYoKK0zET+KMVak11UDCtKvNulOU6gFZp8HI5GN9K8+BhqrliIJU/FP6QADrvRAXFMr3wHxfE3JHOcAxO3GQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1885,7 +1885,7 @@
"@bazel/runfiles": "^6.5.0", "@bazel/runfiles": "^6.5.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"tmp": "^0.2.5", "tmp": "^0.2.5",
"ws": "^8.20.0" "ws": "^8.20.1"
}, },
"engines": { "engines": {
"node": ">= 20.0.0" "node": ">= 20.0.0"
@ -2123,9 +2123,9 @@
"dev": true "dev": true
}, },
"node_modules/wait-on": { "node_modules/wait-on": {
"version": "9.0.6", "version": "9.0.10",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.6.tgz", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz",
"integrity": "sha512-KR+Te+NBg6DmPVil4anyIO72mpt/QDHjRo3nVFkwRgb26oweUp3DDW2szO3EeUY4cqafWy4rQuOOeEk4n+7Oeg==", "integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.16.0", "axios": "^1.16.0",
@ -2259,9 +2259,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.20.0", "version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@ -12,11 +12,11 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"devDependencies": { "devDependencies": {
"chai": "^6.2.2", "chai": "^6.2.2",
"eslint": "^10.3.0", "eslint": "^10.4.0",
"mocha": "^11.7.5", "mocha": "^11.7.5",
"selenium-webdriver": "^4.43.0" "selenium-webdriver": "^4.44.0"
}, },
"dependencies": { "dependencies": {
"wait-on": "^9.0.6" "wait-on": "^9.0.10"
} }
} }

View File

@ -46,7 +46,7 @@ require (
buf.build/go/bufprivateusage v0.1.0 // indirect buf.build/go/bufprivateusage v0.1.0 // indirect
buf.build/go/interrupt v1.1.0 // indirect buf.build/go/interrupt v1.1.0 // indirect
buf.build/go/protovalidate v1.2.0 // indirect buf.build/go/protovalidate v1.2.0 // indirect
buf.build/go/protoyaml v0.6.0 // indirect buf.build/go/protoyaml v0.7.0 // indirect
buf.build/go/spdx v0.2.0 // indirect buf.build/go/spdx v0.2.0 // indirect
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
cel.dev/expr v0.25.2 // indirect cel.dev/expr v0.25.2 // indirect
@ -68,7 +68,7 @@ require (
github.com/cristalhq/acmd v0.12.0 // indirect github.com/cristalhq/acmd v0.12.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.4.3+incompatible // indirect github.com/docker/cli v29.5.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.7 // indirect github.com/docker/docker-credential-helpers v0.9.7 // indirect
@ -88,9 +88,9 @@ require (
github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/flock v0.13.0 // indirect
github.com/google/cel-go v0.28.0 // indirect github.com/google/cel-go v0.28.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.21.5 // indirect github.com/google/go-containerregistry v0.21.6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdx/go-netrc v1.0.0 // indirect github.com/jdx/go-netrc v1.0.0 // indirect
@ -157,8 +157,8 @@ require (
golang.org/x/text v0.37.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/grpc v1.79.3 // indirect google.golang.org/grpc v1.79.3 // indirect
mvdan.cc/xurls/v2 v2.6.0 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect
pluginrpc.com/pluginrpc v0.5.0 // indirect pluginrpc.com/pluginrpc v0.5.0 // indirect

View File

@ -48,6 +48,8 @@ buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= 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/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38=
buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0=
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U= buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U=
@ -166,6 +168,8 @@ github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb
github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU=
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
@ -240,6 +244,8 @@ github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@ -252,6 +258,8 @@ github.com/google/go-containerregistry v0.21.3 h1:Xr+yt3VvwOOn/5nJzd7UoOhwPGiPkY
github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc= github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc=
github.com/google/go-containerregistry v0.21.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM= github.com/google/go-containerregistry v0.21.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM=
github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU= github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU=
github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM=
github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/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= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
@ -673,6 +681,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA=
google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8=
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= 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-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
@ -687,6 +697,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=

View File

@ -204,10 +204,24 @@ func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest,
} }
} }
func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) { func (api *oliveTinAPI) localUserLoginEarlyReject(req *connect.Request[apiv1.LocalUserLoginRequest]) *connect.Response[apiv1.LocalUserLoginResponse] {
if !api.cfg.AuthLocalUsers.Enabled { if !api.cfg.AuthLocalUsers.Enabled {
return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false}), nil return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false})
} }
if isLocalInteractiveLoginDisabledForUser(api.cfg, req.Msg.Username) {
log.WithFields(log.Fields{"username": req.Msg.Username}).Debug("LocalUserLogin: interactive login disabled (no password configured)")
return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false})
}
return nil
}
func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
if early := api.localUserLoginEarlyReject(req); early != nil {
return early, nil
}
match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
if err != nil { if err != nil {
if errors.Is(err, ErrArgon2Busy) { if errors.Is(err, ErrArgon2Busy) {
@ -640,12 +654,13 @@ func calculateReversedIndices(page pageInfo, filteredLen int) (int64, int64) {
return startIdx, endIdx return startIdx, endIdx
} }
// buildActionLogsResponse builds the response with paginated log entries. // buildActionLogsResponse builds the response with paginated log entries (newest first).
func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *authpublic.AuthenticatedUser) *apiv1.GetActionLogsResponse { func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *authpublic.AuthenticatedUser) *apiv1.GetActionLogsResponse {
startIdx, endIdx := calculateReversedIndices(page, len(filtered)) startIdx, endIdx := calculateReversedIndices(page, len(filtered))
ret := &apiv1.GetActionLogsResponse{} ret := &apiv1.GetActionLogsResponse{}
for _, le := range filtered[startIdx:endIdx] { chunk := filtered[int(startIdx):int(endIdx)]
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user)) for i := len(chunk) - 1; i >= 0; i-- {
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(chunk[i], user))
} }
ret.CountRemaining = page.start ret.CountRemaining = page.start
ret.PageSize = page.size ret.PageSize = page.size
@ -723,12 +738,46 @@ func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumen
return arg == nil return arg == nil
} }
func (api *oliveTinAPI) validateArgumentTypeBindingAccess(user *authpublic.AuthenticatedUser, msg *apiv1.ValidateArgumentTypeRequest) error {
if msg == nil || msg.BindingId == "" {
return nil
}
return api.errUnlessUserMayValidateArgumentTypeForBinding(user, msg.BindingId)
}
func (api *oliveTinAPI) errUnlessUserMayValidateArgumentTypeForBinding(user *authpublic.AuthenticatedUser, bindingID string) error {
binding := api.executor.FindBindingByID(bindingID)
if binding == nil || binding.Action == nil {
return connect.NewError(connect.CodeNotFound, fmt.Errorf("action or argument not found for binding ID %s", bindingID))
}
if !api.userCanViewAction(user, binding.Action) {
return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
}
return nil
}
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) { func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
user := auth.UserFromApiCall(ctx, req, api.cfg)
if err := api.checkDashboardAccess(user); err != nil {
return nil, err
}
if err := api.validateArgumentTypeBindingAccess(user, req.Msg); err != nil {
return nil, err
}
if api.argumentNotFoundForValidation(req.Msg) { 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)) 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) return api.validateArgumentTypeConnectResponse(req.Msg)
}
func (api *oliveTinAPI) validateArgumentTypeConnectResponse(msg *apiv1.ValidateArgumentTypeRequest) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
err := api.validateArgumentTypeInternal(msg)
desc := "" desc := ""
if err != nil { if err != nil {
desc = err.Error() desc = err.Error()

View File

@ -408,6 +408,96 @@ func TestGetActionBindingDeniedWhenNoViewPermission(t *testing.T) {
"user with view:false must get permission denied from GetActionBinding") "user with view:false must get permission denied from GetActionBinding")
} }
// TestValidateArgumentTypeDeniesGuestsWhenLoginRequired (GHSA-f637-w7p2-m7fx) asserts that when
// guests must log in, ValidateArgumentType does not bypass dashboard access controls.
func TestValidateArgumentTypeDeniesGuestsWhenLoginRequired(t *testing.T) {
cfg := config.DefaultConfig()
cfg.AuthRequireGuestsToLogin = true
cfg.Actions = append(cfg.Actions, &config.Action{
ID: "a1",
Title: "Probe",
Shell: "echo",
Arguments: []config.ActionArgument{
{Name: "x", Type: "ascii"},
},
})
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
ts, client := getNewTestServerAndClient(cfg)
defer ts.Close()
_, err := client.ValidateArgumentType(context.Background(), connect.NewRequest(&apiv1.ValidateArgumentTypeRequest{
BindingId: "a1",
ArgumentName: "x",
Value: "v",
Type: "ascii",
}))
require.Error(t, err)
assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err),
"guest must not call ValidateArgumentType when AuthRequireGuestsToLogin is true")
}
// TestValidateArgumentTypeDeniedWithoutViewPermission (GHSA-f637-w7p2-m7fx) asserts ValidateArgumentType
// respects the same view ACL as GetActionBinding so the RPC cannot enumerate restricted actions.
func TestValidateArgumentTypeDeniedWithoutViewPermission(t *testing.T) {
cfg, _, _ := buildViewPermissionTestConfig(t)
cfg.AuthHttpHeaderUsername = "X-Ot-User"
for i := range cfg.Actions {
if cfg.Actions[i].ID == "secret_action" {
cfg.Actions[i].Arguments = []config.ActionArgument{{Name: "target", Type: "ascii"}}
break
}
}
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
ts, client := getNewTestServerAndClient(cfg)
defer ts.Close()
req := connect.NewRequest(&apiv1.ValidateArgumentTypeRequest{
BindingId: "secret_action",
ArgumentName: "target",
Value: "ok",
Type: "ascii",
})
req.Header().Set("X-Ot-User", "low")
_, err := client.ValidateArgumentType(context.Background(), req)
require.Error(t, err)
assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err),
"user with view:false must get permission denied from ValidateArgumentType")
}
// TestValidateArgumentTypeAllowedWithViewPermission (GHSA-f637-w7p2-m7fx) asserts authenticated users
// with view access can still use ValidateArgumentType for argument validation.
func TestValidateArgumentTypeAllowedWithViewPermission(t *testing.T) {
cfg, _, _ := buildViewPermissionTestConfig(t)
cfg.AuthHttpHeaderUsername = "X-Ot-User"
for i := range cfg.Actions {
if cfg.Actions[i].ID == "secret_action" {
cfg.Actions[i].Arguments = []config.ActionArgument{{Name: "target", Type: "ascii"}}
break
}
}
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
ts, client := getNewTestServerAndClient(cfg)
defer ts.Close()
req := connect.NewRequest(&apiv1.ValidateArgumentTypeRequest{
BindingId: "secret_action",
ArgumentName: "target",
Value: "ok",
Type: "ascii",
})
req.Header().Set("X-Ot-User", "admin")
resp, err := client.ValidateArgumentType(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Msg)
assert.True(t, resp.Msg.Valid, "admin with view:true should get successful validation for a valid ascii value")
}
// TestViewPermissionAllowedSeesAction (GHSA: view permission) asserts that a user with view: true // TestViewPermissionAllowedSeesAction (GHSA: view permission) asserts that a user with view: true
// still sees the action in the dashboard and can fetch it via GetActionBinding. // still sees the action in the dashboard and can fetch it via GetActionBinding.
func TestViewPermissionAllowedSeesAction(t *testing.T) { func TestViewPermissionAllowedSeesAction(t *testing.T) {

View File

@ -61,6 +61,15 @@ func comparePasswordAndHash(password, hash string) (bool, error) {
return match, nil return match, nil
} }
func isLocalInteractiveLoginDisabledForUser(cfg *config.Config, username string) bool {
user := cfg.FindUserByUsername(username)
if user == nil {
return false
}
return user.Password == ""
}
func checkUserPassword(cfg *config.Config, username, password string) (bool, error) { func checkUserPassword(cfg *config.Config, username, password string) (bool, error) {
user := cfg.FindUserByUsername(username) user := cfg.FindUserByUsername(username)
if user == nil { if user == nil {

View File

@ -0,0 +1,35 @@
package api
import (
"context"
"testing"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
)
func TestLocalUserLoginRejectsUserWithNoPassword(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.AuthLocalUsers.Enabled = true
cfg.AuthLocalUsers.Users = []*config.LocalUser{{
Username: "onlykey",
ApiKey: "k",
Password: "",
}}
ts, client := getNewTestServerAndClient(cfg)
defer ts.Close()
resp, err := client.LocalUserLogin(context.Background(), connect.NewRequest(&apiv1.LocalUserLoginRequest{
Username: "onlykey",
Password: "anything",
}))
require.NoError(t, err)
assert.False(t, resp.Msg.GetSuccess())
}

View File

@ -14,6 +14,7 @@ import (
var authChain = []func(*types.AuthCheckingContext) *types.AuthenticatedUser{ var authChain = []func(*types.AuthCheckingContext) *types.AuthenticatedUser{
checkUserFromHeaders, checkUserFromHeaders,
checkUserFromLocalSession, checkUserFromLocalSession,
checkUserFromLocalBearerApiKey,
otjwt.CheckUserFromJwtHeader, otjwt.CheckUserFromJwtHeader,
otjwt.CheckUserFromJwtCookie, otjwt.CheckUserFromJwtCookie,
} }

View File

@ -0,0 +1,109 @@
package auth
import (
"crypto/subtle"
"strings"
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
"github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
)
const localBearerScheme = "Bearer"
func constantTimeEqualString(a, b string) bool {
if len(a) != len(b) {
return false
}
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
func bearerTokenFromAuthorizationHeader(authz string) (string, bool) {
idx := strings.IndexByte(authz, ' ')
if idx <= 0 {
return "", false
}
if !strings.EqualFold(authz[:idx], localBearerScheme) {
return "", false
}
token := strings.TrimSpace(authz[idx+1:])
if token == "" {
return "", false
}
return token, true
}
func localUserHasAPIKey(user *config.LocalUser) bool {
return user != nil && user.ApiKey != ""
}
func findLocalUserByAPIKey(cfg *config.Config, token string) *config.LocalUser {
for _, user := range cfg.AuthLocalUsers.Users {
if !localUserHasAPIKey(user) {
continue
}
if constantTimeEqualString(token, user.ApiKey) {
return user
}
}
return nil
}
func localBearerAuthorizationHasEmptyCredential(authz string) bool {
idx := strings.IndexByte(authz, ' ')
return idx > 0 &&
strings.EqualFold(authz[:idx], localBearerScheme) &&
strings.TrimSpace(authz[idx+1:]) == ""
}
func logLocalBearerAPIKeyParseFailure(authz string) {
if strings.TrimSpace(authz) == "" {
return
}
if localBearerAuthorizationHasEmptyCredential(authz) {
log.Debugf("Local bearer API key: rejected (empty credential after Bearer prefix)")
return
}
log.Tracef("Local bearer API key: skipped (Authorization is not a Bearer token)")
}
func checkUserFromLocalBearerApiKey(context *types.AuthCheckingContext) *types.AuthenticatedUser {
if !context.Config.AuthLocalUsers.Enabled {
log.Tracef("Local bearer API key: skipped (authLocalUsers disabled)")
return nil
}
authz := context.Request.Header.Get("Authorization")
token, ok := bearerTokenFromAuthorizationHeader(authz)
if !ok {
logLocalBearerAPIKeyParseFailure(authz)
return nil
}
log.Debugf("Local bearer API key: checking configured local user API keys")
user := findLocalUserByAPIKey(context.Config, token)
if user == nil {
log.Debugf("Local bearer API key: rejected (no matching local user)")
return nil
}
log.WithFields(log.Fields{
"username": user.Username,
"usergroup": user.Usergroup,
}).Debugf("Local bearer API key: authenticated")
return &types.AuthenticatedUser{
Username: user.Username,
UsergroupLine: user.Usergroup,
Provider: "local",
}
}

View File

@ -0,0 +1,106 @@
package auth
import (
"net/http/httptest"
"testing"
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckUserFromLocalBearerApiKey_Match_LowercaseBearerScheme(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.AuthLocalUsers.Enabled = true
cfg.AuthLocalUsers.Users = []*config.LocalUser{{
Username: "bot",
Usergroup: "bots",
ApiKey: "secret-api-key",
}}
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Authorization", "bearer secret-api-key")
ctx := &authpublic.AuthCheckingContext{Request: req, Config: cfg}
user := checkUserFromLocalBearerApiKey(ctx)
require.NotNil(t, user)
assert.Equal(t, "bot", user.Username)
assert.Equal(t, "bots", user.UsergroupLine)
assert.Equal(t, "local", user.Provider)
}
func TestCheckUserFromLocalBearerApiKey_Match(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.AuthLocalUsers.Enabled = true
cfg.AuthLocalUsers.Users = []*config.LocalUser{{
Username: "bot",
Usergroup: "bots",
ApiKey: "secret-api-key",
}}
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Authorization", "Bearer secret-api-key")
ctx := &authpublic.AuthCheckingContext{Request: req, Config: cfg}
user := checkUserFromLocalBearerApiKey(ctx)
require.NotNil(t, user)
assert.Equal(t, "bot", user.Username)
assert.Equal(t, "bots", user.UsergroupLine)
assert.Equal(t, "local", user.Provider)
}
func TestCheckUserFromLocalBearerApiKey_WrongKey(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.AuthLocalUsers.Enabled = true
cfg.AuthLocalUsers.Users = []*config.LocalUser{{
Username: "bot",
ApiKey: "secret-api-key",
}}
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Authorization", "Bearer wrong")
ctx := &authpublic.AuthCheckingContext{Request: req, Config: cfg}
assert.Nil(t, checkUserFromLocalBearerApiKey(ctx))
}
func TestCheckUserFromLocalBearerApiKey_DisabledLocalUsers(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.AuthLocalUsers.Enabled = false
cfg.AuthLocalUsers.Users = []*config.LocalUser{{
Username: "bot",
ApiKey: "secret-api-key",
}}
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Authorization", "Bearer secret-api-key")
ctx := &authpublic.AuthCheckingContext{Request: req, Config: cfg}
assert.Nil(t, checkUserFromLocalBearerApiKey(ctx))
}
func TestCheckUserFromLocalBearerApiKey_NoBearerPrefix(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.AuthLocalUsers.Enabled = true
cfg.AuthLocalUsers.Users = []*config.LocalUser{{
Username: "bot",
ApiKey: "secret-api-key",
}}
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Authorization", "secret-api-key")
ctx := &authpublic.AuthCheckingContext{Request: req, Config: cfg}
assert.Nil(t, checkUserFromLocalBearerApiKey(ctx))
}

View File

@ -195,6 +195,7 @@ type LocalUser struct {
Username string `koanf:"username"` Username string `koanf:"username"`
Usergroup string `koanf:"usergroup"` Usergroup string `koanf:"usergroup"`
Password string `koanf:"password"` Password string `koanf:"password"`
ApiKey string `koanf:"apiKey"`
} }
type OAuth2Provider struct { type OAuth2Provider struct {

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"fmt"
"strings" "strings"
"text/template" "text/template"
@ -15,7 +16,7 @@ func (cfg *Config) Sanitize() {
cfg.sanitizeLogLevel() cfg.sanitizeLogLevel()
cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeAuthRequireGuestsToLogin()
cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLogHistoryPageSize()
cfg.sanitizeLocalUserPasswords() cfg.sanitizeLocalUsers()
cfg.sanitizeSecurityHeaders() cfg.sanitizeSecurityHeaders()
// log.Infof("cfg %p", cfg) // log.Infof("cfg %p", cfg)
@ -177,12 +178,55 @@ func (cfg *Config) sanitizeLogHistoryPageSize() {
} }
} }
func (cfg *Config) sanitizeLocalUserPasswords() { func (cfg *Config) sanitizeLocalUsers() {
for _, user := range cfg.AuthLocalUsers.Users { for _, user := range cfg.AuthLocalUsers.Users {
if user.Password != "" { expandLocalUserEnvTemplates(user)
user.Password = parsePasswordTemplate(user.Password) }
if err := validateUniqueLocalUserAPIKeys(cfg.AuthLocalUsers.Users); err != nil {
log.Fatalf("%v", err)
}
}
func expandLocalUserEnvTemplates(user *LocalUser) {
if user == nil {
return
}
if user.Password != "" {
user.Password = expandEnvTemplate(user.Password)
}
if user.ApiKey != "" {
user.ApiKey = expandEnvTemplate(user.ApiKey)
}
}
// validateUniqueLocalUserAPIKeys returns an error when two local users share the same non-empty apiKey.
func validateUniqueLocalUserAPIKeys(users []*LocalUser) error {
seen := make(map[string]string)
for _, user := range users {
if err := recordUniqueLocalUserAPIKey(seen, user); err != nil {
return err
} }
} }
return nil
}
func recordUniqueLocalUserAPIKey(seen map[string]string, user *LocalUser) error {
if user == nil || user.ApiKey == "" {
return nil
}
if prior, ok := seen[user.ApiKey]; ok {
return fmt.Errorf("duplicate authLocalUsers apiKey for users %q and %q", prior, user.Username)
}
seen[user.ApiKey] = user.Username
return nil
} }
func (cfg *Config) sanitizeSecurityHeaders() { func (cfg *Config) sanitizeSecurityHeaders() {
@ -204,16 +248,16 @@ func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() {
cfg.Security.XFrameOptions = "DENY" cfg.Security.XFrameOptions = "DENY"
} }
// parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment. // expandEnvTemplate expands {{ .Env.VAR }} in config strings using the process environment.
func parsePasswordTemplate(source string) string { func expandEnvTemplate(source string) string {
t, err := template.New("password").Option("missingkey=error").Parse(source) t, err := template.New("envTemplate").Option("missingkey=error").Parse(source)
if err != nil { if err != nil {
log.WithFields(log.Fields{"error": err}).Debug("Password template parse failed, using literal") log.WithFields(log.Fields{"error": err}).Debug("Env template parse failed, using literal")
return source return source
} }
var b strings.Builder var b strings.Builder
if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil { if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil {
log.WithFields(log.Fields{"error": err}).Debug("Password template execute failed, using literal") log.WithFields(log.Fields{"error": err}).Debug("Env template execute failed, using literal")
return source return source
} }
return b.String() return b.String()
@ -242,6 +286,8 @@ func sanitizePopupOnStart(raw string, cfg *Config) string {
return raw return raw
case "execution-button": case "execution-button":
return raw return raw
case "history":
return raw
default: default:
return cfg.DefaultPopupOnStart return cfg.DefaultPopupOnStart
} }

View File

@ -1,8 +1,10 @@
package config package config
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSanitizeConfig(t *testing.T) { func TestSanitizeConfig(t *testing.T) {
@ -37,6 +39,23 @@ func TestSanitizeConfig(t *testing.T) {
assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name") assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name")
} }
func TestSanitizePopupOnStartHistory(t *testing.T) {
c := DefaultConfig()
c.DefaultPopupOnStart = "nothing"
c.Actions = append(c.Actions, &Action{
Title: "With history",
PopupOnStart: "history",
Shell: "true",
})
c.Sanitize()
a := c.findAction("With history")
if assert.NotNil(t, a) {
assert.Equal(t, "history", a.PopupOnStart, "history must be preserved, not replaced by defaultPopupOnStart")
}
}
func TestSanitizeConfigInlineDashboardActions(t *testing.T) { func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
c := DefaultConfig() c := DefaultConfig()
@ -72,3 +91,19 @@ func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
assert.NotEmpty(t, found.ID, "Inline action should have a generated ID") assert.NotEmpty(t, found.ID, "Inline action should have a generated ID")
} }
} }
func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
t.Parallel()
err := validateUniqueLocalUserAPIKeys([]*LocalUser{
{Username: "a", ApiKey: "same"},
{Username: "b", ApiKey: "same"},
})
require.Error(t, err)
err = validateUniqueLocalUserAPIKeys([]*LocalUser{
{Username: "a", ApiKey: "one"},
{Username: "b", ApiKey: "two"},
})
require.NoError(t, err)
}

View File

@ -13,6 +13,7 @@ import (
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"path" "path"
"strings"
"github.com/OliveTin/OliveTin/internal/api" "github.com/OliveTin/OliveTin/internal/api"
"github.com/OliveTin/OliveTin/internal/auth" "github.com/OliveTin/OliveTin/internal/auth"
@ -57,13 +58,35 @@ func securityHeadersMiddleware(cfg *config.Config, next http.Handler) http.Handl
}) })
} }
func isSensitiveLogHeaderName(name string) bool {
switch strings.ToLower(name) {
case "authorization", "cookie", "x-forwarded-access-token":
return true
default:
return false
}
}
func redactHeaderValuesForLog(name string, values []string) []string {
if !isSensitiveLogHeaderName(name) {
return values
}
out := make([]string, len(values))
for i := range values {
out[i] = "[redacted]"
}
return out
}
func logDebugRequest(cfg *config.Config, source string, r *http.Request) { func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
if cfg.LogDebugOptions.SingleFrontendRequests { if cfg.LogDebugOptions.SingleFrontendRequests {
log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL) log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL)
if cfg.LogDebugOptions.SingleFrontendRequestHeaders { if cfg.LogDebugOptions.SingleFrontendRequestHeaders {
for name, values := range r.Header { for name, values := range r.Header {
log.Debugf("SingleFrontend HTTP Req Hdr: %v = %v", name, values) log.Debugf("SingleFrontend HTTP Req Hdr: %v = %v", name, redactHeaderValuesForLog(name, values))
} }
} }
} }

View File

@ -0,0 +1,17 @@
package httpservers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRedactHeaderValuesForLog(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{"[redacted]"}, redactHeaderValuesForLog("Authorization", []string{"Bearer secret"}))
assert.Equal(t, []string{"[redacted]", "[redacted]"}, redactHeaderValuesForLog("Cookie", []string{"a=1", "b=2"}))
assert.Equal(t, []string{"[redacted]"}, redactHeaderValuesForLog("authorization", []string{"x"}))
assert.Equal(t, []string{"[redacted]"}, redactHeaderValuesForLog("X-Forwarded-Access-Token", []string{"jwt"}))
assert.Equal(t, []string{"https"}, redactHeaderValuesForLog("X-Forwarded-Proto", []string{"https"}))
}

View File

@ -24,6 +24,8 @@ func jsonFunc(v any) (string, error) {
return string(data), nil return string(data), nil
} }
// Root template (funcs/options). parseTemplate clones before Parse — text/template
// must not receive concurrent Parse calls on the same instance.
var tpl = template.New("tpl"). var tpl = template.New("tpl").
Option("missingkey=error"). Option("missingkey=error").
Funcs(template.FuncMap{"Json": jsonFunc}) Funcs(template.FuncMap{"Json": jsonFunc})
@ -179,7 +181,12 @@ func checkMissingArgumentError(err error) (bool, string) {
} }
func parseTemplate(source string, data any) (string, error) { func parseTemplate(source string, data any) (string, error) {
t, err := tpl.Parse(source) clone, err := tpl.Clone()
if err != nil {
return "", err
}
t, err := clone.Parse(source)
if err != nil { if err != nil {
return "", err return "", err