@hartren/assistant-js 0.0.0-stage → 2.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,140 @@
1
+ # Changelog
2
+
3
+ ## 2.3.1 (unreleased)
4
+
5
+ ### Changed
6
+
7
+ - First version for the public npm registry (`registry.npmjs.org`). 2.3.0 is skipped because tag
8
+ `v2.3.0` already exists in the repository. Built from source in CI; the pack's SHA-256 and file list
9
+ are kept as a CI artifact. Third-party notices are included.
10
+ - `package.json` no longer has `homepage` or `bugs` fields.
11
+
12
+ ## 2.3.0 (2026-11-02)
13
+
14
+ ### Changed
15
+
16
+ - **Renamed to `@hartren/assistant-js`** (GitHub Packages). Replace `@hartren/copilotagent-js` with
17
+ `@hartren/assistant-js` in your `package.json` and imports. The API is unchanged. Existing
18
+ `@hartren/copilotagent-js` versions stay published unchanged.
19
+
20
+ ## 2.2.0 (2026-10-25)
21
+
22
+ ### Added
23
+
24
+ - **`browserTranscriptionFallback`** (boolean, default `false`). When `true`, dictation switches to
25
+ the browser's `SpeechRecognition` when the hosted transcription socket cannot connect or drops
26
+ mid-session while the browser is online, and returns to the hosted service on `online` or at the
27
+ next `startListening()`. Offline behaves as without the option (cloud Web Speech cannot work
28
+ offline either): no switch, and a fallback already running is paused, with no restarts and no
29
+ "network hiccup" errors, until `online`. New `onCopilotError` codes `transcription-fallback` and
30
+ `transcription-restored` (both `final: false`), and `agent.isUsingBrowserTranscriptionFallback()`.
31
+ This restores, behind an opt-in, the automatic Web Speech fallback removed in 2.0.0 (ce9e), now
32
+ with hand-back.
33
+ **Warning:** when on, dictation audio may be processed by the browser vendor's speech service (no
34
+ BAA). Only enable it where that is acceptable, never with PHI. With the default (`false`),
35
+ `SpeechRecognition` is never constructed and nothing changes.
36
+
37
+ ## 2.1.0 (2026-10-24)
38
+
39
+ ### Changed
40
+
41
+ - **Renamed to `@hartren/copilotagent-js` and published to GitHub Packages** (`npm.pkg.github.com`)
42
+ instead of public npm. Replace `@radaid/copilotagent-js` with `@hartren/copilotagent-js` in
43
+ `package.json` and imports, and add the `@hartren` registry line to `.npmrc` (see the README). The
44
+ API is unchanged. Existing `@radaid/copilotagent-js` versions (up to 2.0.1) stay on public npm.
45
+
46
+ ### Fixed
47
+
48
+ - Legacy PAT mode: when `/token/refresh` fails (expired, used or revoked refresh token, or a server
49
+ error), the library now falls back to one PAT re-exchange, shared by concurrent requests, instead
50
+ of failing every request until reload.
51
+ - Errors are logged by name or code only (`errorLabel`), never with their message or response text.
52
+ - HTTP failures throw `HttpError` (exported; `status`, `code` such as `HTTP_500`) and are logged by
53
+ that code. The error message is unchanged.
54
+ - Browser speech start/restart errors log only the error name, and a closed speech socket logs only
55
+ its close code; the full error and the close reason are logged only with debug logging on.
56
+ - Concurrent legacy token refreshes share one request, cleared once when it settles.
57
+
58
+ ### Added
59
+
60
+ - **Host token provider is the supported way to authenticate.** Pass `getAccessToken` (for example
61
+ `() => auth.getAccessToken()` from `@hartren/auth`) and, optionally, `onUnauthorized`
62
+ (`(rejectedToken) => auth.handleUnauthorized(rejectedToken)`). The provider is asked on every API
63
+ request and for the speech socket, and no copy of the token is kept. On a `401` the request is
64
+ retried once, only with a different token from `onUnauthorized` (or, without it, from the provider).
65
+ New exported types `AccessTokenProvider` and `UnauthorizedHandler`.
66
+ - With a provider and no PAT, a missing token rejects with `Not signed in` instead of falling through
67
+ to the PAT exchange.
68
+
69
+ ### Deprecated
70
+
71
+ - `pat`, `setUserId`, the browser PAT exchange (`/token/exchange`) and the legacy refresh
72
+ (`/token/refresh`). They still work when a PAT is passed and the provider is missing or returns
73
+ nothing, and will be removed in a future major version.
74
+
75
+ ### Fixed
76
+
77
+ - `setUserId` no longer starts a PAT exchange when there is no PAT (or when a provider is set), so it
78
+ no longer sends `Authorization: PAT undefined` or leaves an unhandled rejection.
79
+ - In the PAT path, a `401` now drops the cached token and refreshes before the single retry (it used
80
+ to retry with the same token).
81
+
82
+ ## 2.0.1 (2026-10-18)
83
+
84
+ ### Fixed
85
+
86
+ - **No user or AI text in the browser console by default.** Dictated transcripts, prompts, assistant
87
+ replies and raw API responses were logged with `console.log` / `console.info`. That text can be PHI.
88
+ These logs are now off unless a host turns on debug logging: `new CopilotAgent({ debug: true })` or
89
+ `setCopilotDebugLogging(true)` (new exports, with `isCopilotDebugLogging()`). Use it for local
90
+ debugging only. The gated logs are in `CopilotAgent` (API response, combined message, AI reply,
91
+ recognized text) and `ResultsGroupingService` (each transcript and the grouped results).
92
+ - A browser speech-synthesis error logs only its error code (for example `interrupted`), not the
93
+ event: the event's `utterance.text` is the AI reply. A failed tool logs only the error name or code,
94
+ not its message, which can echo tool arguments. The full details are logged only with debug logging on.
95
+ - `package.json` now has real `repository`, `homepage` and `bugs` fields
96
+
97
+ ### Changed
98
+
99
+ - The package no longer ships `src/`. Only `dist/` is published, plus `CHANGELOG.md`, `README.md`,
100
+ `LICENSE` and `package.json`. Nothing imports from `src/`: `main` and `types` point into `dist/`.
101
+
102
+ ## 2.0.0 (2026-10-18)
103
+
104
+ ### BREAKING CHANGE
105
+
106
+ - **The speech token is now sent as a WebSocket subprotocol, never in the URL.** The speech sockets
107
+ (`/ws/speech/transcribe` and `/ws/speech/speak`) offer `hartren.speech.v1` and `bearer.<token>` in
108
+ the `Sec-WebSocket-Protocol` header.
109
+ - **This needs a backend that echoes `hartren.speech.v1`.** Against an
110
+ older backend, Chrome refuses the socket and dictation and voice replies fail. Don't upgrade a host that talks to such a
111
+ backend until the backend is updated.
112
+ - **Deepgram (the hosted Hartren API speech service) is the default, and browser speech is opt-in
113
+ only.** The SDK no longer falls back to the browser's Web Speech (`SpeechRecognition` /
114
+ `speechSynthesis`) under any failure. A host gets it only with `speechProvider: 'browser'`. Don't use
115
+ that setting with PHI: browser speech can send audio and text to the browser or OS vendor.
116
+
117
+ ### Changed
118
+
119
+ - The speech socket URL now defaults to the `copilotApi` origin (`https://api.example.com/api` →
120
+ `wss://api.example.com`). A relative `copilotApi` resolves against `window.location.origin`.
121
+ - Hosted speech failures reconnect with exponential backoff (`speechRetry`, default 3 retries,
122
+ 1 s base, 8 s cap). Each failure is reported through `onCopilotError` with a `code` from
123
+ `SpeechErrorCodes`.
124
+ - The token is sent only over `wss://`, or plain `ws://` to `localhost`, `127.0.0.1` or `[::1]`. Any
125
+ other `ws://` URL is refused before the token is read (`transcription-not-configured` /
126
+ `tts-not-configured`, no retry).
127
+ - A token that cannot be sent as a subprotocol fails once with `transcription-failed`,
128
+ `reason: 'invalid-token'`, with no retry.
129
+ - Microphone errors (ported from #30): `audio-setup-failed` with `reason` (for example `mic-denied`,
130
+ `mic-dismissed`, `mic-policy-blocked`, `mic-no-device`, `vad-asset-failed`), `detail` (the browser
131
+ error name) and `environment`. A session failure (`reason: 'session-failed'`) is retried.
132
+ - The token and token-bearing URLs are never logged.
133
+ - New exports: `speechProvider`, `SpeechErrorCodes`, `DEFAULT_SPEECH_RETRY`, `SPEECH_WS_PROTOCOL`,
134
+ `SPEECH_RECOGNITION_FAILURES`, and the types `CopilotError`, `SpeechProvider`, `SpeechRetryOptions`,
135
+ `SpeechErrorCode`, `SpeechRecognitionFailure`, `MicrophoneEnvironment`.
136
+
137
+ ### Notes
138
+
139
+ - 1.1.10 to 1.1.16 fell back to browser speech when the hosted socket failed. Don't roll back to
140
+ them in production.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hartren LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,274 @@
1
- # Temporary Holding Version
2
1
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
2
+ # @hartren/assistant-js
3
+
4
+ Browser client for the Hartren Assistant: dictation (speech socket) and the AI requests behind the Assistant and Reporting panels in the Hartren viewer and report builder.
5
+
6
+ ## Installation
7
+
8
+ Published on the public npm registry:
9
+
10
+ ```bash
11
+ npm install @hartren/assistant-js
12
+ ```
13
+
14
+ Previously published as `@hartren/copilotagent-js` (and before that `@radaid/copilotagent-js`). Use `@hartren/assistant-js` for 2.3.0 and later.
15
+
16
+
17
+ ## Development
18
+
19
+ ```bash
20
+ # from a checkout of this repository
21
+ npm ci
22
+ npm test
23
+ ```
24
+
25
+ ## Build
26
+ To compile and bundle the package into a single JavaScript file:
27
+ ```bash
28
+ yarn run build
29
+ ```
30
+
31
+ The output will be generated in the `dist/` directory.
32
+
33
+ ## Directory Structure
34
+ ```
35
+ /
36
+ ├── src/ # Source files
37
+ │ └── index.ts # Entry point for the package
38
+ ├── dist/ # Bundled output
39
+ ├── webpack.config.js # Webpack configuration
40
+ ├── package.json # Package configuration
41
+ ├── tsconfig.json # TypeScript configuration
42
+ ├── README.md # This README file
43
+ ```
44
+
45
+ ## Usage
46
+ ```ts
47
+ import CopilotAgent from '@hartren/assistant-js';
48
+ ```
49
+
50
+ ## Scripts
51
+ - **Build**: Compile TypeScript and bundle files:
52
+ ```bash
53
+ yarn run build
54
+ ```
55
+
56
+
57
+ ## How to Configure Package Locally
58
+
59
+
60
+ 1. **Unlink the package** (if it's already linked)
61
+ ```bash
62
+ yarn unlink @hartren/assistant-js
63
+ ```
64
+
65
+ 2. **Link the package**
66
+ ```bash
67
+ yarn dev
68
+ yarn link
69
+ yarn watch
70
+ ```
71
+
72
+ 3. **Go to another repo** where you want to configure this package
73
+
74
+ 4. **Remove the package** from `package.json`
75
+
76
+ 5. **Link the package** in the new repo
77
+ ```bash
78
+ yarn link @hartren/assistant-js
79
+ yarn dev
80
+ ```
81
+
82
+ ## Releasing
83
+
84
+ 1. Merge a PR that sets the new `version` in `package.json` and dates its `CHANGELOG.md` entry.
85
+ 2. A maintainer runs **Actions > Publish > Run workflow** on main with that version.
86
+ 3. The workflow checks that the version matches `package.json`, runs the tests and the build, and
87
+ publishes to npmjs (`--access public`) with npm Trusted Publishing or the `NPM_TOKEN` environment secret in `npm-publish`.
88
+
89
+ ## License
90
+ This project is licensed under the [MIT License](LICENSE).
91
+
92
+
93
+ ## Debug logging (off by default)
94
+
95
+ The agent never writes dictated text, prompts, AI replies or raw API responses to the browser
96
+ console unless you turn on debug logging. That text can be PHI, so do this for local debugging only:
97
+
98
+ ```js
99
+ new CopilotAgent({ ...options, debug: true });
100
+ // or, at any time:
101
+ import { setCopilotDebugLogging } from '@hartren/assistant-js';
102
+ setCopilotDebugLogging(true);
103
+ ```
104
+
105
+ ## Authentication
106
+
107
+ Pass the user's token through a provider. The agent asks for it on every API request and for the
108
+ speech socket, and keeps no copy. With `@hartren/auth`:
109
+
110
+ ```ts
111
+ new CopilotAgent({
112
+ copilotApi: 'https://api.hartren.com/api',
113
+ commands,
114
+ getAccessToken: () => auth.getAccessToken(), // resolve null when signed out
115
+ onUnauthorized: rejected => auth.handleUnauthorized(rejected), // optional: a different token to retry once
116
+ });
117
+ ```
118
+
119
+ On a `401` the request is retried once, and only with a different token. Without `onUnauthorized` the
120
+ agent asks `getAccessToken` again.
121
+
122
+ `pat` and `setUserId` (browser PAT exchange plus `/token/refresh`) are deprecated. They still work
123
+ when a PAT is passed and the provider is missing or returns nothing, and will be removed in a future major version.
124
+
125
+ ## Speech (dictation and voice replies)
126
+
127
+ > **BREAKING CHANGE in 2.0.0.** The speech token is sent as the WebSocket subprotocol
128
+ > `bearer.<token>`, next to `hartren.speech.v1`. This needs a backend that echoes
129
+ > `hartren.speech.v1` (the current Hartren API does). Against older backends, Chrome refuses the socket and dictation fails. Deepgram (hosted speech) is the
130
+ > default, and browser speech is opt-in only (`speechProvider: 'browser'`). See
131
+ > [CHANGELOG.md](CHANGELOG.md).
132
+
133
+ Dictation audio and spoken replies can contain PHI. By default the agent uses **only** the hosted
134
+ Hartren API speech service (Deepgram): `/ws/speech/transcribe` for dictation and `/ws/speech/speak`
135
+ for voice replies. Browsers cannot set headers on a WebSocket, so the user's access token is sent
136
+ as a WebSocket subprotocol: the socket offers `hartren.speech.v1` and `bearer.<token>` in the
137
+ `Sec-WebSocket-Protocol` header, and the server echoes only `hartren.speech.v1`. The token is never
138
+ put in the URL, so it cannot end up in access logs. This needs a Hartren API version that reads and
139
+ echoes the subprotocol (Chrome fails the handshake when a server does not echo one).
140
+
141
+ ```ts
142
+ new CopilotAgent({
143
+ copilotApi: 'https://api.hartren.com/api',
144
+ getAccessToken,
145
+ // Optional. Defaults to the copilotApi origin with a ws/wss scheme (wss://api.hartren.com).
146
+ transcribeWssUrl: 'wss://api.hartren.com',
147
+ // Optional reconnect policy for the transcription socket (these are the defaults).
148
+ speechRetry: { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 8000 },
149
+ });
150
+ ```
151
+
152
+ If the hosted service fails (handshake rejected, socket dropped, missing token or session, no URL),
153
+ the agent never falls back to browser speech by default (see
154
+ [Browser transcription fallback](#browser-transcription-fallback-opt-in) for the opt-in). Instead it reports the failure through
155
+ `onCopilotError` and reconnects with exponential backoff. Each reconnect raises
156
+ `code: 'transcription-retrying'` with `final: false`. When the retries run out, it raises
157
+ `code: 'transcription-failed'` with `final: true` and stops dictation. If hosted TTS fails it raises
158
+ `code: 'tts-failed'`; with no hosted TTS configured it raises `code: 'tts-not-configured'`. If the
159
+ transcription service sends an error on an open socket, it raises `code: 'transcription-error'` with
160
+ `final: false`. The full list is exported as `SpeechErrorCodes`.
161
+
162
+ **Log `code`, not `message`, from `onCopilotError`.** Most messages are fixed strings, but for
163
+ `transcription-error` the `message` is the server's own text, which can echo what was dictated. The
164
+ agent itself prints only a fixed line (the server text only with `debug: true`). Type the listener with
165
+ the exported `CopilotError`:
166
+
167
+ ```ts
168
+ import type { CopilotError } from '@hartren/assistant-js';
169
+ agent.onCopilotError((e: CopilotError) => console.warn('[Copilot] error:', e.code ?? 'unknown'));
170
+ ```
171
+
172
+ **The token is only sent over `wss://`.** A plain `ws://` speech URL is accepted only for
173
+ `localhost`, `127.0.0.1` or `[::1]` (local development). Any other `ws://` URL, including one derived
174
+ from an `http://` copilotApi, is refused before the token is read: no socket is opened, and the agent
175
+ raises `transcription-not-configured` (or `tts-not-configured` for voice replies) with no retry.
176
+
177
+ **Microphone problems** raise `code: 'audio-setup-failed'` with no retry. `reason` names the cause
178
+ (`mic-denied`, `mic-dismissed`, `mic-os-blocked`, `mic-policy-blocked`, `mic-no-device`, `mic-busy`,
179
+ `mic-constraints`, `mic-unsupported`, `mic-insecure-context`, `mic-failed`, `vad-asset-failed`),
180
+ `detail` is the browser's error name (never its raw message), and `environment` says what the browser
181
+ reported about microphone access. The messages are exported as `SPEECH_RECOGNITION_FAILURES`. A
182
+ session that cannot be started is reported as `reason: 'session-failed'` and is retried like a
183
+ connection failure.
184
+
185
+ **Browser speech is opt-in only.** `speechProvider: 'browser'` uses the browser's `SpeechRecognition`
186
+ and `speechSynthesis`. Those can send audio or text to the browser or OS vendor (for example Google
187
+ or Microsoft), and there is no BAA with them. Do not use this setting with PHI.
188
+
189
+ ### Browser transcription fallback (opt-in)
190
+
191
+ > **Warning.** When `browserTranscriptionFallback` is on, dictation audio may be processed by the
192
+ > browser vendor's speech service (for example Google for Chrome, Microsoft for Edge). There is no BAA
193
+ > with them. Only enable it where that is acceptable, never where dictation can contain PHI. Hartren's
194
+ > own deployments leave it off.
195
+
196
+ ```ts
197
+ new CopilotAgent({
198
+ copilotApi: 'https://api.example.com/api',
199
+ getAccessToken,
200
+ browserTranscriptionFallback: true, // default false
201
+ });
202
+ ```
203
+
204
+ `browserTranscriptionFallback` (boolean, default `false`) keeps dictation going on a spotty
205
+ connection by switching to the browser's `SpeechRecognition` / `webkitSpeechRecognition`. Only the
206
+ boolean `true` turns it on; any other value (including the string `'true'`) leaves it off. It applies
207
+ to the hosted (default) provider and to dictation only; voice replies stay hosted-only.
208
+
209
+ - **Off (default):** nothing changes. `SpeechRecognition` is never constructed, and hosted failures
210
+ retry and then raise `transcription-failed` as described above.
211
+ - **When it kicks in:** while dictation is running, on a hosted connection failure that would
212
+ otherwise be retried: the socket cannot be prepared or opened, the handshake is refused, the socket
213
+ drops mid-session, or the session cannot be started. The browser recognizer is created on the first
214
+ switch, not before. The agent raises `code: 'transcription-fallback'` (`final: false`, `reason`
215
+ such as `socket-closed` or `connect-failed`)
216
+ through `onCopilotError`, and `agent.isUsingBrowserTranscriptionFallback()` returns `true`.
217
+ - **Not for configuration or microphone errors:** a refused `ws://` URL, an unusable token and
218
+ `audio-setup-failed` are reported as before, with no fallback.
219
+ - **Offline behaves as without the option.** Chrome and Edge run Web Speech in the cloud, so it
220
+ cannot work offline either. The `offline` event never switches, and a hosted failure while
221
+ `navigator.onLine` is `false` retries and then raises `transcription-failed` as usual.
222
+ - **Going offline while the fallback is active pauses it.** The browser recognizer's `network`
223
+ errors are expected then, so it is not restarted and the "network hiccup" error is not raised
224
+ (one console warning, fixed text). Dictation resumes on the hosted service on `online`. While
225
+ online, a `network` error from browser speech keeps the normal restart-and-report behavior.
226
+ - **Handing back:** when the browser fires `online` during the dictation, the agent stops browser
227
+ speech, reconnects to the hosted service and raises `code: 'transcription-restored'`
228
+ (`final: false`). Every new `startListening()` tries the hosted service first. If the hosted
229
+ service fails again, it falls back again.
230
+ - **Unsupported browsers** (no `SpeechRecognition`, e.g. Firefox): no error from the fallback; the
231
+ hosted retries and the final `transcription-failed` happen as if the option were off.
232
+ - Browser speech needs a network connection itself (Chrome sends audio to Google), so the fallback
233
+ helps when the hosted service or its route is unreachable or flaky but the browser vendor is not.
234
+
235
+ ## License
236
+ This project is licensed under the [MIT License](LICENSE).
237
+
238
+ ## Authentication and the speech socket
239
+
240
+ - Get the user's token with `@hartren/auth` (which uses `POST /api/token/user-exchange`; your backend
241
+ does the first exchange with its integration token, the browser only refreshes) and pass
242
+ `getAccessToken: () => auth.getAccessToken()` and `onUnauthorized: (t) => auth.handleUnauthorized(t)`.
243
+ Don't use the legacy `/api/token/exchange` with `userId`; it is denied by default (`403`).
244
+ - Dictation opens `wss://{api host}/ws/speech/transcribe` offering two WebSocket subprotocols:
245
+ `hartren.speech.v1` and `bearer.<access token>`. The server echoes only `hartren.speech.v1`.
246
+ A handshake without a valid token gets `401`; a user who hasn't accepted the Terms gets
247
+ `403 tos_required`, and a user with report generation disabled gets `403`, both before the socket opens.
248
+
249
+ ## Server responses to handle (Hartren API)
250
+
251
+ Every API error body is JSON. Token endpoints use `{ "error", "error_description" }`; the other
252
+ endpoints use `application/problem+json` (`title`, `detail`, `status`).
253
+
254
+ | Status | When | What the client should do |
255
+ |---|---|---|
256
+ | `204 No Content` | Success with no body (for example deletes and some updates) | Treat as success; don't parse a body |
257
+ | `401` | Token missing, expired or revoked (and the speech handshake before the socket opens) | Get a fresh token once (`onUnauthorized` / `handleUnauthorized`), retry once, then sign the user in again |
258
+ | `403 tos_required` | The user hasn't accepted the current Terms of Use in this tenant. Returned on every API call and on the speech handshake | Read `GET /api/legal/status`, show the Terms (`GET /api/legal/tos`), then `POST /api/legal/accept` with the version. Don't retry the original call until then |
259
+ | `403` "Report generation is disabled for this user" | The organization turned report generation (and dictation) off for this user | Show the message; don't retry |
260
+ | `403` "Report limit reached for your plan" | The user's plan limit is used up | Show the message; don't retry |
261
+ | `403 access_denied` / `unauthorized_client` | `user-exchange`: the user can't get a token through your integration, or wrong token type / IP | Don't retry; check the user mapping or integration token |
262
+ | `409 tos_version_stale` | `POST /api/legal/accept` with an old version: the Terms changed | Fetch the current Terms, show them again, accept the new version |
263
+ | `409` (other endpoints) | A conflicting change (for example the item already exists or changed concurrently) | Reload, then retry once if it still applies |
264
+ | `429 slow_down` | Too many requests | Wait `Retry-After` seconds |
265
+ | `503 legal_storage_unavailable` | Terms status couldn't be read or written (fails closed) | Retry later |
266
+
267
+ Never retry `400`, `401` (after the one refresh), `403` or `409` in a loop.
268
+
269
+ ### Protected npm publishing
270
+
271
+ Store `NPM_TOKEN` as an environment secret in the main-only `npm-publish` GitHub
272
+ environment, never as a repository secret. Configure the npm trusted publisher
273
+ with environment name `npm-publish` (alongside this repository and `publish.yml`).
274
+ The first publish requires the short-lived token; subsequent publishing can use OIDC.