@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.
@@ -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.0](https://github.com/eea/volto-eea-chatbot/compare/2.0.5...3.0.0) - 7 July 2026
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=XXX-XXXXXXXXXXXXXXXXXXXXXX
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-chatbot",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "@eeacms/volto-eea-chatbot: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -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: isLastItem,
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
- // Expanded view (full content) - used for completed tools or when overall complete
206
+ // Detailed view for streaming reasoning/search tools and completed tools
203
207
  return (
204
- <div className="tool-item-expanded">
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
- // If we have final_documents and no citations yet, create a fallback mapping
213
- // This ensures the Sources tab shows up in v3 when citation_info is missing
214
- if (
215
- packet.obj.type === PacketType.MESSAGE_START &&
216
- data.final_documents &&
217
- this._citations.size === 0
218
- ) {
219
- data.final_documents.forEach((doc: OnyxDocument, index: number) => {
220
- if (doc.document_id) {
221
- this._citations.set(index + 1, doc.document_id);
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?.forEach((citation: StreamingCitation) => {
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
  }
@@ -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) {
@@ -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';