@verbumia/feedback 0.1.0
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/CONTRACT.md +165 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/chunk-5NA2TFPG.js +1 -0
- package/dist/chunk-5NA2TFPG.js.map +1 -0
- package/dist/chunk-OX4RJD5H.js +242 -0
- package/dist/chunk-OX4RJD5H.js.map +1 -0
- package/dist/client-CPEcvn23.d.cts +159 -0
- package/dist/client-CPEcvn23.d.ts +159 -0
- package/dist/core/index.cjs +272 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +18 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -0
- package/dist/keys-BySe1O6V.d.ts +25 -0
- package/dist/keys-Dg_nv16u.d.cts +25 -0
- package/dist/native/index.cjs +575 -0
- package/dist/native/index.cjs.map +1 -0
- package/dist/native/index.d.cts +54 -0
- package/dist/native/index.d.ts +54 -0
- package/dist/native/index.js +322 -0
- package/dist/native/index.js.map +1 -0
- package/dist/react/index.cjs +644 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +55 -0
- package/dist/react/index.d.ts +55 -0
- package/dist/react/index.js +384 -0
- package/dist/react/index.js.map +1 -0
- package/dist/svelte/index.cjs +306 -0
- package/dist/svelte/index.cjs.map +1 -0
- package/dist/svelte/index.d.cts +38 -0
- package/dist/svelte/index.d.ts +38 -0
- package/dist/svelte/index.js +52 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +426 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +39 -0
- package/dist/vue/index.d.ts +39 -0
- package/dist/vue/index.js +172 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +108 -0
package/CONTRACT.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# `@verbumia/feedback` — wire contract (v4 — task 616: tosVersion = SDK build-time constant)
|
|
2
|
+
|
|
3
|
+
Canonical reference for what the widget SDK sends and what the Verbumia
|
|
4
|
+
backend accepts. Entry points `/react`, `/native`, `/vue`, `/svelte`,
|
|
5
|
+
`/core` are interface-only differences; the WIRE shape (§1–§5) is
|
|
6
|
+
identical for all. Backend source-of-truth: `docs/feedback-api-contract.md`.
|
|
7
|
+
Supersedes the v3 artifact `b6324d39` (v2 `c8e86de1`, v1 `e614b48b`).
|
|
8
|
+
|
|
9
|
+
**v4 change (task 616):** `tosVersion` is NO LONGER an integrator config
|
|
10
|
+
field. It is a BUILD-TIME constant baked into the package
|
|
11
|
+
(`SDK_TOS_VERSION`, exported from `@verbumia/feedback/core`), sent
|
|
12
|
+
automatically on `POST /v1/feedback/tos`. Rationale: a stale (e.g.
|
|
13
|
+
native) app must record consent to the ToS version IT shipped, not
|
|
14
|
+
backend-latest. A ToS-TEXT change ⇒ bump `SDK_TOS_VERSION` + cut a new
|
|
15
|
+
SDK release (+ add the version to the backend acceptable set). The wire
|
|
16
|
+
body still carries `tos_version` (now SDK-sourced); the backend accepts
|
|
17
|
+
any KNOWN version, records it as-sent, and 422s `tos_version_unknown`
|
|
18
|
+
for unrecognized ones. No config field across ANY entry takes
|
|
19
|
+
`tosVersion`. Wire shape otherwise byte-identical to v3.
|
|
20
|
+
|
|
21
|
+
## 0b. Vue / Svelte entries (task 611 — adapters over /core)
|
|
22
|
+
|
|
23
|
+
`@verbumia/feedback/vue` and `@verbumia/feedback/svelte` are idiomatic
|
|
24
|
+
standalone adapters over the FROZEN `/core` `FeedbackClient` (the
|
|
25
|
+
host-i18n plugin-slot half is descoped — no `@verbumia/vue-i18n` /
|
|
26
|
+
`@verbumia/svelte-i18n` source). Same principles as the React plugin:
|
|
27
|
+
isolated open-state (never re-renders the host app), imperative
|
|
28
|
+
controller, server-minted sessionId (no client `groupingKey`).
|
|
29
|
+
|
|
30
|
+
- **/vue**: `createFeedback(config): { client, isOpen: Ref<boolean>,
|
|
31
|
+
controller: { open, close, client }, FeedbackPanel }`. `FeedbackPanel`
|
|
32
|
+
is a functional component (Teleport-to-body); mount once near root.
|
|
33
|
+
`config = { apiBase, projectId, language, endUserId?, keys?, fetchImpl? }` (NO tosVersion — build-time constant) (explicit — no i18n provider to inherit from).
|
|
34
|
+
- **/svelte**: `createFeedback(config): { client, isOpen:
|
|
35
|
+
Writable<boolean>, strings: Writable<FeedbackString[]>, open(),
|
|
36
|
+
close(), loadStrings(), rate(), suggest() }`. HEADLESS (idiomatic
|
|
37
|
+
Svelte stores; consumer renders its own panel). Same `config` shape.
|
|
38
|
+
|
|
39
|
+
`vue` / `svelte` are OPTIONAL peer deps (only the respective entry needs
|
|
40
|
+
them). The wire contract (§1–§5) is byte-identical to v2.
|
|
41
|
+
|
|
42
|
+
## 0. Integration architecture (task 599)
|
|
43
|
+
|
|
44
|
+
`@verbumia/feedback` is a **plugin of the `@verbumia/*-i18n`
|
|
45
|
+
provider** — NOT a separate React context/provider. The customer adds
|
|
46
|
+
`feedbackPlugin({ … })` to the i18n provider's `plugins`
|
|
47
|
+
slot. The provider calls the plugin's `setup({ i18n, config })` once
|
|
48
|
+
(the plugin reuses the provider's `apiBase` / `projectUuid` /
|
|
49
|
+
`defaultLocale` — no re-config) and mounts the plugin's `render()` as an
|
|
50
|
+
**isolated sibling leaf**. The panel's open/close lives in a private
|
|
51
|
+
store the outlet subscribes to, so enabling/opening feedback **never
|
|
52
|
+
re-renders the host app**. The host triggers the panel via its own CTA
|
|
53
|
+
through the imperative controller (`{ open, close }`) delivered via
|
|
54
|
+
`onReady` / `controllerRef`. No second provider is mounted.
|
|
55
|
+
|
|
56
|
+
Base URL: `FeedbackConfig.apiBase` (e.g. `https://api.verbumia.ca`),
|
|
57
|
+
trailing slashes trimmed. All bodies are `application/json`. Errors are
|
|
58
|
+
RFC 7807 problem+json with a stable `code`.
|
|
59
|
+
|
|
60
|
+
## 1. ToS acceptance → token bootstrap
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
POST <apiBase>/v1/feedback/tos
|
|
64
|
+
{
|
|
65
|
+
"project_id": "<project uuid>",
|
|
66
|
+
"end_user_id": "<opaque id | null>",
|
|
67
|
+
"tos_version": "2026-05-18",
|
|
68
|
+
"locale": "fr | null"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**sessionId / grouping_key is MINTED SERVER-SIDE (task 599).** The
|
|
73
|
+
client MUST NOT send or self-generate it — there is no
|
|
74
|
+
`grouping_key` request field and no `groupingKey` SDK config option.
|
|
75
|
+
The backend generates it (server-known + validated), returns it as
|
|
76
|
+
`grouping_key`, and binds it into the JWT. The SDK exposes it read-only
|
|
77
|
+
via `client.sessionId` after `acceptTos()`. A returning `end_user_id`
|
|
78
|
+
keeps its stable server value; a new end user gets a fresh
|
|
79
|
+
`sess_<uuid7>`.
|
|
80
|
+
|
|
81
|
+
`200` → `TokenBundle`:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
{ "access_token","token_type":"Bearer","expires_in",
|
|
85
|
+
"refresh_token","refresh_expires_in",
|
|
86
|
+
"end_user_id","tos_version","grouping_key" }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`403 end_user_eval_disabled` — add-on not on the project's plan.
|
|
90
|
+
`422 tos_version_unknown` — body carries `current_tos_version` + `acceptable_tos_versions` (SDK sends its build-time SDK_TOS_VERSION; integrator never sets it).
|
|
91
|
+
Not gated by a token (this call bootstraps one).
|
|
92
|
+
|
|
93
|
+
## 2. Token refresh (rotating)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
POST <apiBase>/v1/feedback/token/refresh
|
|
97
|
+
{ "refresh_token": "<opaque>" }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`200` → a fresh `TokenBundle` (refresh token is rotated; the old one is
|
|
101
|
+
invalidated). `401 feedback_refresh_invalid` → restart at §1.
|
|
102
|
+
|
|
103
|
+
## 3. Strings on the current view (Bearer)
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
GET <apiBase>/v1/feedback/strings?language=<bcp47>
|
|
107
|
+
[&keys=ns:key,ns:key][&namespace=<slug>][&limit=200]
|
|
108
|
+
Authorization: Bearer <access_token>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`200`:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
{ "project_id","language",
|
|
115
|
+
"strings":[ { "namespace","key","key_uuid","language_uuid",
|
|
116
|
+
"value","translation_hash",
|
|
117
|
+
"avg_stars": number|null,"ratings_count","my_rating": int|null } ] }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`translation_hash` is the rating target; echo it back unchanged in §4/§5.
|
|
121
|
+
|
|
122
|
+
## 4. Ratings (Bearer, batched)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
POST <apiBase>/v1/feedback/ratings
|
|
126
|
+
Authorization: Bearer <access_token>
|
|
127
|
+
{ "ratings":[ { "namespace","key","language","translation_hash","stars":1..5 } ] }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
1..50 items. Idempotent per (end user, key, language, hash) — last write
|
|
131
|
+
wins. `200` → `{ "accepted","rejected","items":[…] }`.
|
|
132
|
+
|
|
133
|
+
## 5. Suggestions (Bearer, batched)
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
POST <apiBase>/v1/feedback/suggestions
|
|
137
|
+
Authorization: Bearer <access_token>
|
|
138
|
+
{ "suggestions":[ { "namespace","key","language","translation_hash",
|
|
139
|
+
"suggested_text":"1..2000","comment":"<=500 | null" } ] }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
1..20 items. Created `status=pending`; nothing auto-publishes (customer
|
|
143
|
+
moderates). `200` → `{ "accepted","rejected","items":[ { "suggestion_id",… } ] }`.
|
|
144
|
+
|
|
145
|
+
## 6. Transport rules (ltm 280, mirrors the missing-handler)
|
|
146
|
+
|
|
147
|
+
- Ratings + suggestions are **queued**, then flushed on a debounce
|
|
148
|
+
(`flushDebounceMs`, default 1500 ms) or when the queue reaches
|
|
149
|
+
`maxBatch` (default 20), or on widget close.
|
|
150
|
+
- **Best-effort**: a transport or auth failure re-queues the batch once
|
|
151
|
+
and is swallowed — the SDK never throws into the host render path.
|
|
152
|
+
- On `401` the client performs **one** transparent
|
|
153
|
+
`/token/refresh` then retries the request; a failed refresh clears
|
|
154
|
+
consent and the widget returns to the §1 step.
|
|
155
|
+
- Outgoing batch posts carry `X-SDK: @verbumia/feedback@<ver>`.
|
|
156
|
+
|
|
157
|
+
## 7. Auth isolation
|
|
158
|
+
|
|
159
|
+
The end-user token is cryptographically separate from Verbumia customer
|
|
160
|
+
auth (distinct signing key + `iss`/`aud`); it only authorises
|
|
161
|
+
`/v1/feedback/*`. It is never sent to any other endpoint.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
Frozen 2026-05-18. Changes require a contract bump + notifying master and
|
|
165
|
+
client-admin.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Verbumia
|
|
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
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @verbumia/feedback
|
|
2
|
+
|
|
3
|
+
Embed a Verbumia **End-User Translation Evaluation** widget in your app.
|
|
4
|
+
Your end users accept a short ToS, then rate (5-star) and suggest
|
|
5
|
+
alternatives for the translations on screen. You moderate everything from
|
|
6
|
+
the Verbumia dashboard — nothing auto-publishes.
|
|
7
|
+
|
|
8
|
+
- `@verbumia/feedback/react` — React (web)
|
|
9
|
+
- `@verbumia/feedback/native` — React Native / Expo
|
|
10
|
+
- `@verbumia/feedback/core` — framework-agnostic client (advanced)
|
|
11
|
+
|
|
12
|
+
MIT. Add-on available from the **Pro** plan upward.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i @verbumia/feedback
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`react` (and `react-native` for the native entry) are peer deps.
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
`@verbumia/feedback` is a **plugin of your existing `@verbumia/*-i18n`
|
|
25
|
+
provider** — not a second context/provider. You add it to the i18n
|
|
26
|
+
provider's `plugins` slot; it reuses that provider's `apiBase` /
|
|
27
|
+
`projectUuid` / locale (no re-config) and never re-renders your app when
|
|
28
|
+
the panel opens. The sessionId is minted server-side — you don't supply
|
|
29
|
+
a cohort/session id.
|
|
30
|
+
|
|
31
|
+
## React
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { VerbumiaProvider } from "@verbumia/react-i18next";
|
|
35
|
+
import { feedbackPlugin } from "@verbumia/feedback/react";
|
|
36
|
+
|
|
37
|
+
const feedbackCtl = { current: null };
|
|
38
|
+
|
|
39
|
+
<VerbumiaProvider
|
|
40
|
+
token="vrb_live_…"
|
|
41
|
+
projectUuid="<project-uuid>"
|
|
42
|
+
defaultLocale="fr"
|
|
43
|
+
plugins={[
|
|
44
|
+
feedbackPlugin({
|
|
45
|
+
tosVersion: "2026-05-18",
|
|
46
|
+
keys: [{ namespace: "common", key: "home.title" }], // or omit → i18n registry
|
|
47
|
+
controllerRef: feedbackCtl, // imperative handle
|
|
48
|
+
}),
|
|
49
|
+
]}
|
|
50
|
+
>
|
|
51
|
+
<App />
|
|
52
|
+
</VerbumiaProvider>;
|
|
53
|
+
|
|
54
|
+
// trigger from your own CTA — no hook, no extra provider:
|
|
55
|
+
<button onClick={() => feedbackCtl.current?.open()}>Rate translations</button>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`feedbackPlugin` also accepts `onReady(controller)`, and optional
|
|
59
|
+
`apiBase` / `projectId` / `language` overrides (defaults come from the
|
|
60
|
+
i18n provider config).
|
|
61
|
+
|
|
62
|
+
## React Native / Expo
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { feedbackPlugin } from "@verbumia/feedback/native";
|
|
66
|
+
// same plugin API; renders a bottom-sheet <Modal> instead of a side panel.
|
|
67
|
+
// add it to your @verbumia/react-i18next provider's `plugins` slot.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Key discovery
|
|
71
|
+
|
|
72
|
+
If `@verbumia/*-i18n` is on the page it exposes a tiny key registry; the
|
|
73
|
+
widget reads the on-screen keys from it automatically. The explicit
|
|
74
|
+
`keys` prop always overrides it and is the reliable fallback.
|
|
75
|
+
|
|
76
|
+
## Behaviour
|
|
77
|
+
|
|
78
|
+
Ratings/suggestions are debounced + batched and sent best-effort — the
|
|
79
|
+
widget never blocks or breaks your app. See [CONTRACT.md](./CONTRACT.md)
|
|
80
|
+
for the exact wire shape.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-5NA2TFPG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/core/types.ts
|
|
2
|
+
var FeedbackError = class extends Error {
|
|
3
|
+
constructor(message, status, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "FeedbackError";
|
|
8
|
+
}
|
|
9
|
+
status;
|
|
10
|
+
code;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/core/tos.ts
|
|
14
|
+
var SDK_TOS_VERSION = "2026-05-18";
|
|
15
|
+
|
|
16
|
+
// src/core/client.ts
|
|
17
|
+
var SDK_LIB = "@verbumia/feedback";
|
|
18
|
+
var SDK_VER = "0.1.0";
|
|
19
|
+
var FeedbackClient = class {
|
|
20
|
+
cfg;
|
|
21
|
+
fetchImpl;
|
|
22
|
+
tokens = null;
|
|
23
|
+
queue = [];
|
|
24
|
+
timer = null;
|
|
25
|
+
bootstrapping = null;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.cfg = {
|
|
28
|
+
flushDebounceMs: 1500,
|
|
29
|
+
maxBatch: 20,
|
|
30
|
+
...config
|
|
31
|
+
};
|
|
32
|
+
const f = config.fetchImpl ?? globalThis.fetch;
|
|
33
|
+
if (!f) {
|
|
34
|
+
throw new FeedbackError(
|
|
35
|
+
"no fetch implementation available; pass config.fetchImpl"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
this.fetchImpl = f.bind(globalThis);
|
|
39
|
+
}
|
|
40
|
+
base() {
|
|
41
|
+
return this.cfg.apiBase.replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
get endUserId() {
|
|
44
|
+
return this.tokens?.end_user_id ?? this.cfg.endUserId;
|
|
45
|
+
}
|
|
46
|
+
get hasConsented() {
|
|
47
|
+
return this.tokens !== null;
|
|
48
|
+
}
|
|
49
|
+
/** Server-minted sessionId / grouping_key (from the token bundle).
|
|
50
|
+
* Available only after `acceptTos()`; never client-generated. */
|
|
51
|
+
get sessionId() {
|
|
52
|
+
return this.tokens?.grouping_key;
|
|
53
|
+
}
|
|
54
|
+
/** BCP-47 language the widget is rating strings in. */
|
|
55
|
+
get language() {
|
|
56
|
+
return this.cfg.language;
|
|
57
|
+
}
|
|
58
|
+
/** ToS version the end user is asked to accept — the SDK's
|
|
59
|
+
* build-time constant (task 616). NOT integrator/server set. */
|
|
60
|
+
get tosVersion() {
|
|
61
|
+
return SDK_TOS_VERSION;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Accept the ToS and bootstrap a scoped token. Idempotent: a second call
|
|
65
|
+
* returns the in-flight / existing bundle rather than re-accepting.
|
|
66
|
+
*/
|
|
67
|
+
async acceptTos() {
|
|
68
|
+
if (this.tokens) return this.tokens;
|
|
69
|
+
if (this.bootstrapping) return this.bootstrapping;
|
|
70
|
+
this.bootstrapping = (async () => {
|
|
71
|
+
const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
// NO grouping_key: server-minted (task 599). It comes back in
|
|
76
|
+
// the token bundle and is bound into the JWT server-side.
|
|
77
|
+
project_id: this.cfg.projectId,
|
|
78
|
+
end_user_id: this.cfg.endUserId,
|
|
79
|
+
tos_version: SDK_TOS_VERSION,
|
|
80
|
+
locale: this.cfg.locale
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) throw await this.problem(res, "tos acceptance failed");
|
|
84
|
+
this.tokens = await res.json();
|
|
85
|
+
return this.tokens;
|
|
86
|
+
})();
|
|
87
|
+
try {
|
|
88
|
+
return await this.bootstrapping;
|
|
89
|
+
} finally {
|
|
90
|
+
this.bootstrapping = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async refresh() {
|
|
94
|
+
if (!this.tokens) throw new FeedbackError("not consented");
|
|
95
|
+
const res = await this.fetchImpl(
|
|
96
|
+
`${this.base()}/v1/feedback/token/refresh`,
|
|
97
|
+
{
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify({ refresh_token: this.tokens.refresh_token })
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
this.tokens = null;
|
|
105
|
+
throw await this.problem(res, "token refresh failed");
|
|
106
|
+
}
|
|
107
|
+
this.tokens = await res.json();
|
|
108
|
+
}
|
|
109
|
+
/** Authenticated fetch with a single transparent refresh-on-401 retry. */
|
|
110
|
+
async authed(path, init, retry = true) {
|
|
111
|
+
if (!this.tokens) await this.acceptTos();
|
|
112
|
+
const res = await this.fetchImpl(`${this.base()}${path}`, {
|
|
113
|
+
...init,
|
|
114
|
+
headers: {
|
|
115
|
+
...init.headers ?? {},
|
|
116
|
+
Authorization: `Bearer ${this.tokens.access_token}`
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
if (res.status === 401 && retry) {
|
|
120
|
+
await this.refresh();
|
|
121
|
+
return this.authed(path, init, false);
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
/** Strings rendered on the current view, with this end user's prior rating. */
|
|
126
|
+
async getStrings(opts) {
|
|
127
|
+
const qs = new URLSearchParams({ language: this.cfg.language });
|
|
128
|
+
if (opts?.keys?.length) {
|
|
129
|
+
qs.set("keys", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(","));
|
|
130
|
+
}
|
|
131
|
+
if (opts?.namespace) qs.set("namespace", opts.namespace);
|
|
132
|
+
if (opts?.limit) qs.set("limit", String(opts.limit));
|
|
133
|
+
const res = await this.authed(`/v1/feedback/strings?${qs}`, {
|
|
134
|
+
method: "GET"
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) throw await this.problem(res, "failed to load strings");
|
|
137
|
+
return await res.json();
|
|
138
|
+
}
|
|
139
|
+
/** Queue a rating; flushed on debounce or when the batch fills. */
|
|
140
|
+
rate(payload) {
|
|
141
|
+
this.enqueue({ kind: "rating", payload });
|
|
142
|
+
}
|
|
143
|
+
/** Queue a suggestion; flushed on debounce or when the batch fills. */
|
|
144
|
+
suggest(payload) {
|
|
145
|
+
this.enqueue({ kind: "suggestion", payload });
|
|
146
|
+
}
|
|
147
|
+
enqueue(item) {
|
|
148
|
+
this.queue.push(item);
|
|
149
|
+
if (this.queue.length >= this.cfg.maxBatch) {
|
|
150
|
+
void this.flush();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.timer) clearTimeout(this.timer);
|
|
154
|
+
this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Flush queued items. Best-effort: a transport/auth failure re-queues the
|
|
158
|
+
* batch once and swallows the error so the host app never sees a throw.
|
|
159
|
+
*/
|
|
160
|
+
async flush() {
|
|
161
|
+
if (this.timer) {
|
|
162
|
+
clearTimeout(this.timer);
|
|
163
|
+
this.timer = null;
|
|
164
|
+
}
|
|
165
|
+
if (!this.queue.length) return;
|
|
166
|
+
const batch = this.queue;
|
|
167
|
+
this.queue = [];
|
|
168
|
+
const ratings = batch.filter((b) => b.kind === "rating").map((b) => b.payload);
|
|
169
|
+
const suggestions = batch.filter((b) => b.kind === "suggestion").map((b) => b.payload);
|
|
170
|
+
try {
|
|
171
|
+
if (ratings.length) {
|
|
172
|
+
await this.postBatch("/v1/feedback/ratings", { ratings });
|
|
173
|
+
}
|
|
174
|
+
if (suggestions.length) {
|
|
175
|
+
await this.postBatch("/v1/feedback/suggestions", { suggestions });
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
this.queue.unshift(...batch);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async postBatch(path, body) {
|
|
182
|
+
const res = await this.authed(path, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "Content-Type": "application/json", "X-SDK": `${SDK_LIB}@${SDK_VER}` },
|
|
185
|
+
body: JSON.stringify(body)
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) throw await this.problem(res, "batch post failed");
|
|
188
|
+
return await res.json();
|
|
189
|
+
}
|
|
190
|
+
async problem(res, fallback) {
|
|
191
|
+
let code;
|
|
192
|
+
let detail = fallback;
|
|
193
|
+
try {
|
|
194
|
+
const body = await res.json();
|
|
195
|
+
code = body.code;
|
|
196
|
+
if (body.detail) detail = body.detail;
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
return new FeedbackError(detail, res.status, code);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/core/keys.ts
|
|
204
|
+
var REGISTRY_GLOBAL = "__verbumia_key_registry__";
|
|
205
|
+
function getRegistry() {
|
|
206
|
+
const g = globalThis;
|
|
207
|
+
const reg = g[REGISTRY_GLOBAL];
|
|
208
|
+
if (reg && typeof reg.snapshot === "function") {
|
|
209
|
+
return reg;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function hasKeyRegistry() {
|
|
214
|
+
return getRegistry() !== null;
|
|
215
|
+
}
|
|
216
|
+
function resolveKeys(explicit) {
|
|
217
|
+
if (explicit && explicit.length) return dedupe(explicit);
|
|
218
|
+
const reg = getRegistry();
|
|
219
|
+
if (reg) return dedupe(reg.snapshot());
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
function dedupe(keys) {
|
|
223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const k of keys) {
|
|
226
|
+
const id = `${k.namespace}:${k.key}`;
|
|
227
|
+
if (!seen.has(id)) {
|
|
228
|
+
seen.add(id);
|
|
229
|
+
out.push(k);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
FeedbackError,
|
|
237
|
+
SDK_TOS_VERSION,
|
|
238
|
+
FeedbackClient,
|
|
239
|
+
hasKeyRegistry,
|
|
240
|
+
resolveKeys
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=chunk-OX4RJD5H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/types.ts","../src/core/tos.ts","../src/core/client.ts","../src/core/keys.ts"],"sourcesContent":["/**\n * Wire types for the Verbumia End-User Translation Evaluation API.\n * Canonical reference: ./CONTRACT.md (frozen). Backend task 591.\n */\n\nexport interface FeedbackConfig {\n /** API base, no trailing slash needed. e.g. https://api.verbumia.ca */\n apiBase: string;\n /** Public project UUID the widget targets. */\n projectId: string;\n /**\n * sessionId / grouping_key is MINTED SERVER-SIDE by the Verbumia\n * backend at POST /v1/feedback/tos and returned in the token bundle\n * (`TokenBundle.grouping_key`). The widget receives + sends it; it\n * MUST NOT self-generate it. There is intentionally no client config\n * field for it.\n */\n /**\n * NOTE: there is intentionally NO `tosVersion` field. The ToS version\n * is a BUILD-TIME constant baked into @verbumia/feedback at release\n * (`SDK_TOS_VERSION`, task 616) — integrators do not set it; the SDK\n * sends it automatically.\n */\n /** Optional opaque end-user id; server generates one when absent. */\n endUserId?: string;\n /** BCP-47 language the widget rates strings in (e.g. \"fr\"). */\n language: string;\n /** Optional locale tag stored for analytics. */\n locale?: string;\n /** Debounce window (ms) before a queued batch is flushed. Default 1500. */\n flushDebounceMs?: number;\n /** Max queued items before an immediate flush. Default 20. */\n maxBatch?: number;\n /** Injected fetch (tests / RN polyfills). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface TokenBundle {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n refresh_expires_in: number;\n end_user_id: string;\n tos_version: string;\n grouping_key: string;\n}\n\nexport interface FeedbackString {\n namespace: string;\n key: string;\n key_uuid: string;\n language_uuid: string;\n value: string;\n translation_hash: string;\n avg_stars: number | null;\n ratings_count: number;\n my_rating: number | null;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/** A 5-star rating queued for a rendered string variant. */\nexport interface RatingInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n stars: number; // 1..5\n}\n\n/** A suggested alternative translation queued for moderation. */\nexport interface SuggestionInput {\n namespace: string;\n key: string;\n language: string;\n translation_hash: string;\n suggested_text: string;\n comment?: string;\n}\n\nexport interface BatchResponse {\n accepted: number;\n rejected: number;\n items: Array<Record<string, unknown>>;\n}\n\n/** A key the host app declares as on-screen. */\nexport interface DeclaredKey {\n namespace: string;\n key: string;\n}\n\nexport class FeedbackError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"FeedbackError\";\n }\n}\n","/**\n * BUILD-TIME ToS version constant (task 616, human decision option B).\n *\n * This is baked into the @verbumia/feedback PACKAGE at release. It is\n * NOT an integrator-set config field and NOT server-driven: a stale\n * (e.g. native) app must record consent to the ToS version IT shipped\n * and displayed, for legal correctness — never backend-latest.\n *\n * Releasing a ToS-TEXT change ⇒ bump this constant + cut a new\n * @verbumia/feedback release (per the human/real-CI publish handoff).\n * The backend accepts any version in its acceptable set and records it.\n */\nexport const SDK_TOS_VERSION = \"2026-05-18\";\n","/**\n * Framework-agnostic Verbumia feedback client.\n *\n * Responsibilities:\n * - versioned-ToS acceptance -> scoped end-user JWT bootstrap\n * - transparent access-token refresh (rotating refresh token)\n * - fetch on-screen strings for the widget\n * - debounced + size-capped batched POST of ratings / suggestions,\n * mirroring the missing-handler transport contract (ltm 280):\n * best-effort, never throws into the host app render path.\n *\n * The React and React Native entry points are thin UI shells over this.\n */\n\nimport {\n type BatchResponse,\n type FeedbackConfig,\n FeedbackError,\n type RatingInput,\n type StringsResponse,\n type SuggestionInput,\n type TokenBundle,\n} from \"./types\";\nimport { SDK_TOS_VERSION } from \"./tos\";\n\nconst SDK_LIB = \"@verbumia/feedback\";\nconst SDK_VER = \"0.1.0\";\n\ntype QueueItem =\n | { kind: \"rating\"; payload: RatingInput }\n | { kind: \"suggestion\"; payload: SuggestionInput };\n\nexport class FeedbackClient {\n private cfg: Required<\n Pick<FeedbackConfig, \"flushDebounceMs\" | \"maxBatch\">\n > &\n FeedbackConfig;\n private fetchImpl: typeof fetch;\n private tokens: TokenBundle | null = null;\n private queue: QueueItem[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private bootstrapping: Promise<TokenBundle> | null = null;\n\n constructor(config: FeedbackConfig) {\n this.cfg = {\n flushDebounceMs: 1500,\n maxBatch: 20,\n ...config,\n };\n const f = config.fetchImpl ?? globalThis.fetch;\n if (!f) {\n throw new FeedbackError(\n \"no fetch implementation available; pass config.fetchImpl\",\n );\n }\n this.fetchImpl = f.bind(globalThis);\n }\n\n private base(): string {\n return this.cfg.apiBase.replace(/\\/+$/, \"\");\n }\n\n get endUserId(): string | undefined {\n return this.tokens?.end_user_id ?? this.cfg.endUserId;\n }\n\n get hasConsented(): boolean {\n return this.tokens !== null;\n }\n\n /** Server-minted sessionId / grouping_key (from the token bundle).\n * Available only after `acceptTos()`; never client-generated. */\n get sessionId(): string | undefined {\n return this.tokens?.grouping_key;\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /** ToS version the end user is asked to accept — the SDK's\n * build-time constant (task 616). NOT integrator/server set. */\n get tosVersion(): string {\n return SDK_TOS_VERSION;\n }\n\n /**\n * Accept the ToS and bootstrap a scoped token. Idempotent: a second call\n * returns the in-flight / existing bundle rather than re-accepting.\n */\n async acceptTos(): Promise<TokenBundle> {\n if (this.tokens) return this.tokens;\n if (this.bootstrapping) return this.bootstrapping;\n this.bootstrapping = (async () => {\n const res = await this.fetchImpl(`${this.base()}/v1/feedback/tos`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n // NO grouping_key: server-minted (task 599). It comes back in\n // the token bundle and is bound into the JWT server-side.\n project_id: this.cfg.projectId,\n end_user_id: this.cfg.endUserId,\n tos_version: SDK_TOS_VERSION,\n locale: this.cfg.locale,\n }),\n });\n if (!res.ok) throw await this.problem(res, \"tos acceptance failed\");\n this.tokens = (await res.json()) as TokenBundle;\n return this.tokens;\n })();\n try {\n return await this.bootstrapping;\n } finally {\n this.bootstrapping = null;\n }\n }\n\n private async refresh(): Promise<void> {\n if (!this.tokens) throw new FeedbackError(\"not consented\");\n const res = await this.fetchImpl(\n `${this.base()}/v1/feedback/token/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: this.tokens.refresh_token }),\n },\n );\n if (!res.ok) {\n this.tokens = null; // force a fresh ToS step\n throw await this.problem(res, \"token refresh failed\");\n }\n this.tokens = (await res.json()) as TokenBundle;\n }\n\n /** Authenticated fetch with a single transparent refresh-on-401 retry. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) await this.acceptTos();\n const res = await this.fetchImpl(`${this.base()}${path}`, {\n ...init,\n headers: {\n ...(init.headers ?? {}),\n Authorization: `Bearer ${this.tokens!.access_token}`,\n },\n });\n if (res.status === 401 && retry) {\n await this.refresh();\n return this.authed(path, init, false);\n }\n return res;\n }\n\n /** Strings rendered on the current view, with this end user's prior rating. */\n async getStrings(opts?: {\n keys?: Array<{ namespace: string; key: string }>;\n namespace?: string;\n limit?: number;\n }): Promise<StringsResponse> {\n const qs = new URLSearchParams({ language: this.cfg.language });\n if (opts?.keys?.length) {\n qs.set(\"keys\", opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\"));\n }\n if (opts?.namespace) qs.set(\"namespace\", opts.namespace);\n if (opts?.limit) qs.set(\"limit\", String(opts.limit));\n const res = await this.authed(`/v1/feedback/strings?${qs}`, {\n method: \"GET\",\n });\n if (!res.ok) throw await this.problem(res, \"failed to load strings\");\n return (await res.json()) as StringsResponse;\n }\n\n /** Queue a rating; flushed on debounce or when the batch fills. */\n rate(payload: RatingInput): void {\n this.enqueue({ kind: \"rating\", payload });\n }\n\n /** Queue a suggestion; flushed on debounce or when the batch fills. */\n suggest(payload: SuggestionInput): void {\n this.enqueue({ kind: \"suggestion\", payload });\n }\n\n private enqueue(item: QueueItem): void {\n this.queue.push(item);\n if (this.queue.length >= this.cfg.maxBatch) {\n void this.flush();\n return;\n }\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => void this.flush(), this.cfg.flushDebounceMs);\n }\n\n /**\n * Flush queued items. Best-effort: a transport/auth failure re-queues the\n * batch once and swallows the error so the host app never sees a throw.\n */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (!this.queue.length) return;\n const batch = this.queue;\n this.queue = [];\n const ratings = batch\n .filter((b) => b.kind === \"rating\")\n .map((b) => b.payload as RatingInput);\n const suggestions = batch\n .filter((b) => b.kind === \"suggestion\")\n .map((b) => b.payload as SuggestionInput);\n try {\n if (ratings.length) {\n await this.postBatch(\"/v1/feedback/ratings\", { ratings });\n }\n if (suggestions.length) {\n await this.postBatch(\"/v1/feedback/suggestions\", { suggestions });\n }\n } catch {\n // Re-queue once; feedback must never break the host app.\n this.queue.unshift(...batch);\n }\n }\n\n private async postBatch(\n path: string,\n body: unknown,\n ): Promise<BatchResponse> {\n const res = await this.authed(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-SDK\": `${SDK_LIB}@${SDK_VER}` },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw await this.problem(res, \"batch post failed\");\n return (await res.json()) as BatchResponse;\n }\n\n private async problem(res: Response, fallback: string): Promise<FeedbackError> {\n let code: string | undefined;\n let detail = fallback;\n try {\n const body = (await res.json()) as { code?: string; detail?: string };\n code = body.code;\n if (body.detail) detail = body.detail;\n } catch {\n /* non-JSON body */\n }\n return new FeedbackError(detail, res.status, code);\n }\n}\n","/**\n * On-screen key discovery.\n *\n * Preferred source: the `@verbumia/*-i18n` SDK exposes a lightweight key\n * registry of keys it has rendered. When that registry is present on the\n * global (the i18n SDK publishes `globalThis.__verbumia_key_registry__`),\n * we read the keys touched on the current view. Otherwise the host app\n * passes an explicit `keys` list — the always-available fallback.\n *\n * The registry contract is intentionally tiny so any framework port of the\n * i18n SDK can implement it without depending on this package.\n */\n\nimport type { DeclaredKey } from \"./types\";\n\nconst REGISTRY_GLOBAL = \"__verbumia_key_registry__\";\n\ninterface KeyRegistry {\n /** Returns the keys rendered since the last `reset()` (or ever). */\n snapshot(): DeclaredKey[];\n reset?(): void;\n}\n\nfunction getRegistry(): KeyRegistry | null {\n const g = globalThis as Record<string, unknown>;\n const reg = g[REGISTRY_GLOBAL];\n if (\n reg &&\n typeof (reg as KeyRegistry).snapshot === \"function\"\n ) {\n return reg as KeyRegistry;\n }\n return null;\n}\n\n/** True when a compatible `@verbumia/*-i18n` registry is detectable. */\nexport function hasKeyRegistry(): boolean {\n return getRegistry() !== null;\n}\n\n/**\n * Resolve the on-screen keys: explicit `keys` prop always wins (it is the\n * customer's authoritative declaration); otherwise fall back to the i18n\n * registry snapshot; otherwise an empty list (widget shows \"no strings\").\n */\nexport function resolveKeys(explicit?: DeclaredKey[]): DeclaredKey[] {\n if (explicit && explicit.length) return dedupe(explicit);\n const reg = getRegistry();\n if (reg) return dedupe(reg.snapshot());\n return [];\n}\n\nfunction dedupe(keys: DeclaredKey[]): DeclaredKey[] {\n const seen = new Set<string>();\n const out: DeclaredKey[] = [];\n for (const k of keys) {\n const id = `${k.namespace}:${k.key}`;\n if (!seen.has(id)) {\n seen.add(id);\n out.push(k);\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|