Merge branch 'next' into feat-default-icon-cli-hugeicon
This commit is contained in:
commit
ae928625a6
|
|
@ -10,6 +10,8 @@ updates:
|
|||
labels:
|
||||
- "3k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# npm updates for frontend - targeting "release/2k" branch (security updates only)
|
||||
- package-ecosystem: "npm"
|
||||
|
|
@ -21,6 +23,8 @@ updates:
|
|||
labels:
|
||||
- "2k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# npm updates for integration-tests - targeting "next" branch
|
||||
- package-ecosystem: "npm"
|
||||
|
|
@ -32,6 +36,8 @@ updates:
|
|||
labels:
|
||||
- "3k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# npm updates for integration-tests - targeting "release/2k" branch (security updates only)
|
||||
- package-ecosystem: "npm"
|
||||
|
|
@ -43,6 +49,8 @@ updates:
|
|||
labels:
|
||||
- "2k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Go modules updates for service - targeting "next" branch
|
||||
- package-ecosystem: "gomod"
|
||||
|
|
@ -54,6 +62,8 @@ updates:
|
|||
labels:
|
||||
- "3k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Go modules updates for service - targeting "release/2k" branch (security updates only)
|
||||
- package-ecosystem: "gomod"
|
||||
|
|
@ -65,6 +75,8 @@ updates:
|
|||
labels:
|
||||
- "2k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Go modules updates for lang - targeting "next" branch
|
||||
- package-ecosystem: "gomod"
|
||||
|
|
@ -76,6 +88,8 @@ updates:
|
|||
labels:
|
||||
- "3k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Go modules updates for lang - targeting "release/2k" branch (security updates only)
|
||||
- package-ecosystem: "gomod"
|
||||
|
|
@ -87,6 +101,8 @@ updates:
|
|||
labels:
|
||||
- "2k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Docker updates - targeting "next" branch
|
||||
- package-ecosystem: "docker"
|
||||
|
|
@ -98,6 +114,8 @@ updates:
|
|||
labels:
|
||||
- "3k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Docker updates - targeting "release/2k" branch (security updates only)
|
||||
- package-ecosystem: "docker"
|
||||
|
|
@ -109,4 +127,6 @@ updates:
|
|||
labels:
|
||||
- "2k"
|
||||
- "dependencies"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ name: "Build & Release pipeline"
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/build-and-release.yml'
|
||||
- '.goreleaser.yml'
|
||||
- 'Dockerfile.multiarches'
|
||||
- 'Dockerfile.singlearch'
|
||||
- 'Makefile'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
|
|
@ -11,31 +21,46 @@ on:
|
|||
- main
|
||||
- next
|
||||
- beta
|
||||
paths:
|
||||
- '.github/workflows/build-and-release.yml'
|
||||
- '.goreleaser.yml'
|
||||
- 'Dockerfile.multiarches'
|
||||
- 'Dockerfile.singlearch'
|
||||
- 'Makefile'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: arm64,arm
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup node (npm cache)
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
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
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
|
|
@ -45,7 +70,7 @@ jobs:
|
|||
run: go version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_KEY }}
|
||||
|
|
@ -74,7 +99,7 @@ jobs:
|
|||
run: cd integration-tests && make -w
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
|
||||
|
|
|
|||
|
|
@ -15,13 +15,19 @@ name: "CodeQL"
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'cmd/**'
|
||||
- 'internal/**'
|
||||
- 'webui.dev/**'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'OliveTin.proto'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
branches: [main]
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '25 10 * * 5'
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ name: "Codestyle checks"
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'cmd/**'
|
||||
- 'internal/**'
|
||||
- 'webui.dev/**'
|
||||
- '.github/workflows/codestyle.yml'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'OliveTin.proto'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
|
|
@ -7,8 +7,28 @@ name: DevSkim
|
|||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/devskim.yml'
|
||||
- '.goreleaser.yml'
|
||||
- 'Dockerfile.multiarches'
|
||||
- 'Dockerfile.singlearch'
|
||||
- 'Makefile'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/devskim.yml'
|
||||
- '.goreleaser.yml'
|
||||
- 'Dockerfile.multiarches'
|
||||
- 'Dockerfile.singlearch'
|
||||
- 'Makefile'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '34 21 * * 2'
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@
|
|||
** xref:dashboards/2-fieldsets.adoc[Fieldsets]
|
||||
** xref:dashboards/3-folders.adoc[Folders]
|
||||
** xref:dashboards/4-displays.adoc[Displays]
|
||||
** xref:dashboards/faq-display-hyperlinks.adoc[Hyperlinks (Displays)]
|
||||
** xref:dashboards/5-output-views.adoc[Output Views]
|
||||
** xref:dashboards/entity-directories.adoc[Entity Directories]
|
||||
* xref:entities/intro.adoc[Entities]
|
||||
|
|
@ -95,6 +96,7 @@
|
|||
* xref:security/concepts.adoc[Security]
|
||||
** xref:security/acl.adoc[Access Control Lists]
|
||||
** xref:security/local.adoc[Local Users Authorization]
|
||||
** xref:security/api_keys.adoc[API Keys]
|
||||
** xref:security/trusted_header.adoc[Trusted Header Authorization]
|
||||
** xref:security/jwt.adoc[JWT Authorization]
|
||||
*** xref:security/jwt_keys.adoc[JWT with Keys]
|
||||
|
|
|
|||
|
|
@ -68,4 +68,16 @@ actions:
|
|||
|
||||
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
|
||||
----
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
* **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
|
||||
* **Run in background** — the action runs without opening a dialog
|
||||
|
||||
|
|
|
|||
|
|
@ -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]).
|
||||
|
|
@ -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/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/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:entities/intro.adoc[Use entities with dashboards] - Dynamically generate actions from entity files
|
||||
* xref:dashboards/examples.adoc[View dashboard examples] - See complete dashboard configurations
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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.
|
||||
|
||||
For programmatic access (scripts, integrations) using per-user bearer API keys, see xref:security/api_keys.adoc[API Keys].
|
||||
|
||||
== Define a user
|
||||
|
||||
include::partial$config-start.adoc[]
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
"dependencies": {
|
||||
"@connectrpc/connect": "^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",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
|
|
@ -21,14 +21,14 @@
|
|||
"picocrank": "^1.15.0",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.12",
|
||||
"vite": "^8.0.13",
|
||||
"vue": "^3.5.34",
|
||||
"vue-i18n": "^11.4.2",
|
||||
"vue-router": "^5.0.6"
|
||||
"vue-i18n": "^11.4.4",
|
||||
"vue-router": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^17.11.0",
|
||||
"stylelint": "^17.11.1",
|
||||
"stylelint-config-standard": "^40.0.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -48,19 +48,66 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
|
||||
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
|
||||
"version": "8.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/parser": "^8.0.0-rc.5",
|
||||
"@babel/types": "^8.0.0-rc.5",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"@types/jsesc": "^2.5.0",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"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": {
|
||||
|
|
@ -891,9 +938,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@hugeicons/core-free-icons": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.1.3.tgz",
|
||||
"integrity": "sha512-FWPrKnlYKpSaitUtlZhFlDQXDgHiayTPFJYWvyIKkW2RI6Vj5KBvjxI+lAnnFPu07SwgIMiDDj+Gttl0t+o/oQ==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.1.4.tgz",
|
||||
"integrity": "sha512-vkMvlnW7Atqh7juhZCadYDvqABWcEeVAkavAK6HJBFbYkLCme5CN51Oc91D5j5ruDja+HQPjMfWkTxxulcwvAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hugeicons/vue": {
|
||||
|
|
@ -947,61 +994,61 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.2.tgz",
|
||||
"integrity": "sha512-7fpuCcVmeLv2T9qHsARqGvh8xt+sV2fH+Q+gMHFwB/rPXzo85DpbJFKn7dBH1L5p0c2cSh2DW+2h/64EKrISmA==",
|
||||
"version": "11.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.4.tgz",
|
||||
"integrity": "sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/devtools-types": "11.4.2",
|
||||
"@intlify/message-compiler": "11.4.2",
|
||||
"@intlify/shared": "11.4.2"
|
||||
"@intlify/devtools-types": "11.4.4",
|
||||
"@intlify/message-compiler": "11.4.4",
|
||||
"@intlify/shared": "11.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-types": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.2.tgz",
|
||||
"integrity": "sha512-3u8EN1kB6EMSi96KXs5k7a8y2X2g4+h3X6iwVZU47cP4n+mTuq//WMjG588BzSp/2XQ/dTXo2BLUXX+XS+PNfA==",
|
||||
"version": "11.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.4.tgz",
|
||||
"integrity": "sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.4.2",
|
||||
"@intlify/shared": "11.4.2"
|
||||
"@intlify/core-base": "11.4.4",
|
||||
"@intlify/shared": "11.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.2.tgz",
|
||||
"integrity": "sha512-a6CDSGSMTGrg0BjD97x8TBYPf7qQMDlZipJ6UDfv/pd4OIym8TMlHu3MsH0bTNnRdAG2D6EFEykIgiQPqvtTkA==",
|
||||
"version": "11.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.4.tgz",
|
||||
"integrity": "sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.4.2",
|
||||
"@intlify/shared": "11.4.4",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.2.tgz",
|
||||
"integrity": "sha512-NzpHbguRCsOHDwxmlBa9qu/imc+/QWgsYUaK6FZeNC0wK8QfAbhqrktEp/haVzxU1aikH8IX4ytD+mfFEMi/9A==",
|
||||
"version": "11.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.4.tgz",
|
||||
"integrity": "sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
|
|
@ -1110,18 +1157,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.129.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
|
||||
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
|
||||
"version": "0.130.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
|
||||
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
|
||||
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1135,9 +1182,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
|
||||
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1151,9 +1198,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
|
||||
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1167,9 +1214,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
|
||||
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1183,9 +1230,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
|
||||
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
|
||||
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -1199,9 +1246,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1215,9 +1262,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
|
||||
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
|
||||
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1231,9 +1278,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -1247,9 +1294,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -1263,9 +1310,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1279,9 +1326,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
|
||||
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
|
||||
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1295,9 +1342,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
|
||||
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1311,9 +1358,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
|
||||
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
|
||||
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
|
|
@ -1329,9 +1376,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
|
||||
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
|
||||
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1345,9 +1392,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
|
||||
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
|
||||
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1361,9 +1408,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.13",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
|
||||
"integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
|
|
@ -1395,6 +1442,12 @@
|
|||
"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": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
|
|
@ -1408,12 +1461,12 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
|
||||
"integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==",
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
||||
"integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-rc.13"
|
||||
"@rolldown/pluginutils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
|
|
@ -1507,28 +1560,22 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/devtools-kit": {
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz",
|
||||
"integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==",
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz",
|
||||
"integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^8.0.6",
|
||||
"@vue/devtools-shared": "^8.1.2",
|
||||
"birpc": "^2.6.1",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^2.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
"perfect-debounce": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-shared": {
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz",
|
||||
"integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz",
|
||||
"integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.34",
|
||||
|
|
@ -2086,21 +2133,6 @@
|
|||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"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": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
|
||||
|
|
@ -4036,15 +4068,6 @@
|
|||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
|
|
@ -4181,18 +4204,6 @@
|
|||
"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": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
|
|
@ -4798,12 +4809,6 @@
|
|||
"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": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
||||
|
|
@ -5577,12 +5582,6 @@
|
|||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
|
|
@ -5600,13 +5599,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
|
||||
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
||||
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.129.0",
|
||||
"@rolldown/pluginutils": "1.0.0"
|
||||
"@oxc-project/types": "=0.130.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
|
|
@ -5615,29 +5614,23 @@
|
|||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0"
|
||||
"@rolldown/binding-android-arm64": "1.0.1",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.1",
|
||||
"@rolldown/binding-darwin-x64": "1.0.1",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.1",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.1",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.1",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.1",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.1",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
|
||||
"@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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
|
@ -5918,15 +5911,6 @@
|
|||
"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": {
|
||||
"version": "17.1.2",
|
||||
"resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz",
|
||||
|
|
@ -6146,9 +6130,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/stylelint": {
|
||||
"version": "17.11.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.0.tgz",
|
||||
"integrity": "sha512-/3czzmbF9XdGWvReDF3Ex4R23Ajolo7j8RB2bFNEqk6Ht356nlpVV+G5bG2Qt8AW1ofJzXztBRDnAtd7cgowWA==",
|
||||
"version": "17.11.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.1.tgz",
|
||||
"integrity": "sha512-+smN/HqVTggUx3iuAzOi9fPh8SrH+cJWlZrYVldXoJ06orWBhZ4Ue/QEp64oei6pVrAh4w3tG+Y12Vw7MbCFRQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -6183,13 +6167,12 @@
|
|||
"html-tags": "^5.1.0",
|
||||
"ignore": "^7.0.5",
|
||||
"import-meta-resolve": "^4.2.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"mathml-tag-names": "^4.0.0",
|
||||
"meow": "^14.1.0",
|
||||
"micromatch": "^4.0.8",
|
||||
"normalize-path": "^3.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.5.13",
|
||||
"postcss": "^8.5.14",
|
||||
"postcss-safe-parser": "^7.0.1",
|
||||
"postcss-selector-parser": "^7.1.1",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
|
|
@ -6333,18 +6316,6 @@
|
|||
"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": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
@ -6798,15 +6769,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
|
||||
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
||||
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.14",
|
||||
"rolldown": "1.0.0",
|
||||
"rolldown": "1.0.1",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -6908,18 +6879,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.2.tgz",
|
||||
"integrity": "sha512-sADDeKXqAGsPX6tK3t3y2ZiMpbVWN12tG+MhTiJ06rVoh58eGtM4wFyw3uWGbVkXByVp9Ne/AP+nSSzI+J9OAQ==",
|
||||
"version": "11.4.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.4.tgz",
|
||||
"integrity": "sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.4.2",
|
||||
"@intlify/devtools-types": "11.4.2",
|
||||
"@intlify/shared": "11.4.2",
|
||||
"@intlify/core-base": "11.4.4",
|
||||
"@intlify/devtools-types": "11.4.4",
|
||||
"@intlify/shared": "11.4.4",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
|
|
@ -6929,14 +6900,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.6.tgz",
|
||||
"integrity": "sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.7.tgz",
|
||||
"integrity": "sha512-dqfk8kvRbCutmCOCj/XLDqDEYxc1wBdAOGLuVy5M93ifYMsBd5fIjfaPN4tQAbxr5IprdBDIox1gr4wYyOx/SA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.28.6",
|
||||
"@babel/generator": "^8.0.0-rc.4",
|
||||
"@vue-macros/common": "^3.1.1",
|
||||
"@vue/devtools-api": "^8.0.6",
|
||||
"@vue/devtools-api": "^8.1.1",
|
||||
"ast-walker-scope": "^0.8.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"json5": "^2.2.3",
|
||||
|
|
@ -6957,9 +6928,9 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"@pinia/colada": ">=0.21.2",
|
||||
"@vue/compiler-sfc": "^3.5.17",
|
||||
"@vue/compiler-sfc": "^3.5.34",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.0"
|
||||
"vue": "^3.5.34"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@pinia/colada": {
|
||||
|
|
@ -6974,12 +6945,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz",
|
||||
"integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==",
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz",
|
||||
"integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^8.0.6"
|
||||
"@vue/devtools-kit": "^8.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router/node_modules/json5": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"source": "index.html",
|
||||
"devDependencies": {
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^17.11.0",
|
||||
"stylelint": "^17.11.1",
|
||||
"stylelint-config-standard": "^40.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -24,9 +24,9 @@
|
|||
"dependencies": {
|
||||
"@connectrpc/connect": "^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",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
|
|
@ -34,9 +34,9 @@
|
|||
"picocrank": "^1.15.0",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.12",
|
||||
"vite": "^8.0.13",
|
||||
"vue": "^3.5.34",
|
||||
"vue-i18n": "^11.4.2",
|
||||
"vue-router": "^5.0.6"
|
||||
"vue-i18n": "^11.4.4",
|
||||
"vue-router": "^5.0.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
|
||||
<HugeiconsIcon :icon="TypeCursorIcon" />
|
||||
</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">
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
</div>
|
||||
|
|
@ -28,7 +31,7 @@ import { buttonResults } from './stores/buttonResults'
|
|||
import { rateLimits } from './stores/rateLimits'
|
||||
import { useRouter } from 'vue-router'
|
||||
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'
|
||||
|
||||
|
|
@ -101,6 +104,8 @@ function constructFromJson(json) {
|
|||
|
||||
if (popupOnStart.value.includes('execution-dialog')) {
|
||||
navigateOnStart.value = 'pop'
|
||||
} else if (popupOnStart.value === 'history') {
|
||||
navigateOnStart.value = 'hist'
|
||||
} else if (props.actionData.arguments.length > 0) {
|
||||
navigateOnStart.value = 'arg'
|
||||
}
|
||||
|
|
@ -233,6 +238,8 @@ function onLogEntryChanged(logEntry) {
|
|||
function onExecutionStarted(logEntry) {
|
||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||
router.push(`/logs/${logEntry.executionTrackingId}`)
|
||||
} else if (popupOnStart.value === 'history') {
|
||||
router.push(`/action/${bindingId.value}`)
|
||||
}
|
||||
|
||||
isDisabled.value = true
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<dt>Timeout</dt>
|
||||
<dd>{{ action.timeout }} seconds</dd>
|
||||
</dl>
|
||||
<p v-if="action" class = "fg1">
|
||||
<p class = "fg1">
|
||||
Execution history for this action. You can filter by execution tracking ID.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Duration</th>
|
||||
<th>Execution ID</th>
|
||||
<th>Metadata</th>
|
||||
<th>Status</th>
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
<tbody>
|
||||
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||
<td class="duration">{{ formatExecutionDuration(log) }}</td>
|
||||
<td>
|
||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||
{{ log.executionTrackingId }}
|
||||
|
|
@ -70,9 +72,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<span :class="getStatusClass(log) + ' annotation'">
|
||||
{{ getStatusText(log) }}
|
||||
</span>
|
||||
<ActionStatusDisplay :logEntry="log" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -90,11 +90,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
|
@ -106,6 +107,8 @@ const pageSize = ref(10)
|
|||
const currentPage = ref(1)
|
||||
const loading = ref(false)
|
||||
const totalCount = ref(0)
|
||||
const durationClock = ref(Date.now())
|
||||
let durationTicker = null
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
if (!searchText.value) {
|
||||
|
|
@ -138,6 +141,7 @@ async function fetchActionLogs() {
|
|||
pageSize.value = serverPageSize
|
||||
}
|
||||
totalCount.value = Number(response.totalCount) || 0
|
||||
syncDurationTicker()
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch action logs:', err)
|
||||
window.showBigError('fetch-action-logs', 'getting action logs', err, false)
|
||||
|
|
@ -169,6 +173,7 @@ function resetState() {
|
|||
currentPage.value = 1
|
||||
searchText.value = ''
|
||||
loading.value = true
|
||||
syncDurationTicker()
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
|
|
@ -185,20 +190,78 @@ function formatTimestamp(timestamp) {
|
|||
}
|
||||
}
|
||||
|
||||
function getStatusClass(log) {
|
||||
if (log.timedOut) return 'status-timeout'
|
||||
if (log.blocked) return 'status-blocked'
|
||||
if (log.exitCode !== 0) return 'status-error'
|
||||
return 'status-success'
|
||||
function plural(n, singular, pluralForm) {
|
||||
return n === 1 ? `1 ${singular}` : `${n} ${pluralForm}`
|
||||
}
|
||||
|
||||
function getStatusText(log) {
|
||||
if (log.timedOut) return 'Timed out'
|
||||
if (log.blocked) return 'Blocked'
|
||||
if (log.exitCode !== 0) return `Exit code ${log.exitCode}`
|
||||
return 'Completed'
|
||||
function formatDurationSimple(ms) {
|
||||
if (!Number.isFinite(ms) || ms < 0) {
|
||||
return '—'
|
||||
}
|
||||
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) {
|
||||
currentPage.value = page
|
||||
fetchActionLogs()
|
||||
|
|
@ -247,16 +310,6 @@ watch(
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.action-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
|
@ -288,6 +341,12 @@ watch(
|
|||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
|
|
@ -367,22 +426,6 @@ watch(
|
|||
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: 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,6 +422,8 @@ async function handleSubmit(event) {
|
|||
const response = await startAction(argvs)
|
||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||
router.push(`/logs/${response.executionTrackingId}`)
|
||||
} else if (popupOnStart.value === 'history') {
|
||||
router.push(`/action/${props.bindingId}`)
|
||||
} else {
|
||||
router.back()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
<template>
|
||||
<Section class = "with-header-and-content" v-if="entityDefinitions.length === 0" title="Loading entity definitions...">
|
||||
<div class = "section-header">
|
||||
<h2 class="loading-message">
|
||||
Loading entity definitions...
|
||||
</h2>
|
||||
</div>
|
||||
<Section v-if="!definitionsLoaded" title="Loading entity definitions..." />
|
||||
<Section
|
||||
v-else-if="totalInstances === 0"
|
||||
title="There are no entities to show yet."
|
||||
>
|
||||
<p>
|
||||
When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
|
||||
</p>
|
||||
</Section>
|
||||
<template v-else>
|
||||
<Section v-for="def in entityDefinitions" :key="def.title" :title="'Entity: ' + def.title ">
|
||||
<div class = "section-content">
|
||||
<p>{{ def.instances.length }} instances.</p>
|
||||
|
||||
<ul>
|
||||
|
|
@ -31,21 +32,35 @@
|
|||
<span v-else>{{ dash }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const definitionsLoaded = ref(false)
|
||||
const entityDefinitions = ref([])
|
||||
|
||||
async function fetchEntities() {
|
||||
const ret = await window.client.getEntities()
|
||||
const totalInstances = computed(() =>
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@
|
|||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"wait-on": "^9.0.6"
|
||||
"wait-on": "^9.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^6.2.2",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint": "^10.4.0",
|
||||
"mocha": "^11.7.5",
|
||||
"selenium-webdriver": "^4.43.0"
|
||||
"selenium-webdriver": "^4.44.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
|
@ -92,9 +92,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
|
||||
"integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
|
||||
"integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
|
@ -783,16 +783,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz",
|
||||
"integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
|
||||
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@eslint/config-array": "^0.23.5",
|
||||
"@eslint/config-helpers": "^0.5.5",
|
||||
"@eslint/config-helpers": "^0.6.0",
|
||||
"@eslint/core": "^1.2.1",
|
||||
"@eslint/plugin-kit": "^0.7.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
|
|
@ -1866,9 +1866,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/selenium-webdriver": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.43.0.tgz",
|
||||
"integrity": "sha512-dV4zBTT37or3Z3/8uD6rS8zvd4ZxPuG4EJVlqYIbZCGZCYttZm7xb9rlFLSk4rrsQHAeDYvudl7cquo0vWpHjg==",
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.44.0.tgz",
|
||||
"integrity": "sha512-7RbYoKK0zET+KMVak11UDCtKvNulOU6gFZp8HI5GN9K8+BhqrliIJU/FP6QADrvRAXFMr3wHxfE3JHOcAxO3GQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -1885,7 +1885,7 @@
|
|||
"@bazel/runfiles": "^6.5.0",
|
||||
"jszip": "^3.10.1",
|
||||
"tmp": "^0.2.5",
|
||||
"ws": "^8.20.0"
|
||||
"ws": "^8.20.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.0.0"
|
||||
|
|
@ -2123,9 +2123,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.6.tgz",
|
||||
"integrity": "sha512-KR+Te+NBg6DmPVil4anyIO72mpt/QDHjRo3nVFkwRgb26oweUp3DDW2szO3EeUY4cqafWy4rQuOOeEk4n+7Oeg==",
|
||||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz",
|
||||
"integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.16.0",
|
||||
|
|
@ -2259,9 +2259,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"chai": "^6.2.2",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint": "^10.4.0",
|
||||
"mocha": "^11.7.5",
|
||||
"selenium-webdriver": "^4.43.0"
|
||||
"selenium-webdriver": "^4.44.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"wait-on": "^9.0.6"
|
||||
"wait-on": "^9.0.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ require (
|
|||
buf.build/go/bufprivateusage v0.1.0 // indirect
|
||||
buf.build/go/interrupt v1.1.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/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
|
|
@ -68,7 +68,7 @@ require (
|
|||
github.com/cristalhq/acmd v0.12.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // 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/docker v28.5.2+incompatible // 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-viper/mapstructure/v2 v2.5.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-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/inconshreveable/mousetrap v1.1.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/time v0.15.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/rpc 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-20260519071638-aa98bba5eb94 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
||||
pluginrpc.com/pluginrpc v0.5.0 // indirect
|
||||
|
|
|
|||
|
|
@ -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/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||
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/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
|
||||
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.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.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/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
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.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.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.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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.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.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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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-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-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/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
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-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-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/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrArgon2Busy) {
|
||||
|
|
@ -640,12 +654,13 @@ func calculateReversedIndices(page pageInfo, filteredLen int) (int64, int64) {
|
|||
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 {
|
||||
startIdx, endIdx := calculateReversedIndices(page, len(filtered))
|
||||
ret := &apiv1.GetActionLogsResponse{}
|
||||
for _, le := range filtered[startIdx:endIdx] {
|
||||
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
|
||||
chunk := filtered[int(startIdx):int(endIdx)]
|
||||
for i := len(chunk) - 1; i >= 0; i-- {
|
||||
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(chunk[i], user))
|
||||
}
|
||||
ret.CountRemaining = page.start
|
||||
ret.PageSize = page.size
|
||||
|
|
@ -723,12 +738,46 @@ func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumen
|
|||
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) {
|
||||
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) {
|
||||
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 := ""
|
||||
if err != nil {
|
||||
desc = err.Error()
|
||||
|
|
|
|||
|
|
@ -408,6 +408,96 @@ func TestGetActionBindingDeniedWhenNoViewPermission(t *testing.T) {
|
|||
"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
|
||||
// still sees the action in the dashboard and can fetch it via GetActionBinding.
|
||||
func TestViewPermissionAllowedSeesAction(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ func comparePasswordAndHash(password, hash string) (bool, error) {
|
|||
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) {
|
||||
user := cfg.FindUserByUsername(username)
|
||||
if user == nil {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import (
|
|||
var authChain = []func(*types.AuthCheckingContext) *types.AuthenticatedUser{
|
||||
checkUserFromHeaders,
|
||||
checkUserFromLocalSession,
|
||||
checkUserFromLocalBearerApiKey,
|
||||
otjwt.CheckUserFromJwtHeader,
|
||||
otjwt.CheckUserFromJwtCookie,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -195,6 +195,7 @@ type LocalUser struct {
|
|||
Username string `koanf:"username"`
|
||||
Usergroup string `koanf:"usergroup"`
|
||||
Password string `koanf:"password"`
|
||||
ApiKey string `koanf:"apiKey"`
|
||||
}
|
||||
|
||||
type OAuth2Provider struct {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ func (cfg *Config) Sanitize() {
|
|||
cfg.sanitizeLogLevel()
|
||||
cfg.sanitizeAuthRequireGuestsToLogin()
|
||||
cfg.sanitizeLogHistoryPageSize()
|
||||
cfg.sanitizeLocalUserPasswords()
|
||||
cfg.sanitizeLocalUsers()
|
||||
cfg.sanitizeSecurityHeaders()
|
||||
|
||||
// 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 {
|
||||
expandLocalUserEnvTemplates(user)
|
||||
}
|
||||
|
||||
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 = parsePasswordTemplate(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() {
|
||||
|
|
@ -204,16 +248,16 @@ func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() {
|
|||
cfg.Security.XFrameOptions = "DENY"
|
||||
}
|
||||
|
||||
// parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment.
|
||||
func parsePasswordTemplate(source string) string {
|
||||
t, err := template.New("password").Option("missingkey=error").Parse(source)
|
||||
// expandEnvTemplate expands {{ .Env.VAR }} in config strings using the process environment.
|
||||
func expandEnvTemplate(source string) string {
|
||||
t, err := template.New("envTemplate").Option("missingkey=error").Parse(source)
|
||||
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
|
||||
}
|
||||
var b strings.Builder
|
||||
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 b.String()
|
||||
|
|
@ -242,6 +286,8 @@ func sanitizePopupOnStart(raw string, cfg *Config) string {
|
|||
return raw
|
||||
case "execution-button":
|
||||
return raw
|
||||
case "history":
|
||||
return raw
|
||||
default:
|
||||
return cfg.DefaultPopupOnStart
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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) {
|
||||
c := DefaultConfig()
|
||||
|
||||
|
|
@ -72,3 +91,19 @@ func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/api"
|
||||
"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) {
|
||||
if cfg.LogDebugOptions.SingleFrontendRequests {
|
||||
log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL)
|
||||
|
||||
if cfg.LogDebugOptions.SingleFrontendRequestHeaders {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}))
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ func jsonFunc(v any) (string, error) {
|
|||
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").
|
||||
Option("missingkey=error").
|
||||
Funcs(template.FuncMap{"Json": jsonFunc})
|
||||
|
|
@ -179,7 +181,12 @@ func checkMissingArgumentError(err error) (bool, string) {
|
|||
}
|
||||
|
||||
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 {
|
||||
return "", err
|
||||
|
|
|
|||
Loading…
Reference in New Issue