@eeacms/volto-eea-chatbot 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/betterleaks.yml +133 -0
- package/.gitleaks.toml +89 -0
- package/CHANGELOG.md +10 -1
- package/README.md +59 -0
- package/RELEASE.md +1 -1
- package/package.json +1 -1
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +20 -11
- package/src/ChatBlock/services/messageProcessor.ts +32 -14
- package/src/ChatBlock/style.less +41 -51
- package/src/halloumi/filtering.js +2 -2
- package/src/halloumi/generative.test.js +1 -1
- package/src/halloumi/middleware.js +2 -2
- package/src/halloumi/middleware.test.js +5 -5
- package/src/middleware.js +2 -2
- package/src/middleware.test.js +11 -11
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
name: Betterleaks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
scan:
|
|
12
|
+
name: Scan for secrets
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
env:
|
|
15
|
+
SMTP_URL: ${{ secrets.SMTP_URL }}
|
|
16
|
+
SMTP_PORT: ${{ secrets.SMTP_PORT || '25' }}
|
|
17
|
+
SMTP_EMAIL: ${{ secrets.SMTP_EMAIL }}
|
|
18
|
+
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout repository
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 1
|
|
24
|
+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
25
|
+
|
|
26
|
+
- name: Run Betterleaks
|
|
27
|
+
id: betterleaks
|
|
28
|
+
continue-on-error: true
|
|
29
|
+
uses: dortort/betterleaks-action@v0.1.0
|
|
30
|
+
with:
|
|
31
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
scan-mode: dir
|
|
33
|
+
scan-path: .
|
|
34
|
+
config: .gitleaks.toml
|
|
35
|
+
report-format: json
|
|
36
|
+
report-path: betterleaks-report.json
|
|
37
|
+
redact: "true"
|
|
38
|
+
no-color: "true"
|
|
39
|
+
no-banner: "true"
|
|
40
|
+
fail-on-leak: "true"
|
|
41
|
+
|
|
42
|
+
- name: Upload Betterleaks report
|
|
43
|
+
if: always()
|
|
44
|
+
uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: betterleaks-report
|
|
47
|
+
path: betterleaks-report.json
|
|
48
|
+
if-no-files-found: ignore
|
|
49
|
+
|
|
50
|
+
- name: Build Betterleaks email summary
|
|
51
|
+
id: leak_summary
|
|
52
|
+
if: steps.betterleaks.outcome == 'failure'
|
|
53
|
+
shell: bash
|
|
54
|
+
run: |
|
|
55
|
+
if [[ -s betterleaks-report.json ]]; then
|
|
56
|
+
jq -r '
|
|
57
|
+
def one_line:
|
|
58
|
+
tostring
|
|
59
|
+
| gsub("[\r\n]+"; " ")
|
|
60
|
+
| if length > 240 then .[0:240] + "..." else . end;
|
|
61
|
+
|
|
62
|
+
.[:20][]
|
|
63
|
+
| "- " + (.RuleID // "unknown-rule")
|
|
64
|
+
+ " at " + (.File // "unknown-file")
|
|
65
|
+
+ ":" + ((.StartLine // 0) | tostring)
|
|
66
|
+
+ "\n match: " + ((.Match // .Secret // "REDACTED") | one_line)
|
|
67
|
+
' betterleaks-report.json > betterleaks-email-summary.txt
|
|
68
|
+
|
|
69
|
+
count="$(jq 'length' betterleaks-report.json)"
|
|
70
|
+
if (( count > 20 )); then
|
|
71
|
+
{
|
|
72
|
+
echo ""
|
|
73
|
+
echo "... and $((count - 20)) more finding(s). Download the artifact for full details."
|
|
74
|
+
} >> betterleaks-email-summary.txt
|
|
75
|
+
fi
|
|
76
|
+
else
|
|
77
|
+
echo "No JSON report was generated. Download the workflow logs for details." > betterleaks-email-summary.txt
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
echo "text<<BETTERLEAKS_SUMMARY"
|
|
82
|
+
cat betterleaks-email-summary.txt
|
|
83
|
+
echo "BETTERLEAKS_SUMMARY"
|
|
84
|
+
} >> "$GITHUB_OUTPUT"
|
|
85
|
+
|
|
86
|
+
- name: Resolve committer email
|
|
87
|
+
id: committer
|
|
88
|
+
if: steps.betterleaks.outcome == 'failure'
|
|
89
|
+
shell: bash
|
|
90
|
+
run: |
|
|
91
|
+
committer_email="$(git log -1 --format='%ce')"
|
|
92
|
+
author_email="$(git log -1 --format='%ae')"
|
|
93
|
+
email="$committer_email"
|
|
94
|
+
if [[ -z "$email" || "$email" == *"noreply.github.com"* ]]; then
|
|
95
|
+
email="$author_email"
|
|
96
|
+
fi
|
|
97
|
+
if [[ "$email" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ && "$email" != *"noreply.github.com"* ]]; then
|
|
98
|
+
echo "email=$email" >> "$GITHUB_OUTPUT"
|
|
99
|
+
else
|
|
100
|
+
echo "No deliverable committer email found; skipping Betterleaks email notification."
|
|
101
|
+
echo "email=" >> "$GITHUB_OUTPUT"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
- name: Email committer on Betterleaks failure
|
|
105
|
+
if: steps.betterleaks.outcome == 'failure' && steps.committer.outputs.email != '' && env.SMTP_URL != '' && env.SMTP_EMAIL != ''
|
|
106
|
+
uses: dawidd6/action-send-mail@v18
|
|
107
|
+
with:
|
|
108
|
+
server_address: ${{ env.SMTP_URL }}
|
|
109
|
+
server_port: ${{ env.SMTP_PORT }}
|
|
110
|
+
secure: ${{ env.SMTP_PORT == '465' }}
|
|
111
|
+
username: ${{ env.SMTP_EMAIL }}
|
|
112
|
+
password: ${{ env.SMTP_PASSWORD }}
|
|
113
|
+
from: ${{ env.SMTP_EMAIL }}
|
|
114
|
+
to: ${{ steps.committer.outputs.email }}
|
|
115
|
+
subject: "[Betterleaks] Secret scan failed in ${{ github.repository }}"
|
|
116
|
+
body: |
|
|
117
|
+
Betterleaks detected one or more potential secrets.
|
|
118
|
+
|
|
119
|
+
Repository: ${{ github.repository }}
|
|
120
|
+
Branch: ${{ github.ref_name }}
|
|
121
|
+
Commit: ${{ github.sha }}
|
|
122
|
+
Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
123
|
+
|
|
124
|
+
Findings:
|
|
125
|
+
${{ steps.leak_summary.outputs.text }}
|
|
126
|
+
|
|
127
|
+
Download the betterleaks-report artifact from the workflow run for details.
|
|
128
|
+
|
|
129
|
+
- name: Fail if Betterleaks found leaks
|
|
130
|
+
if: steps.betterleaks.outcome == 'failure'
|
|
131
|
+
run: |
|
|
132
|
+
echo "Betterleaks detected one or more secrets. Download the betterleaks-report artifact from this workflow run for details."
|
|
133
|
+
exit 1
|
package/.gitleaks.toml
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
title = "Betterleaks config"
|
|
2
|
+
|
|
3
|
+
[extend]
|
|
4
|
+
useDefault = true
|
|
5
|
+
|
|
6
|
+
[[rules]]
|
|
7
|
+
id = "secret-literal-assignment"
|
|
8
|
+
description = "Secret-like literal assignment in source, YAML, env and config files"
|
|
9
|
+
regex = '''(?i)([A-Za-z0-9_.-]*(?:password|passwd|pwd|api[_-]?key|apikey|access[_-]?key|secret[_-]?access[_-]?key|private[_-]?key|client[_-]?secret|consumer[_-]?key|consumer[_-]?secret|(?:auth|access|refresh|session|api|bearer|id|jwt|csrf|xsrf|oauth)[_-]?token|translate[_-]?auth|translation[_-]?auth|auth[_-]?password|auth[_-]?key|auth[_-]?credentials|database[_-]?url|connection[_-]?string)[A-Za-z0-9_.-]*)[ \t]*[:=][ \t]*["'']?([^"''[:space:]#{}$.][^"''\n#{}]{2,})["'']?'''
|
|
10
|
+
secretGroup = 2
|
|
11
|
+
keywords = [
|
|
12
|
+
"password",
|
|
13
|
+
"passwd",
|
|
14
|
+
"pwd",
|
|
15
|
+
"api_key",
|
|
16
|
+
"apikey",
|
|
17
|
+
"access_key",
|
|
18
|
+
"private_key",
|
|
19
|
+
"client_secret",
|
|
20
|
+
"consumer_key",
|
|
21
|
+
"consumer_secret",
|
|
22
|
+
"auth_token",
|
|
23
|
+
"access_token",
|
|
24
|
+
"refresh_token",
|
|
25
|
+
"session_token",
|
|
26
|
+
"api_token",
|
|
27
|
+
"bearer_token",
|
|
28
|
+
"id_token",
|
|
29
|
+
"jwt_token",
|
|
30
|
+
"csrf_token",
|
|
31
|
+
"xsrf_token",
|
|
32
|
+
"oauth_token",
|
|
33
|
+
"translate_auth",
|
|
34
|
+
"translation_auth",
|
|
35
|
+
"auth_password",
|
|
36
|
+
"auth_key",
|
|
37
|
+
"auth_credentials",
|
|
38
|
+
"database_url",
|
|
39
|
+
"connection_string"
|
|
40
|
+
]
|
|
41
|
+
tags = ["literal-secret"]
|
|
42
|
+
|
|
43
|
+
[[rules]]
|
|
44
|
+
id = "env-short-secret-assignment"
|
|
45
|
+
description = "Uppercase env-style PASS/TOKEN/SECRET assignment"
|
|
46
|
+
regex = '''\b((?:PASS|TOKEN|SECRET|[A-Z0-9_]*(?:_PASS|_TOKEN|_SECRET|PASS_|TOKEN_|SECRET_)[A-Z0-9_]*))[ \t]*[:=][ \t]*["'']?([^"''[:space:]#{}$.][^"''\n#{}]{2,})["'']?'''
|
|
47
|
+
secretGroup = 2
|
|
48
|
+
keywords = [
|
|
49
|
+
"PASS",
|
|
50
|
+
"TOKEN",
|
|
51
|
+
"SECRET"
|
|
52
|
+
]
|
|
53
|
+
tags = ["env", "literal-secret"]
|
|
54
|
+
|
|
55
|
+
[[rules]]
|
|
56
|
+
id = "standalone-sk-token"
|
|
57
|
+
description = "Standalone sk-* token not attached to a secret-like variable name"
|
|
58
|
+
regex = '''(?i)\b(sk-[A-Za-z0-9][A-Za-z0-9_-]{20,})\b'''
|
|
59
|
+
secretGroup = 1
|
|
60
|
+
entropy = 2.5
|
|
61
|
+
keywords = ["sk-"]
|
|
62
|
+
tags = ["standalone-token", "generic"]
|
|
63
|
+
|
|
64
|
+
[[rules]]
|
|
65
|
+
id = "dotenv-only-jest-setup"
|
|
66
|
+
description = ".env may only contain the committed Jest setup lines"
|
|
67
|
+
path = '''(?i)(^|/)\.env$'''
|
|
68
|
+
regex = '''(?m)^(.+)$'''
|
|
69
|
+
secretGroup = 1
|
|
70
|
+
tags = ["file", "dotenv"]
|
|
71
|
+
[[rules.allowlists]]
|
|
72
|
+
regexTarget = "match"
|
|
73
|
+
regexes = [
|
|
74
|
+
'''^JEST_USE_SETUP=OFF # Jest configuration variables: ON, OFF\r?$''',
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[[rules]]
|
|
78
|
+
id = "forbidden-secret-file"
|
|
79
|
+
description = "Forbidden secret-bearing file committed to repository"
|
|
80
|
+
path = '''(?i)(^|/)(\.env\..*|\.npmrc|\.pypirc|id_rsa|id_ed25519|.*\.(pem|key|p12|pfx|jks|kubeconfig))$'''
|
|
81
|
+
regex = '''(?s).{1,}'''
|
|
82
|
+
tags = ["file", "secret-file"]
|
|
83
|
+
|
|
84
|
+
[[allowlists]]
|
|
85
|
+
description = "Allow Jenkins SonarQube token environment variable reference"
|
|
86
|
+
regexTarget = "match"
|
|
87
|
+
regexes = [
|
|
88
|
+
'''SONAR_AUTH_TOKEN''',
|
|
89
|
+
]
|
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
-
### [3.0.
|
|
7
|
+
### [3.0.1](https://github.com/eea/volto-eea-chatbot/compare/3.0.0...3.0.1) - 5 August 2026
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: Add betterleaks github action - refs #304517 [dobri1408 - [`837c461`](https://github.com/eea/volto-eea-chatbot/commit/837c4619bd3dab3ffe06aeaabd0513c50e3066b4)]
|
|
12
|
+
|
|
13
|
+
## [3.0.0](https://github.com/eea/volto-eea-chatbot/compare/2.0.5...3.0.0) - 7 July 2026
|
|
8
14
|
|
|
9
15
|
#### :bug: Bug Fixes
|
|
10
16
|
|
|
@@ -29,6 +35,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
29
35
|
#### :house: Internal changes
|
|
30
36
|
|
|
31
37
|
- chore: bump version to 3.0.0 (EEA design system dependency) [skip ci] [Tiberiu Ichim - [`bc5a8c1`](https://github.com/eea/volto-eea-chatbot/commit/bc5a8c1f72d4e706b096cb96d0d24c2c3f5ac0f8)]
|
|
38
|
+
- chore: [JENKINSFILE] sanitize sonarqube command Refs #305084 [valentinab25 - [`83bf08b`](https://github.com/eea/volto-eea-chatbot/commit/83bf08b7188548a50801b8e464a4d113f0cbbac4)]
|
|
32
39
|
|
|
33
40
|
#### :house: Documentation changes
|
|
34
41
|
|
|
@@ -106,6 +113,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
106
113
|
#### :house: Internal changes
|
|
107
114
|
|
|
108
115
|
- style: Automated code fix [eea-jenkins - [`8a8c3c4`](https://github.com/eea/volto-eea-chatbot/commit/8a8c3c4172a4f669661378cf1b5a3569d85609e6)]
|
|
116
|
+
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`535d986`](https://github.com/eea/volto-eea-chatbot/commit/535d986b7adc77743a668bc4ac63f835eef58df3)]
|
|
109
117
|
|
|
110
118
|
#### :hammer_and_wrench: Others
|
|
111
119
|
|
|
@@ -116,6 +124,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
116
124
|
#### :house: Internal changes
|
|
117
125
|
|
|
118
126
|
- style: Automated code fix [eea-jenkins - [`fdcd884`](https://github.com/eea/volto-eea-chatbot/commit/fdcd8848fd4c3f990ca5ea021f407487aebd6010)]
|
|
127
|
+
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`3d428d7`](https://github.com/eea/volto-eea-chatbot/commit/3d428d72f32c3d05452b0961c76f5db1c416e05c)]
|
|
119
128
|
|
|
120
129
|
#### :hammer_and_wrench: Others
|
|
121
130
|
|
package/README.md
CHANGED
|
@@ -162,6 +162,65 @@ See [RELEASE.md](https://github.com/eea/volto-eea-chatbot/blob/master/RELEASE.md
|
|
|
162
162
|
|
|
163
163
|
See [DEVELOP.md](https://github.com/eea/volto-eea-chatbot/blob/master/DEVELOP.md).
|
|
164
164
|
|
|
165
|
+
## Secret Scanning
|
|
166
|
+
|
|
167
|
+
This repository uses the Betterleaks GitHub Action to scan the current
|
|
168
|
+
repository content on every push and pull request. The scan uses the rules in
|
|
169
|
+
`.gitleaks.toml` and uploads a `betterleaks-report` artifact when a finding is
|
|
170
|
+
detected.
|
|
171
|
+
|
|
172
|
+
If the optional SMTP secrets are configured, failed scans also send an email to
|
|
173
|
+
the last commit committer. The workflow expects these repository or
|
|
174
|
+
organization secrets:
|
|
175
|
+
|
|
176
|
+
- `SMTP_URL`
|
|
177
|
+
- `SMTP_PORT` (optional, defaults to `25`)
|
|
178
|
+
- `SMTP_EMAIL`
|
|
179
|
+
- `SMTP_PASSWORD` (optional if the SMTP server does not require authentication)
|
|
180
|
+
|
|
181
|
+
Port `465` is sent with direct TLS; other ports use the default SMTP handshake.
|
|
182
|
+
The email includes a short finding summary from the redacted Betterleaks report,
|
|
183
|
+
including the redacted matched line from each finding.
|
|
184
|
+
|
|
185
|
+
There are three common outcomes:
|
|
186
|
+
|
|
187
|
+
1. **Everything is OK.** The `Betterleaks / Scan for secrets` check is green and
|
|
188
|
+
no action is needed. Regular references to runtime values are OK, for example:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
const tokenFromCookie = req.universalCookies.get('auth_token');
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
2. **A real secret was found.** The check is red and the workflow log asks you to
|
|
195
|
+
download the `betterleaks-report` artifact. Open the artifact from the GitHub
|
|
196
|
+
Actions run and check the reported file, line and rule. Remove the committed
|
|
197
|
+
value, move it to the proper secret store, and rotate it if it was exposed.
|
|
198
|
+
A report entry looks like this:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"RuleID": "secret-literal-assignment",
|
|
203
|
+
"File": "src/config.js",
|
|
204
|
+
"StartLine": 12,
|
|
205
|
+
"Secret": "[REDACTED]"
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
3. **The finding is a false positive.** Keep the value only if it is clearly not
|
|
210
|
+
sensitive, such as a test fixture, placeholder, or public example. Add
|
|
211
|
+
`betterleaks:allow` on the same line and include a short explanation in the
|
|
212
|
+
pull request.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
const testPassword = 'admin'; //betterleaks:allow
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
password: "admin" #betterleaks:allow
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Do not add `betterleaks:allow` to real credentials.
|
|
223
|
+
|
|
165
224
|
## Copyright and license
|
|
166
225
|
|
|
167
226
|
The Initial Owner of the Original Code is European Environment Agency (EEA).
|
package/RELEASE.md
CHANGED
|
@@ -33,7 +33,7 @@ Release-it is a tool that automates 4 important steps in the release process:
|
|
|
33
33
|
To configure the authentification, you need to export GITHUB_TOKEN for [GitHub](https://github.com/settings/tokens)
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
-
export GITHUB_TOKEN=
|
|
36
|
+
export GITHUB_TOKEN="${GITHUB_TOKEN}"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
To configure npm, you can use the `npm login` command or use a configuration file with a TOKEN :
|
package/package.json
CHANGED
|
@@ -133,7 +133,14 @@ export function MultiToolRenderer({
|
|
|
133
133
|
{filteredToolGroups.map((toolGroup, index) => {
|
|
134
134
|
const isLastItem = index === filteredToolGroups.length - 1;
|
|
135
135
|
const toolState = toolStates.get(toolGroup.ind);
|
|
136
|
-
const isToolCompleted = toolState?.isCompleted;
|
|
136
|
+
const isToolCompleted = toolState?.isCompleted === true;
|
|
137
|
+
const isToolActive =
|
|
138
|
+
isOverallStreaming && !isToolCompleted && isLastItem;
|
|
139
|
+
const toolDisplayState = isToolActive
|
|
140
|
+
? 'active'
|
|
141
|
+
: isToolCompleted
|
|
142
|
+
? 'completed'
|
|
143
|
+
: 'pending';
|
|
137
144
|
|
|
138
145
|
return (
|
|
139
146
|
<div
|
|
@@ -141,6 +148,8 @@ export function MultiToolRenderer({
|
|
|
141
148
|
className={cx({
|
|
142
149
|
'tool-collapsed-wrapper': isOverallStreaming,
|
|
143
150
|
})}
|
|
151
|
+
data-state={toolDisplayState}
|
|
152
|
+
aria-current={isToolActive ? 'step' : undefined}
|
|
144
153
|
>
|
|
145
154
|
<RendererComponent
|
|
146
155
|
packets={toolGroup.packets}
|
|
@@ -158,12 +167,7 @@ export function MultiToolRenderer({
|
|
|
158
167
|
const finalIcon = icon ? (
|
|
159
168
|
React.createElement(icon, { size: 14 })
|
|
160
169
|
) : (
|
|
161
|
-
<span
|
|
162
|
-
className={cx({
|
|
163
|
-
'tool-icon-dot': isOverallStreaming,
|
|
164
|
-
'tool-icon-default': !isOverallStreaming,
|
|
165
|
-
})}
|
|
166
|
-
/>
|
|
170
|
+
<span className="tool-icon-dot" />
|
|
167
171
|
);
|
|
168
172
|
|
|
169
173
|
// If tool is not completed and we are overall streaming, show collapsed view
|
|
@@ -184,14 +188,14 @@ export function MultiToolRenderer({
|
|
|
184
188
|
return (
|
|
185
189
|
<div
|
|
186
190
|
className={cx('tool-item-collapsed', {
|
|
187
|
-
active:
|
|
191
|
+
active: isToolActive,
|
|
188
192
|
completed: isToolCompleted,
|
|
189
193
|
})}
|
|
190
194
|
>
|
|
191
195
|
<div className="tool-collapsed-icon">
|
|
192
196
|
{finalIcon}
|
|
193
197
|
</div>
|
|
194
|
-
<span className="tool-collapsed-status">
|
|
198
|
+
<span className="tool-status tool-collapsed-status">
|
|
195
199
|
{status}
|
|
196
200
|
</span>
|
|
197
201
|
</div>
|
|
@@ -199,9 +203,14 @@ export function MultiToolRenderer({
|
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
//
|
|
206
|
+
// Detailed view for streaming reasoning/search tools and completed tools
|
|
203
207
|
return (
|
|
204
|
-
<div
|
|
208
|
+
<div
|
|
209
|
+
className={cx('tool-item-expanded', {
|
|
210
|
+
active: isToolActive,
|
|
211
|
+
completed: isToolCompleted,
|
|
212
|
+
})}
|
|
213
|
+
>
|
|
205
214
|
<div className="tool-connector-line" />
|
|
206
215
|
|
|
207
216
|
<div className="tool-item-row">
|
|
@@ -33,6 +33,7 @@ export class MessageProcessor {
|
|
|
33
33
|
private _errorContent: string = '';
|
|
34
34
|
private _documents: OnyxDocument[] = [];
|
|
35
35
|
private _citations = new Map<number, string>();
|
|
36
|
+
private _citationsAreFallback: boolean = false;
|
|
36
37
|
private _isComplete: boolean = false;
|
|
37
38
|
private _isFinalMessageComing: boolean = false;
|
|
38
39
|
|
|
@@ -208,20 +209,28 @@ export class MessageProcessor {
|
|
|
208
209
|
|
|
209
210
|
if (newDocuments) {
|
|
210
211
|
this._documents = Array.from(this.documentMap.values());
|
|
212
|
+
}
|
|
211
213
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
// If we have final_documents and no citations yet, create a fallback mapping
|
|
215
|
+
// This ensures the Sources tab shows up in v3 when citation_info is missing
|
|
216
|
+
if (
|
|
217
|
+
packet.obj.type === PacketType.MESSAGE_START &&
|
|
218
|
+
data.final_documents &&
|
|
219
|
+
this._citations.size === 0
|
|
220
|
+
) {
|
|
221
|
+
data.final_documents.forEach((doc: OnyxDocument, index: number) => {
|
|
222
|
+
if (doc.document_id) {
|
|
223
|
+
this._citations.set(index + 1, doc.document_id);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
this._citationsAreFallback = this._citations.size > 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private clearFallbackCitations(): void {
|
|
231
|
+
if (this._citationsAreFallback) {
|
|
232
|
+
this._citations.clear();
|
|
233
|
+
this._citationsAreFallback = false;
|
|
225
234
|
}
|
|
226
235
|
}
|
|
227
236
|
|
|
@@ -233,6 +242,7 @@ export class MessageProcessor {
|
|
|
233
242
|
if (packet.obj.type === PacketType.CITATION_INFO) {
|
|
234
243
|
const citationInfo = packet.obj as any;
|
|
235
244
|
if (citationInfo.citation_number && citationInfo.document_id) {
|
|
245
|
+
this.clearFallbackCitations();
|
|
236
246
|
this._citations.set(
|
|
237
247
|
citationInfo.citation_number,
|
|
238
248
|
citationInfo.document_id,
|
|
@@ -244,7 +254,14 @@ export class MessageProcessor {
|
|
|
244
254
|
return;
|
|
245
255
|
}
|
|
246
256
|
const citationDelta = packet.obj as CitationDelta;
|
|
247
|
-
citationDelta.citations
|
|
257
|
+
const explicitCitations = (citationDelta.citations || []).filter(
|
|
258
|
+
(citation: StreamingCitation) =>
|
|
259
|
+
citation.citation_num != null && Boolean(citation.document_id),
|
|
260
|
+
);
|
|
261
|
+
if (explicitCitations.length > 0) {
|
|
262
|
+
this.clearFallbackCitations();
|
|
263
|
+
}
|
|
264
|
+
explicitCitations.forEach((citation: StreamingCitation) => {
|
|
248
265
|
if (!this._citations.has(citation.citation_num)) {
|
|
249
266
|
this._citations.set(citation.citation_num, citation.document_id);
|
|
250
267
|
}
|
|
@@ -397,6 +414,7 @@ export class MessageProcessor {
|
|
|
397
414
|
this._errorContent = '';
|
|
398
415
|
this._documents = [];
|
|
399
416
|
this._citations.clear();
|
|
417
|
+
this._citationsAreFallback = false;
|
|
400
418
|
this._isComplete = false;
|
|
401
419
|
this._isFinalMessageComing = false;
|
|
402
420
|
}
|
package/src/ChatBlock/style.less
CHANGED
|
@@ -1604,14 +1604,6 @@ mark {
|
|
|
1604
1604
|
}
|
|
1605
1605
|
}
|
|
1606
1606
|
|
|
1607
|
-
.tool-icon-default {
|
|
1608
|
-
display: inline-block;
|
|
1609
|
-
width: 0.5rem;
|
|
1610
|
-
height: 0.5rem;
|
|
1611
|
-
border-radius: 50%;
|
|
1612
|
-
background-color: currentColor;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
1607
|
// Content column
|
|
1616
1608
|
.tool-content {
|
|
1617
1609
|
flex: 1;
|
|
@@ -1625,17 +1617,6 @@ mark {
|
|
|
1625
1617
|
display: flex;
|
|
1626
1618
|
}
|
|
1627
1619
|
|
|
1628
|
-
.tool-status {
|
|
1629
|
-
display: flex;
|
|
1630
|
-
align-items: center;
|
|
1631
|
-
border-radius: 12px;
|
|
1632
|
-
margin-bottom: 0.25rem;
|
|
1633
|
-
color: @teal;
|
|
1634
|
-
font-size: 0.875rem;
|
|
1635
|
-
font-weight: 500;
|
|
1636
|
-
gap: 0.25rem;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
1620
|
.tool-text {
|
|
1640
1621
|
color: #6b7280;
|
|
1641
1622
|
font-size: 0.75rem;
|
|
@@ -1651,6 +1632,47 @@ mark {
|
|
|
1651
1632
|
.multi-tool-renderer {
|
|
1652
1633
|
margin-bottom: 1em;
|
|
1653
1634
|
|
|
1635
|
+
// Shared tool state styles
|
|
1636
|
+
.tool-icon-dot {
|
|
1637
|
+
display: inline-block;
|
|
1638
|
+
width: 0.5rem;
|
|
1639
|
+
height: 0.5rem;
|
|
1640
|
+
border-radius: 50%;
|
|
1641
|
+
background-color: currentColor;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.tool-status {
|
|
1645
|
+
display: flex;
|
|
1646
|
+
align-items: center;
|
|
1647
|
+
border-radius: 12px;
|
|
1648
|
+
margin-bottom: 0.25rem;
|
|
1649
|
+
color: @teal;
|
|
1650
|
+
font-size: 0.875rem;
|
|
1651
|
+
font-weight: 500;
|
|
1652
|
+
gap: 0.25rem;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
.tool-item-expanded.active .tool-icon-circle,
|
|
1656
|
+
.tool-item-collapsed.active .tool-collapsed-icon {
|
|
1657
|
+
color: @teal;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.active .tool-status {
|
|
1661
|
+
animation: shimmer 2.5s linear infinite;
|
|
1662
|
+
background: linear-gradient(
|
|
1663
|
+
90deg,
|
|
1664
|
+
@teal 0%,
|
|
1665
|
+
@teal 40%,
|
|
1666
|
+
rgba(0, 179, 174, 0.5) 50%,
|
|
1667
|
+
@teal 60%,
|
|
1668
|
+
@teal 100%
|
|
1669
|
+
);
|
|
1670
|
+
-webkit-background-clip: text;
|
|
1671
|
+
background-clip: text;
|
|
1672
|
+
background-size: 200% 100%;
|
|
1673
|
+
-webkit-text-fill-color: transparent;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1654
1676
|
// Common header styles for both collapsed and summary headers
|
|
1655
1677
|
.tools-collapsed-header,
|
|
1656
1678
|
.tools-summary-header {
|
|
@@ -1766,29 +1788,6 @@ mark {
|
|
|
1766
1788
|
}
|
|
1767
1789
|
}
|
|
1768
1790
|
|
|
1769
|
-
&.active {
|
|
1770
|
-
.tool-collapsed-icon {
|
|
1771
|
-
position: relative;
|
|
1772
|
-
color: @teal;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
.tool-collapsed-status {
|
|
1776
|
-
animation: shimmer 2.5s linear infinite;
|
|
1777
|
-
background: linear-gradient(
|
|
1778
|
-
90deg,
|
|
1779
|
-
@teal 0%,
|
|
1780
|
-
@teal 40%,
|
|
1781
|
-
rgba(0, 179, 174, 0.5) 50%,
|
|
1782
|
-
@teal 60%,
|
|
1783
|
-
@teal 100%
|
|
1784
|
-
);
|
|
1785
|
-
-webkit-background-clip: text;
|
|
1786
|
-
background-clip: text;
|
|
1787
|
-
background-size: 200% 100%;
|
|
1788
|
-
-webkit-text-fill-color: transparent;
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
1791
|
.tool-collapsed-icon {
|
|
1793
1792
|
display: flex;
|
|
1794
1793
|
width: 1rem;
|
|
@@ -1798,17 +1797,8 @@ mark {
|
|
|
1798
1797
|
justify-content: center;
|
|
1799
1798
|
}
|
|
1800
1799
|
|
|
1801
|
-
.tool-icon-dot {
|
|
1802
|
-
display: inline-block;
|
|
1803
|
-
width: 6px;
|
|
1804
|
-
height: 6px;
|
|
1805
|
-
border-radius: 50%;
|
|
1806
|
-
background-color: currentColor;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
1800
|
.tool-collapsed-status {
|
|
1810
1801
|
flex: 1;
|
|
1811
|
-
font-weight: 500;
|
|
1812
1802
|
}
|
|
1813
1803
|
}
|
|
1814
1804
|
|
|
@@ -4,13 +4,13 @@ import fetch from 'node-fetch';
|
|
|
4
4
|
const log = debug('halloumi');
|
|
5
5
|
|
|
6
6
|
const LLMGW_URL = process.env.LLMGW_URL;
|
|
7
|
-
const LLMGW_API_KEY = process.env.LLMGW_TOKEN;
|
|
7
|
+
const LLMGW_API_KEY = process.env.LLMGW_TOKEN; //betterleaks:allow
|
|
8
8
|
const MIN_CONTEXT_SENTENCES_FOR_FILTERING = 75;
|
|
9
9
|
|
|
10
10
|
const filterModel = {
|
|
11
11
|
name: 'Inhouse-LLM/gpt-oss-120b',
|
|
12
12
|
apiUrl: `${LLMGW_URL}/chat/completions`,
|
|
13
|
-
apiKey: LLMGW_API_KEY,
|
|
13
|
+
apiKey: LLMGW_API_KEY, //betterleaks:allow
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export async function callLLM(apiUrl, apiKey, requestBody, { ip } = {}) {
|
|
@@ -196,7 +196,7 @@ describe('halloumiGenerativeAPI via real fetch', () => {
|
|
|
196
196
|
const model = {
|
|
197
197
|
name: 'test-model',
|
|
198
198
|
apiUrl: 'http://test.com/api',
|
|
199
|
-
apiKey: 'test-key',
|
|
199
|
+
apiKey: 'test-key', //betterleaks:allow
|
|
200
200
|
};
|
|
201
201
|
const prompt = {
|
|
202
202
|
prompt: 'test prompt',
|
|
@@ -15,7 +15,7 @@ const ALLOWED_HALLOUMI_PATHS = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
const LLMGW_URL = process.env.LLMGW_URL;
|
|
18
|
-
const LLMGW_TOKEN = process.env.LLMGW_TOKEN;
|
|
18
|
+
const LLMGW_TOKEN = process.env.LLMGW_TOKEN; //betterleaks:allow
|
|
19
19
|
|
|
20
20
|
const generativeModel = {
|
|
21
21
|
name: 'Inhouse-LLM/HallOumi-8B',
|
|
@@ -57,7 +57,7 @@ export default async function middleware(req, res, next) {
|
|
|
57
57
|
|
|
58
58
|
const model = {
|
|
59
59
|
...(path === '/classify' ? classifyModel : generativeModel),
|
|
60
|
-
apiKey: LLMGW_TOKEN,
|
|
60
|
+
apiKey: LLMGW_TOKEN, //betterleaks:allow
|
|
61
61
|
};
|
|
62
62
|
const body = req.body;
|
|
63
63
|
|
|
@@ -42,7 +42,7 @@ describe('halloumi middleware', () => {
|
|
|
42
42
|
error: 'Invalid configuration: missing LLMGW_TOKEN or LLMGW_URL',
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
process.env.LLMGW_TOKEN = origToken;
|
|
45
|
+
process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
|
|
46
46
|
process.env.LLMGW_URL = origUrl;
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -92,7 +92,7 @@ describe('halloumi middleware - dynamic import', () => {
|
|
|
92
92
|
it('sends response on successful getVerifyClaimResponse', async () => {
|
|
93
93
|
const origToken = process.env.LLMGW_TOKEN;
|
|
94
94
|
const origUrl = process.env.LLMGW_URL;
|
|
95
|
-
process.env.LLMGW_TOKEN = 'test-token';
|
|
95
|
+
process.env.LLMGW_TOKEN = 'test-token'; //betterleaks:allow
|
|
96
96
|
process.env.LLMGW_URL = 'http://test-url';
|
|
97
97
|
|
|
98
98
|
jest.resetModules();
|
|
@@ -116,14 +116,14 @@ describe('halloumi middleware - dynamic import', () => {
|
|
|
116
116
|
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/json');
|
|
117
117
|
expect(res.send).toHaveBeenCalledWith({ claims: [], segments: {} });
|
|
118
118
|
|
|
119
|
-
process.env.LLMGW_TOKEN = origToken;
|
|
119
|
+
process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
|
|
120
120
|
process.env.LLMGW_URL = origUrl;
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('handles errors from getVerifyClaimResponse', async () => {
|
|
124
124
|
const origToken = process.env.LLMGW_TOKEN;
|
|
125
125
|
const origUrl = process.env.LLMGW_URL;
|
|
126
|
-
process.env.LLMGW_TOKEN = 'test-token';
|
|
126
|
+
process.env.LLMGW_TOKEN = 'test-token'; //betterleaks:allow
|
|
127
127
|
process.env.LLMGW_URL = 'http://test-url';
|
|
128
128
|
|
|
129
129
|
jest.resetModules();
|
|
@@ -148,7 +148,7 @@ describe('halloumi middleware - dynamic import', () => {
|
|
|
148
148
|
expect.objectContaining({ error: expect.stringContaining('LLM error') }),
|
|
149
149
|
);
|
|
150
150
|
|
|
151
|
-
process.env.LLMGW_TOKEN = origToken;
|
|
151
|
+
process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
|
|
152
152
|
process.env.LLMGW_URL = origUrl;
|
|
153
153
|
});
|
|
154
154
|
});
|
package/src/middleware.js
CHANGED
|
@@ -289,7 +289,7 @@ export default async function middleware(req, res, next) {
|
|
|
289
289
|
|
|
290
290
|
const reqUrl = `${process.env.ONYX_URL || ''}/api${path}`;
|
|
291
291
|
|
|
292
|
-
const api_key = process.env.ONYX_API_KEY;
|
|
292
|
+
const api_key = process.env.ONYX_API_KEY; //betterleaks:allow
|
|
293
293
|
if (!api_key) {
|
|
294
294
|
res.statusCode = 500;
|
|
295
295
|
res.statusMessage = MSG_INVALID_CONFIGURATION;
|
|
@@ -300,7 +300,7 @@ export default async function middleware(req, res, next) {
|
|
|
300
300
|
try {
|
|
301
301
|
await send_onyx_request(req, res, {
|
|
302
302
|
url: reqUrl,
|
|
303
|
-
api_key: api_key,
|
|
303
|
+
api_key: api_key, //betterleaks:allow
|
|
304
304
|
is_related_question,
|
|
305
305
|
});
|
|
306
306
|
} catch (error) {
|
package/src/middleware.test.js
CHANGED
|
@@ -89,7 +89,7 @@ describe('src/middleware', () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('proxies POST request with api_key and pipes response', async () => {
|
|
92
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
92
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
93
93
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
94
94
|
|
|
95
95
|
await middleware(req, res, next);
|
|
@@ -110,7 +110,7 @@ describe('src/middleware', () => {
|
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
it('handles related question paths', async () => {
|
|
113
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
113
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
114
114
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
115
115
|
req.url = '/_rq/chat/send-message';
|
|
116
116
|
|
|
@@ -124,7 +124,7 @@ describe('src/middleware', () => {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
it('sends error response when fetch throws', async () => {
|
|
127
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
127
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
128
128
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
129
129
|
|
|
130
130
|
nodeFetch.mockRejectedValueOnce(
|
|
@@ -145,7 +145,7 @@ describe('src/middleware', () => {
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
it('handles error without response text', async () => {
|
|
148
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
148
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
149
149
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
150
150
|
|
|
151
151
|
nodeFetch.mockRejectedValueOnce(new Error('No response'));
|
|
@@ -162,7 +162,7 @@ describe('src/middleware', () => {
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
it('handles GET requests without body', async () => {
|
|
165
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
165
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
166
166
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
167
167
|
req.method = 'GET';
|
|
168
168
|
req.url = '/_da/persona/1';
|
|
@@ -175,7 +175,7 @@ describe('src/middleware', () => {
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
it('uses mock create-chat-session when MOCK_LLM_FILE_PATH is set', async () => {
|
|
178
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
178
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
179
179
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
180
180
|
process.env.MOCK_LLM_FILE_PATH = '/tmp/mock.jsonl';
|
|
181
181
|
req.url = '/_da/chat/create-chat-session';
|
|
@@ -189,7 +189,7 @@ describe('src/middleware', () => {
|
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
it('uses mock send-message with stream when MOCK_LLM_FILE_PATH is set', async () => {
|
|
192
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
192
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
193
193
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
194
194
|
process.env.MOCK_LLM_FILE_PATH = '/tmp/mock.jsonl';
|
|
195
195
|
req.url = '/_da/chat/send-message';
|
|
@@ -223,7 +223,7 @@ describe('src/middleware', () => {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
it('dumps LLM response when DUMP_LLM_FILE_PATH is set', async () => {
|
|
226
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
226
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
227
227
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
228
228
|
process.env.DUMP_LLM_FILE_PATH = '/tmp/dumped_response.jsonl';
|
|
229
229
|
|
|
@@ -236,7 +236,7 @@ describe('src/middleware', () => {
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
it('rejects disallowed paths with 404', async () => {
|
|
239
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
239
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
240
240
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
241
241
|
req.url = '/_da/admin/users/delete';
|
|
242
242
|
req.method = 'POST';
|
|
@@ -249,7 +249,7 @@ describe('src/middleware', () => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
it('rejects allowed path with wrong HTTP method', async () => {
|
|
252
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
252
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
253
253
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
254
254
|
req.url = '/_da/persona';
|
|
255
255
|
req.method = 'DELETE';
|
|
@@ -262,7 +262,7 @@ describe('src/middleware', () => {
|
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
it('rejects path traversal attempts', async () => {
|
|
265
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
265
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
266
266
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
267
267
|
req.url = '/_da/../../etc/passwd';
|
|
268
268
|
req.method = 'GET';
|