@verbumia/feedback 0.2.6 → 0.2.8
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/dist/chunk-EKU6NNWI.js +40 -0
- package/dist/chunk-EKU6NNWI.js.map +1 -0
- package/dist/{chunk-5RTFWOGT.js → chunk-EMGN6MR4.js} +87 -3
- package/dist/chunk-EMGN6MR4.js.map +1 -0
- package/dist/{client-qgDSbz3A.d.cts → client-CnEK_2SD.d.cts} +114 -1
- package/dist/{client-qgDSbz3A.d.ts → client-CnEK_2SD.d.ts} +114 -1
- package/dist/core/index.cjs +86 -2
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{keys-BhuK_fy1.d.cts → keys-2_T5bDpX.d.cts} +1 -1
- package/dist/{keys-CEWu0Htb.d.ts → keys-eHc_lx5v.d.ts} +1 -1
- package/dist/native/index.cjs +317 -48
- package/dist/native/index.cjs.map +1 -1
- package/dist/native/index.d.cts +48 -4
- package/dist/native/index.d.ts +48 -4
- package/dist/native/index.js +200 -48
- package/dist/native/index.js.map +1 -1
- package/dist/react/index.cjs +319 -41
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +119 -4
- package/dist/react/index.d.ts +119 -4
- package/dist/react/index.js +202 -41
- package/dist/react/index.js.map +1 -1
- package/dist/svelte/index.cjs +86 -2
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.d.cts +2 -2
- package/dist/svelte/index.d.ts +2 -2
- package/dist/svelte/index.js +1 -1
- package/dist/vue/index.cjs +86 -2
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.cts +2 -2
- package/dist/vue/index.d.ts +2 -2
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5RTFWOGT.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/svelte/index.ts","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/core/keys.ts"],"sourcesContent":["/**\n * `@verbumia/feedback/svelte` — idiomatic Svelte adapter over the FROZEN\n * /core FeedbackClient (task 611, master-approved). Host-i18n\n * plugin-slot half is descoped (no @verbumia/svelte-i18n source), so\n * this is a standalone HEADLESS store adapter (idiomatic Svelte = stores;\n * the consumer renders its own panel — keeps the published build pure-TS,\n * no Svelte compiler needed). ISOLATED open-state (a per-instance\n * writable — toggling it never re-renders the host app). sessionId is\n * server-minted (no client groupingKey). Mirrors React-plugin principles.\n */\nimport { writable, type Writable } from \"svelte/store\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type {\n DeclaredKey,\n FeedbackString,\n RatingInput,\n SuggestionInput,\n} from \"../core/types\";\n\nexport interface SvelteFeedbackConfig {\n apiBase: string;\n projectId: string;\n language: string;\n // No tosVersion — SDK build-time constant (SDK_TOS_VERSION, task 616).\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface SvelteFeedback {\n client: FeedbackClient;\n /** Isolated open-state store. Bind your own panel to it. */\n isOpen: Writable<boolean>;\n /** Strings store — populated by loadStrings(). */\n strings: Writable<FeedbackString[]>;\n open: () => Promise<void>;\n close: () => void;\n loadStrings: () => Promise<void>;\n rate: (r: RatingInput) => void;\n suggest: (s: SuggestionInput) => void;\n}\n\nexport function createFeedback(config: SvelteFeedbackConfig): SvelteFeedback {\n const client = new FeedbackClient({\n apiBase: config.apiBase,\n projectId: config.projectId,\n language: config.language,\n endUserId: config.endUserId,\n fetchImpl: config.fetchImpl,\n });\n const isOpen = writable(false);\n const strings = writable<FeedbackString[]>([]);\n\n async function loadStrings(): Promise<void> {\n if (!client.hasConsented) await client.acceptTos();\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): ONLY\n // rendered keys; empty -> show none, never fetch the whole project.\n const resolved = resolveKeys(config.keys, config.namespace);\n if (!resolved.length) {\n strings.set([]);\n return;\n }\n const r = await client.getStrings({ keys: resolved });\n strings.set(r.strings);\n }\n\n return {\n client,\n isOpen,\n strings,\n async open() {\n isOpen.set(true);\n try {\n await loadStrings();\n } catch {\n /* best-effort — never break the host app */\n }\n },\n close() {\n isOpen.set(false);\n void client.flush();\n },\n loadStrings,\n rate: (r) => client.rate(r),\n suggest: (s) => client.suggest(s),\n };\n}\n\nexport { FeedbackClient } from \"../core/client\";\nexport type {\n DeclaredKey, FeedbackString, RatingInput, SuggestionInput,\n TokenBundle, StringsResponse, BatchResponse,\n} from \"../core/types\";\nexport { FeedbackError } from \"../core/types\";\n","/**\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.dev */\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.2.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 /**\n * Re-target the widget's working language at runtime (#806 SeedSower\n * lang-change propagation). Subsequent `getStrings` / `rate` /\n * `suggest` use the new language without rebuilding the client. The\n * server-minted ToS bundle has no `locale` field, so a language flip\n * does NOT invalidate consent — only the next `?language=` query\n * needs updating. The i18n provider plugin (feedback ≥0.2.6)\n * subscribes to `@verbumia/react-i18next` ≥1.0.5's\n * `VerbumiaPluginContext.onLanguageChange` and calls this on every\n * runtime language change so the host never has to wire it manually.\n */\n setLanguage(lang: string): void {\n if (typeof lang !== \"string\" || lang.length === 0) return;\n if (lang === this.cfg.language) return;\n this.cfg = { ...this.cfg, language: lang };\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 // #806 SeedSower P1 (Hermes URLSearchParams polyfill): React Native's\n // bundled URLSearchParams implements ONLY the constructor + toString().\n // `.set` / `.get` / `.append` / `.delete` / `.has` / `.entries` /\n // `.forEach` / `.keys` / `.values` / the iterator all throw on Hermes\n // (documented RN limitation since ~0.50, refused for bundle-size).\n // Build a plain Record<string, string> first, then pass to the\n // constructor — that variant is supported on Node + browser + Hermes\n // + JSC, and produces the same query string. NO mutation after\n // construction anywhere in the SDK.\n const params: Record<string, string> = { language: this.cfg.language };\n if (opts?.keys?.length) {\n params.keys = opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\");\n }\n if (opts?.namespace) params.namespace = opts.namespace;\n if (opts?.limit) params.limit = String(opts.limit);\n const qs = new URLSearchParams(params);\n const res = await this.authed(`/v1/feedback/strings?${qs.toString()}`, {\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 *\n * Optional `namespace` filter (task 618, CONTRACT v6 — ADDITIVE): when\n * set (a single namespace or a list), the resolved set is narrowed to\n * keys whose `namespace` is in the filter. The filter is applied AFTER\n * resolution, so it composes with v5 rendered-scoping as\n * `rendered ∩ namespace`. UNSET / empty ⇒ unchanged v5 behaviour (all\n * resolved keys). Backward-compatible: `resolveKeys(keys)` is unaffected.\n */\nexport function resolveKeys(\n explicit?: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const base =\n explicit && explicit.length\n ? dedupe(explicit)\n : getRegistry()\n ? dedupe(getRegistry()!.snapshot())\n : [];\n return filterByNamespace(base, namespace);\n}\n\n/** Narrow keys to those whose namespace is in `namespace`. An undefined\n * filter, an empty string, or an empty list means \"no filter\" (the\n * additive default — identical to v5). Exported so non-panel callers\n * (e.g. a custom /core integration) can apply the same semantics. */\nexport function filterByNamespace(\n keys: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const list = (Array.isArray(namespace) ? namespace : namespace ? [namespace] : [])\n .map((n) => n.trim())\n .filter(Boolean);\n if (!list.length) return keys;\n const allow = new Set(list);\n return keys.filter((k) => allow.has(k.namespace));\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/svelte/index.ts","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/core/keys.ts"],"sourcesContent":["/**\n * `@verbumia/feedback/svelte` — idiomatic Svelte adapter over the FROZEN\n * /core FeedbackClient (task 611, master-approved). Host-i18n\n * plugin-slot half is descoped (no @verbumia/svelte-i18n source), so\n * this is a standalone HEADLESS store adapter (idiomatic Svelte = stores;\n * the consumer renders its own panel — keeps the published build pure-TS,\n * no Svelte compiler needed). ISOLATED open-state (a per-instance\n * writable — toggling it never re-renders the host app). sessionId is\n * server-minted (no client groupingKey). Mirrors React-plugin principles.\n */\nimport { writable, type Writable } from \"svelte/store\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type {\n DeclaredKey,\n FeedbackString,\n RatingInput,\n SuggestionInput,\n} from \"../core/types\";\n\nexport interface SvelteFeedbackConfig {\n apiBase: string;\n projectId: string;\n language: string;\n // No tosVersion — SDK build-time constant (SDK_TOS_VERSION, task 616).\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface SvelteFeedback {\n client: FeedbackClient;\n /** Isolated open-state store. Bind your own panel to it. */\n isOpen: Writable<boolean>;\n /** Strings store — populated by loadStrings(). */\n strings: Writable<FeedbackString[]>;\n open: () => Promise<void>;\n close: () => void;\n loadStrings: () => Promise<void>;\n rate: (r: RatingInput) => void;\n suggest: (s: SuggestionInput) => void;\n}\n\nexport function createFeedback(config: SvelteFeedbackConfig): SvelteFeedback {\n const client = new FeedbackClient({\n apiBase: config.apiBase,\n projectId: config.projectId,\n language: config.language,\n endUserId: config.endUserId,\n fetchImpl: config.fetchImpl,\n });\n const isOpen = writable(false);\n const strings = writable<FeedbackString[]>([]);\n\n async function loadStrings(): Promise<void> {\n if (!client.hasConsented) await client.acceptTos();\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5): ONLY\n // rendered keys; empty -> show none, never fetch the whole project.\n const resolved = resolveKeys(config.keys, config.namespace);\n if (!resolved.length) {\n strings.set([]);\n return;\n }\n const r = await client.getStrings({ keys: resolved });\n strings.set(r.strings);\n }\n\n return {\n client,\n isOpen,\n strings,\n async open() {\n isOpen.set(true);\n try {\n await loadStrings();\n } catch {\n /* best-effort — never break the host app */\n }\n },\n close() {\n isOpen.set(false);\n void client.flush();\n },\n loadStrings,\n rate: (r) => client.rate(r),\n suggest: (s) => client.suggest(s),\n };\n}\n\nexport { FeedbackClient } from \"../core/client\";\nexport type {\n DeclaredKey, FeedbackString, RatingInput, SuggestionInput,\n TokenBundle, StringsResponse, BatchResponse,\n} from \"../core/types\";\nexport { FeedbackError } from \"../core/types\";\n","/**\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.dev */\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 * 0.2.7 — host's project API key (`vrb_live_…`, scope `project:read`).\n * When set, the client can call addon-state at setup time (BEFORE\n * the end-user accepts ToS) using `Authorization: ApiKey …` — mirrors\n * the existing scheme used by the i18n SDK's dev-env loaders. When\n * unset, addon-state is deferred until the user's session bearer is\n * minted via `acceptTos()`.\n */\n apiKey?: string;\n /**\n * #806 / 0.2.7 — when `false`, the client must NOT silently call\n * `POST /v1/feedback/tos` to bootstrap a session on the user's behalf.\n * Set by the plugin when the host opts into `tos: \"skip\"` (the host\n * promises to handle consent externally via `controller.acceptTos()`).\n * `authed()` then throws `FeedbackError(\"not consented\")` instead of\n * auto-accepting — the panel surfaces the existing error state.\n * Default `true` preserves all pre-0.2.7 behaviour.\n */\n autoAcceptTos?: boolean;\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 * 0.2.8 — backend deploy `45190c8` (task 846) adds source-language\n * fields so the panel can render the source-locale text alongside\n * the target-locale text when the user toggles \"Show original\".\n *\n * - `source_text`: the source-locale rendering for this key\n * (== `value` whenever `source_locale === language` — the panel\n * uses that equality to hide the source row and avoid\n * duplication on a single-language project).\n * - `source_locale`: BCP-47 of the source.\n * - `my_suggestion`: the end-user's pending suggestion for this\n * key in this language (or `null`). Pre-fills the suggestion\n * editor + flips the submit path from POST → PATCH.\n */\n source_text: string;\n source_locale: string;\n my_suggestion: MySuggestion | null;\n}\n\n/** 0.2.8 — end-user's pending suggestion for one (key, language).\n * Returned inline on `FeedbackString` so the panel can render an\n * edit-mode editor without a second round-trip. */\nexport interface MySuggestion {\n /** Suggestion UUID; passed to `PATCH /v1/feedback/suggestions/{id}` to\n * update the text. */\n id: string;\n text: string;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/**\n * 0.2.7 — addon state for a project, returned by\n * `GET /v1/projects/{projectId}/feedback-addon/state`. Drives the\n * controller surface (`isActive`, `enabledLanguages`, `sku`,\n * `languagesLimit`) so the host can hide its own CTA on Starter +\n * narrow the widget to enabled languages.\n *\n * Backend contract (camelCase aliases, populate_by_name=true on the\n * server schema, confirmed against backend task 836 SHA d78b939 + the\n * follow-up PR that extends the auth surface to accept project API\n * keys with `project:read` scope):\n * - `isActive`: org.feedback_addon.active\n * - `sku`: the SKU code, or `null` if no active SKU\n * - `enabledLanguages`: the project's allow-list, or literal `\"all\"`\n * when sku === `\"feedback_unlimited\"`; an empty array when no SKU\n * - `languagesLimit`: hard cap for Starter (3), null otherwise\n */\nexport interface FeedbackAddonState {\n isActive: boolean;\n sku: \"feedback_starter\" | \"feedback_unlimited\" | null;\n enabledLanguages: string[] | \"all\";\n languagesLimit: number | null;\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 FeedbackAddonState,\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.2.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 /** Alias of `hasConsented` exposed under the 0.2.7 naming used by the\n * `controller.hasAcceptedTos` getter (the SDK's built-in modal and a\n * host's external ToS page share the same persisted token bundle, so\n * both flip this boolean once a session is bootstrapped). */\n get hasAcceptedTos(): 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 /**\n * 0.2.7 — `GET /v1/projects/{projectId}/feedback-addon/state`.\n *\n * Auth selection (matches backend's dual-acceptance after task 836's\n * follow-up PR — see CONTRACT note in the response type):\n * - If `cfg.apiKey` is set → `Authorization: ApiKey <key>` (so the\n * plugin can fetch state at `setup()` time, before the user has\n * accepted ToS).\n * - Else if a user-session bundle exists → `Authorization: Bearer\n * <access_token>` (the deferred-after-acceptTos path).\n * - Else → throw `FeedbackError(\"addon state requires apiKey or\n * acceptTos\")`. The plugin's setup catches and surfaces a\n * console.warn the first time, then re-attempts on the\n * bundle-mint that follows `acceptTos()`.\n *\n * Best-effort: a transport error returns `null` instead of throwing,\n * so the controller's `isActive` falls back to whatever was last\n * known (or its initial value) without flipping the host's CTA into\n * an inconsistent state on a transient blip.\n */\n async getAddonState(): Promise<FeedbackAddonState | null> {\n let auth: string | undefined;\n if (this.cfg.apiKey) {\n auth = `ApiKey ${this.cfg.apiKey}`;\n } else if (this.tokens) {\n auth = `Bearer ${this.tokens.access_token}`;\n } else {\n throw new FeedbackError(\n \"addon state requires apiKey or acceptTos\",\n );\n }\n const url = `${this.base()}/v1/projects/${this.cfg.projectId}/feedback-addon/state`;\n try {\n const res = await this.fetchImpl(url, {\n method: \"GET\",\n headers: { Authorization: auth },\n });\n if (!res.ok) return null;\n return (await res.json()) as FeedbackAddonState;\n } catch {\n return null;\n }\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /**\n * Re-target the widget's working language at runtime (#806 SeedSower\n * lang-change propagation). Subsequent `getStrings` / `rate` /\n * `suggest` use the new language without rebuilding the client. The\n * server-minted ToS bundle has no `locale` field, so a language flip\n * does NOT invalidate consent — only the next `?language=` query\n * needs updating. The i18n provider plugin (feedback ≥0.2.6)\n * subscribes to `@verbumia/react-i18next` ≥1.0.5's\n * `VerbumiaPluginContext.onLanguageChange` and calls this on every\n * runtime language change so the host never has to wire it manually.\n */\n setLanguage(lang: string): void {\n if (typeof lang !== \"string\" || lang.length === 0) return;\n if (lang === this.cfg.language) return;\n this.cfg = { ...this.cfg, language: lang };\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 * When `cfg.autoAcceptTos === false` (the plugin's `tos: \"skip\"` opt-in\n * / 0.2.7) and no token has been minted yet, throws a `not consented`\n * error instead of silently calling `acceptTos()` — the host promises\n * to handle consent externally via `controller.acceptTos()`. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) {\n if (this.cfg.autoAcceptTos === false) {\n throw new FeedbackError(\"not consented\");\n }\n await this.acceptTos();\n }\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 // #806 SeedSower P1 (Hermes URLSearchParams polyfill): React Native's\n // bundled URLSearchParams implements ONLY the constructor + toString().\n // `.set` / `.get` / `.append` / `.delete` / `.has` / `.entries` /\n // `.forEach` / `.keys` / `.values` / the iterator all throw on Hermes\n // (documented RN limitation since ~0.50, refused for bundle-size).\n // Build a plain Record<string, string> first, then pass to the\n // constructor — that variant is supported on Node + browser + Hermes\n // + JSC, and produces the same query string. NO mutation after\n // construction anywhere in the SDK.\n const params: Record<string, string> = { language: this.cfg.language };\n if (opts?.keys?.length) {\n params.keys = opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\");\n }\n if (opts?.namespace) params.namespace = opts.namespace;\n if (opts?.limit) params.limit = String(opts.limit);\n const qs = new URLSearchParams(params);\n const res = await this.authed(`/v1/feedback/strings?${qs.toString()}`, {\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 /**\n * 0.2.8 — `PATCH /v1/feedback/suggestions/{id}` (backend task 847,\n * deploy `45190c8`). Used by the panel's edit-mode editor when the\n * end-user already has a pending suggestion for a string (the\n * `FeedbackString.my_suggestion.id` from a prior submit). Submits\n * synchronously rather than going through the rating/suggestion\n * batch queue — the host triggered an explicit edit, not a passive\n * rating, so we want the round-trip to surface before the panel\n * closes. Best-effort failures bubble; the caller catches and\n * shows an error row in the panel.\n *\n * Wire body: `{ text: string }` (backend probe confirmed). NOT the\n * legacy `{ suggested_text }` of the batched POST path.\n */\n async editSuggestion(id: string, text: string): Promise<void> {\n if (!id) throw new FeedbackError(\"editSuggestion: id is required\");\n const trimmed = (text ?? \"\").trim();\n if (!trimmed) throw new FeedbackError(\"editSuggestion: text is required\");\n const res = await this.authed(`/v1/feedback/suggestions/${encodeURIComponent(id)}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text: trimmed }),\n });\n if (!res.ok) throw await this.problem(res, \"failed to update suggestion\");\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 *\n * Optional `namespace` filter (task 618, CONTRACT v6 — ADDITIVE): when\n * set (a single namespace or a list), the resolved set is narrowed to\n * keys whose `namespace` is in the filter. The filter is applied AFTER\n * resolution, so it composes with v5 rendered-scoping as\n * `rendered ∩ namespace`. UNSET / empty ⇒ unchanged v5 behaviour (all\n * resolved keys). Backward-compatible: `resolveKeys(keys)` is unaffected.\n */\nexport function resolveKeys(\n explicit?: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const base =\n explicit && explicit.length\n ? dedupe(explicit)\n : getRegistry()\n ? dedupe(getRegistry()!.snapshot())\n : [];\n return filterByNamespace(base, namespace);\n}\n\n/** Narrow keys to those whose namespace is in `namespace`. An undefined\n * filter, an empty string, or an empty list means \"no filter\" (the\n * additive default — identical to v5). Exported so non-panel callers\n * (e.g. a custom /core integration) can apply the same semantics. */\nexport function filterByNamespace(\n keys: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const list = (Array.isArray(namespace) ? namespace : namespace ? [namespace] : [])\n .map((n) => n.trim())\n .filter(Boolean);\n if (!list.length) return keys;\n const allow = new Set(list);\n return keys.filter((k) => allow.has(k.namespace));\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":[]}
|
package/dist/svelte/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Writable } from 'svelte/store';
|
|
2
|
-
import { F as FeedbackClient, c as FeedbackString, R as RatingInput, d as SuggestionInput, D as DeclaredKey } from '../client-
|
|
3
|
-
export { B as BatchResponse, b as FeedbackError, S as StringsResponse, T as TokenBundle } from '../client-
|
|
2
|
+
import { F as FeedbackClient, c as FeedbackString, R as RatingInput, d as SuggestionInput, D as DeclaredKey } from '../client-CnEK_2SD.cjs';
|
|
3
|
+
export { B as BatchResponse, b as FeedbackError, S as StringsResponse, T as TokenBundle } from '../client-CnEK_2SD.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* `@verbumia/feedback/svelte` — idiomatic Svelte adapter over the FROZEN
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Writable } from 'svelte/store';
|
|
2
|
-
import { F as FeedbackClient, c as FeedbackString, R as RatingInput, d as SuggestionInput, D as DeclaredKey } from '../client-
|
|
3
|
-
export { B as BatchResponse, b as FeedbackError, S as StringsResponse, T as TokenBundle } from '../client-
|
|
2
|
+
import { F as FeedbackClient, c as FeedbackString, R as RatingInput, d as SuggestionInput, D as DeclaredKey } from '../client-CnEK_2SD.js';
|
|
3
|
+
export { B as BatchResponse, b as FeedbackError, S as StringsResponse, T as TokenBundle } from '../client-CnEK_2SD.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* `@verbumia/feedback/svelte` — idiomatic Svelte adapter over the FROZEN
|
package/dist/svelte/index.js
CHANGED
package/dist/vue/index.cjs
CHANGED
|
@@ -75,11 +75,61 @@ var FeedbackClient = class {
|
|
|
75
75
|
get hasConsented() {
|
|
76
76
|
return this.tokens !== null;
|
|
77
77
|
}
|
|
78
|
+
/** Alias of `hasConsented` exposed under the 0.2.7 naming used by the
|
|
79
|
+
* `controller.hasAcceptedTos` getter (the SDK's built-in modal and a
|
|
80
|
+
* host's external ToS page share the same persisted token bundle, so
|
|
81
|
+
* both flip this boolean once a session is bootstrapped). */
|
|
82
|
+
get hasAcceptedTos() {
|
|
83
|
+
return this.tokens !== null;
|
|
84
|
+
}
|
|
78
85
|
/** Server-minted sessionId / grouping_key (from the token bundle).
|
|
79
86
|
* Available only after `acceptTos()`; never client-generated. */
|
|
80
87
|
get sessionId() {
|
|
81
88
|
return this.tokens?.grouping_key;
|
|
82
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* 0.2.7 — `GET /v1/projects/{projectId}/feedback-addon/state`.
|
|
92
|
+
*
|
|
93
|
+
* Auth selection (matches backend's dual-acceptance after task 836's
|
|
94
|
+
* follow-up PR — see CONTRACT note in the response type):
|
|
95
|
+
* - If `cfg.apiKey` is set → `Authorization: ApiKey <key>` (so the
|
|
96
|
+
* plugin can fetch state at `setup()` time, before the user has
|
|
97
|
+
* accepted ToS).
|
|
98
|
+
* - Else if a user-session bundle exists → `Authorization: Bearer
|
|
99
|
+
* <access_token>` (the deferred-after-acceptTos path).
|
|
100
|
+
* - Else → throw `FeedbackError("addon state requires apiKey or
|
|
101
|
+
* acceptTos")`. The plugin's setup catches and surfaces a
|
|
102
|
+
* console.warn the first time, then re-attempts on the
|
|
103
|
+
* bundle-mint that follows `acceptTos()`.
|
|
104
|
+
*
|
|
105
|
+
* Best-effort: a transport error returns `null` instead of throwing,
|
|
106
|
+
* so the controller's `isActive` falls back to whatever was last
|
|
107
|
+
* known (or its initial value) without flipping the host's CTA into
|
|
108
|
+
* an inconsistent state on a transient blip.
|
|
109
|
+
*/
|
|
110
|
+
async getAddonState() {
|
|
111
|
+
let auth;
|
|
112
|
+
if (this.cfg.apiKey) {
|
|
113
|
+
auth = `ApiKey ${this.cfg.apiKey}`;
|
|
114
|
+
} else if (this.tokens) {
|
|
115
|
+
auth = `Bearer ${this.tokens.access_token}`;
|
|
116
|
+
} else {
|
|
117
|
+
throw new FeedbackError(
|
|
118
|
+
"addon state requires apiKey or acceptTos"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const url = `${this.base()}/v1/projects/${this.cfg.projectId}/feedback-addon/state`;
|
|
122
|
+
try {
|
|
123
|
+
const res = await this.fetchImpl(url, {
|
|
124
|
+
method: "GET",
|
|
125
|
+
headers: { Authorization: auth }
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) return null;
|
|
128
|
+
return await res.json();
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
83
133
|
/** BCP-47 language the widget is rating strings in. */
|
|
84
134
|
get language() {
|
|
85
135
|
return this.cfg.language;
|
|
@@ -151,9 +201,18 @@ var FeedbackClient = class {
|
|
|
151
201
|
}
|
|
152
202
|
this.tokens = await res.json();
|
|
153
203
|
}
|
|
154
|
-
/** Authenticated fetch with a single transparent refresh-on-401 retry.
|
|
204
|
+
/** Authenticated fetch with a single transparent refresh-on-401 retry.
|
|
205
|
+
* When `cfg.autoAcceptTos === false` (the plugin's `tos: "skip"` opt-in
|
|
206
|
+
* / 0.2.7) and no token has been minted yet, throws a `not consented`
|
|
207
|
+
* error instead of silently calling `acceptTos()` — the host promises
|
|
208
|
+
* to handle consent externally via `controller.acceptTos()`. */
|
|
155
209
|
async authed(path, init, retry = true) {
|
|
156
|
-
if (!this.tokens)
|
|
210
|
+
if (!this.tokens) {
|
|
211
|
+
if (this.cfg.autoAcceptTos === false) {
|
|
212
|
+
throw new FeedbackError("not consented");
|
|
213
|
+
}
|
|
214
|
+
await this.acceptTos();
|
|
215
|
+
}
|
|
157
216
|
const res = await this.fetchImpl(`${this.base()}${path}`, {
|
|
158
217
|
...init,
|
|
159
218
|
headers: {
|
|
@@ -190,6 +249,31 @@ var FeedbackClient = class {
|
|
|
190
249
|
suggest(payload) {
|
|
191
250
|
this.enqueue({ kind: "suggestion", payload });
|
|
192
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* 0.2.8 — `PATCH /v1/feedback/suggestions/{id}` (backend task 847,
|
|
254
|
+
* deploy `45190c8`). Used by the panel's edit-mode editor when the
|
|
255
|
+
* end-user already has a pending suggestion for a string (the
|
|
256
|
+
* `FeedbackString.my_suggestion.id` from a prior submit). Submits
|
|
257
|
+
* synchronously rather than going through the rating/suggestion
|
|
258
|
+
* batch queue — the host triggered an explicit edit, not a passive
|
|
259
|
+
* rating, so we want the round-trip to surface before the panel
|
|
260
|
+
* closes. Best-effort failures bubble; the caller catches and
|
|
261
|
+
* shows an error row in the panel.
|
|
262
|
+
*
|
|
263
|
+
* Wire body: `{ text: string }` (backend probe confirmed). NOT the
|
|
264
|
+
* legacy `{ suggested_text }` of the batched POST path.
|
|
265
|
+
*/
|
|
266
|
+
async editSuggestion(id, text) {
|
|
267
|
+
if (!id) throw new FeedbackError("editSuggestion: id is required");
|
|
268
|
+
const trimmed = (text ?? "").trim();
|
|
269
|
+
if (!trimmed) throw new FeedbackError("editSuggestion: text is required");
|
|
270
|
+
const res = await this.authed(`/v1/feedback/suggestions/${encodeURIComponent(id)}`, {
|
|
271
|
+
method: "PATCH",
|
|
272
|
+
headers: { "Content-Type": "application/json" },
|
|
273
|
+
body: JSON.stringify({ text: trimmed })
|
|
274
|
+
});
|
|
275
|
+
if (!res.ok) throw await this.problem(res, "failed to update suggestion");
|
|
276
|
+
}
|
|
193
277
|
enqueue(item) {
|
|
194
278
|
this.queue.push(item);
|
|
195
279
|
if (this.queue.length >= this.cfg.maxBatch) {
|
package/dist/vue/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/vue/index.ts","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/core/keys.ts"],"sourcesContent":["/**\n * `@verbumia/feedback/vue` — idiomatic Vue 3 adapter over the FROZEN\n * /core FeedbackClient (task 611, master-approved). The host-i18n\n * plugin-slot half is descoped (no @verbumia/vue-i18n source) so this is\n * a standalone composable: it takes explicit config, owns an ISOLATED\n * reactive open-state (a closure-scoped ref — opening it re-renders only\n * the panel, never the host app), and exposes an imperative controller.\n * sessionId is server-minted (no client groupingKey). Mirrors the React\n * plugin principles without a second app-wide context.\n */\nimport { defineComponent, h, ref, Teleport, type Ref } from \"vue\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nexport interface VueFeedbackConfig {\n apiBase: string;\n projectId: string;\n language: string;\n // No tosVersion — SDK build-time constant (SDK_TOS_VERSION, task 616).\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n client: FeedbackClient;\n}\n\nconst C = {\n bg: \"#0b0f0e\", panel: \"#111714\", border: \"#1f2a25\",\n text: \"#e7f5ef\", dim: \"#8aa79b\", emerald: \"#10b981\", emeraldSoft: \"#34d399\",\n};\n\nexport interface VueFeedback {\n client: FeedbackClient;\n /** Reactive open-state (isolated; not app-wide provide/inject). */\n isOpen: Ref<boolean>;\n controller: FeedbackController;\n /** Functional panel component — mount once near the app root. */\n FeedbackPanel: ReturnType<typeof defineComponent>;\n}\n\nexport function createFeedback(config: VueFeedbackConfig): VueFeedback {\n const client = new FeedbackClient({\n apiBase: config.apiBase,\n projectId: config.projectId,\n language: config.language,\n endUserId: config.endUserId,\n fetchImpl: config.fetchImpl,\n });\n const isOpen = ref(false);\n const controller: FeedbackController = {\n open: () => {\n isOpen.value = true;\n },\n close: () => {\n isOpen.value = false;\n void client.flush();\n },\n client,\n };\n\n const FeedbackPanel = defineComponent({\n name: \"VerbumiaFeedbackPanel\",\n setup() {\n const strings = ref<FeedbackString[]>([]);\n const busy = ref(false);\n const consented = ref(client.hasConsented);\n const error = ref<string | null>(null);\n\n async function load() {\n busy.value = true;\n error.value = null;\n try {\n if (!consented.value) {\n await client.acceptTos();\n consented.value = true;\n }\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5):\n // ONLY rendered keys; empty -> show none, never fetch all.\n const resolved = resolveKeys(config.keys, config.namespace);\n if (!resolved.length) {\n strings.value = [];\n } else {\n const r = await client.getStrings({ keys: resolved });\n strings.value = r.strings;\n }\n } catch (e) {\n error.value = e instanceof Error ? e.message : \"Failed to load\";\n } finally {\n busy.value = false;\n }\n }\n\n return () => {\n if (!isOpen.value) return null;\n if (!strings.value.length && !busy.value && !error.value) void load();\n return h(\n Teleport,\n { to: \"body\" },\n h(\n \"div\",\n {\n role: \"dialog\",\n style: {\n position: \"fixed\", inset: 0, zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\", display: \"flex\",\n justifyContent: \"flex-end\",\n },\n onClick: controller.close,\n },\n [\n h(\n \"div\",\n {\n onClick: (e: Event) => e.stopPropagation(),\n style: {\n width: \"min(420px,100%)\", height: \"100%\",\n background: C.bg, color: C.text,\n borderLeft: `1px solid ${C.border}`, padding: \"18px\",\n fontFamily: \"system-ui,sans-serif\", overflowY: \"auto\",\n },\n },\n [\n h(\"strong\", { style: { color: C.emeraldSoft } },\n \"Translation feedback\"),\n error.value\n ? h(\"p\", { style: { color: \"#f87171\" } }, error.value)\n : null,\n ...strings.value.map((s) =>\n h(\n \"div\",\n {\n key: `${s.namespace}:${s.key}`,\n style: {\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: \"10px\", padding: \"12px\",\n margin: \"10px 0\",\n },\n },\n [\n h(\"div\", { style: { fontSize: \"12px\", color: C.dim } },\n `${s.namespace} · ${s.key}`),\n h(\"div\", { style: { margin: \"6px 0\" } }, s.value),\n h(\n \"div\",\n {},\n [1, 2, 3, 4, 5].map((n) =>\n h(\n \"button\",\n {\n key: n,\n \"aria-label\": `${n} stars`,\n onClick: () =>\n client.rate({\n namespace: s.namespace, key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars: n,\n }),\n style: {\n background: \"transparent\", border: \"none\",\n cursor: \"pointer\", fontSize: \"20px\",\n color: C.border,\n },\n },\n \"★\",\n ),\n ),\n ),\n ],\n ),\n ),\n ],\n ),\n ],\n ),\n );\n };\n },\n });\n\n return { client, isOpen, controller, FeedbackPanel };\n}\n\nexport { FeedbackClient } from \"../core/client\";\nexport type {\n DeclaredKey, FeedbackString, RatingInput, SuggestionInput,\n TokenBundle, StringsResponse, BatchResponse,\n} from \"../core/types\";\nexport { FeedbackError } from \"../core/types\";\n","/**\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.dev */\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.2.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 /**\n * Re-target the widget's working language at runtime (#806 SeedSower\n * lang-change propagation). Subsequent `getStrings` / `rate` /\n * `suggest` use the new language without rebuilding the client. The\n * server-minted ToS bundle has no `locale` field, so a language flip\n * does NOT invalidate consent — only the next `?language=` query\n * needs updating. The i18n provider plugin (feedback ≥0.2.6)\n * subscribes to `@verbumia/react-i18next` ≥1.0.5's\n * `VerbumiaPluginContext.onLanguageChange` and calls this on every\n * runtime language change so the host never has to wire it manually.\n */\n setLanguage(lang: string): void {\n if (typeof lang !== \"string\" || lang.length === 0) return;\n if (lang === this.cfg.language) return;\n this.cfg = { ...this.cfg, language: lang };\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 // #806 SeedSower P1 (Hermes URLSearchParams polyfill): React Native's\n // bundled URLSearchParams implements ONLY the constructor + toString().\n // `.set` / `.get` / `.append` / `.delete` / `.has` / `.entries` /\n // `.forEach` / `.keys` / `.values` / the iterator all throw on Hermes\n // (documented RN limitation since ~0.50, refused for bundle-size).\n // Build a plain Record<string, string> first, then pass to the\n // constructor — that variant is supported on Node + browser + Hermes\n // + JSC, and produces the same query string. NO mutation after\n // construction anywhere in the SDK.\n const params: Record<string, string> = { language: this.cfg.language };\n if (opts?.keys?.length) {\n params.keys = opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\");\n }\n if (opts?.namespace) params.namespace = opts.namespace;\n if (opts?.limit) params.limit = String(opts.limit);\n const qs = new URLSearchParams(params);\n const res = await this.authed(`/v1/feedback/strings?${qs.toString()}`, {\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 *\n * Optional `namespace` filter (task 618, CONTRACT v6 — ADDITIVE): when\n * set (a single namespace or a list), the resolved set is narrowed to\n * keys whose `namespace` is in the filter. The filter is applied AFTER\n * resolution, so it composes with v5 rendered-scoping as\n * `rendered ∩ namespace`. UNSET / empty ⇒ unchanged v5 behaviour (all\n * resolved keys). Backward-compatible: `resolveKeys(keys)` is unaffected.\n */\nexport function resolveKeys(\n explicit?: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const base =\n explicit && explicit.length\n ? dedupe(explicit)\n : getRegistry()\n ? dedupe(getRegistry()!.snapshot())\n : [];\n return filterByNamespace(base, namespace);\n}\n\n/** Narrow keys to those whose namespace is in `namespace`. An undefined\n * filter, an empty string, or an empty list means \"no filter\" (the\n * additive default — identical to v5). Exported so non-panel callers\n * (e.g. a custom /core integration) can apply the same semantics. */\nexport function filterByNamespace(\n keys: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const list = (Array.isArray(namespace) ? namespace : namespace ? [namespace] : [])\n .map((n) => n.trim())\n .filter(Boolean);\n if (!list.length) return keys;\n const allow = new Set(list);\n return keys.filter((k) => allow.has(k.namespace));\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/vue/index.ts","../../src/core/types.ts","../../src/core/tos.ts","../../src/core/client.ts","../../src/core/keys.ts"],"sourcesContent":["/**\n * `@verbumia/feedback/vue` — idiomatic Vue 3 adapter over the FROZEN\n * /core FeedbackClient (task 611, master-approved). The host-i18n\n * plugin-slot half is descoped (no @verbumia/vue-i18n source) so this is\n * a standalone composable: it takes explicit config, owns an ISOLATED\n * reactive open-state (a closure-scoped ref — opening it re-renders only\n * the panel, never the host app), and exposes an imperative controller.\n * sessionId is server-minted (no client groupingKey). Mirrors the React\n * plugin principles without a second app-wide context.\n */\nimport { defineComponent, h, ref, Teleport, type Ref } from \"vue\";\n\nimport { FeedbackClient } from \"../core/client\";\nimport { resolveKeys } from \"../core/keys\";\nimport type { DeclaredKey, FeedbackString } from \"../core/types\";\n\nexport interface VueFeedbackConfig {\n apiBase: string;\n projectId: string;\n language: string;\n // No tosVersion — SDK build-time constant (SDK_TOS_VERSION, task 616).\n endUserId?: string;\n keys?: DeclaredKey[];\n /** Optional namespace filter (CONTRACT v6, additive). Single ns or\n * list; applied AFTER rendered-scoping (`rendered ∩ namespace`).\n * Unset ⇒ all resolved keys (v5 behaviour). */\n namespace?: string | string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface FeedbackController {\n open: () => void;\n close: () => void;\n client: FeedbackClient;\n}\n\nconst C = {\n bg: \"#0b0f0e\", panel: \"#111714\", border: \"#1f2a25\",\n text: \"#e7f5ef\", dim: \"#8aa79b\", emerald: \"#10b981\", emeraldSoft: \"#34d399\",\n};\n\nexport interface VueFeedback {\n client: FeedbackClient;\n /** Reactive open-state (isolated; not app-wide provide/inject). */\n isOpen: Ref<boolean>;\n controller: FeedbackController;\n /** Functional panel component — mount once near the app root. */\n FeedbackPanel: ReturnType<typeof defineComponent>;\n}\n\nexport function createFeedback(config: VueFeedbackConfig): VueFeedback {\n const client = new FeedbackClient({\n apiBase: config.apiBase,\n projectId: config.projectId,\n language: config.language,\n endUserId: config.endUserId,\n fetchImpl: config.fetchImpl,\n });\n const isOpen = ref(false);\n const controller: FeedbackController = {\n open: () => {\n isOpen.value = true;\n },\n close: () => {\n isOpen.value = false;\n void client.flush();\n },\n client,\n };\n\n const FeedbackPanel = defineComponent({\n name: \"VerbumiaFeedbackPanel\",\n setup() {\n const strings = ref<FeedbackString[]>([]);\n const busy = ref(false);\n const consented = ref(client.hasConsented);\n const error = ref<string | null>(null);\n\n async function load() {\n busy.value = true;\n error.value = null;\n try {\n if (!consented.value) {\n await client.acceptTos();\n consented.value = true;\n }\n // On-screen scoping is NORMATIVE (spec ltm 373, CONTRACT v5):\n // ONLY rendered keys; empty -> show none, never fetch all.\n const resolved = resolveKeys(config.keys, config.namespace);\n if (!resolved.length) {\n strings.value = [];\n } else {\n const r = await client.getStrings({ keys: resolved });\n strings.value = r.strings;\n }\n } catch (e) {\n error.value = e instanceof Error ? e.message : \"Failed to load\";\n } finally {\n busy.value = false;\n }\n }\n\n return () => {\n if (!isOpen.value) return null;\n if (!strings.value.length && !busy.value && !error.value) void load();\n return h(\n Teleport,\n { to: \"body\" },\n h(\n \"div\",\n {\n role: \"dialog\",\n style: {\n position: \"fixed\", inset: 0, zIndex: 2147483600,\n background: \"rgba(0,0,0,.55)\", display: \"flex\",\n justifyContent: \"flex-end\",\n },\n onClick: controller.close,\n },\n [\n h(\n \"div\",\n {\n onClick: (e: Event) => e.stopPropagation(),\n style: {\n width: \"min(420px,100%)\", height: \"100%\",\n background: C.bg, color: C.text,\n borderLeft: `1px solid ${C.border}`, padding: \"18px\",\n fontFamily: \"system-ui,sans-serif\", overflowY: \"auto\",\n },\n },\n [\n h(\"strong\", { style: { color: C.emeraldSoft } },\n \"Translation feedback\"),\n error.value\n ? h(\"p\", { style: { color: \"#f87171\" } }, error.value)\n : null,\n ...strings.value.map((s) =>\n h(\n \"div\",\n {\n key: `${s.namespace}:${s.key}`,\n style: {\n background: C.panel,\n border: `1px solid ${C.border}`,\n borderRadius: \"10px\", padding: \"12px\",\n margin: \"10px 0\",\n },\n },\n [\n h(\"div\", { style: { fontSize: \"12px\", color: C.dim } },\n `${s.namespace} · ${s.key}`),\n h(\"div\", { style: { margin: \"6px 0\" } }, s.value),\n h(\n \"div\",\n {},\n [1, 2, 3, 4, 5].map((n) =>\n h(\n \"button\",\n {\n key: n,\n \"aria-label\": `${n} stars`,\n onClick: () =>\n client.rate({\n namespace: s.namespace, key: s.key,\n language: client.language,\n translation_hash: s.translation_hash,\n stars: n,\n }),\n style: {\n background: \"transparent\", border: \"none\",\n cursor: \"pointer\", fontSize: \"20px\",\n color: C.border,\n },\n },\n \"★\",\n ),\n ),\n ),\n ],\n ),\n ),\n ],\n ),\n ],\n ),\n );\n };\n },\n });\n\n return { client, isOpen, controller, FeedbackPanel };\n}\n\nexport { FeedbackClient } from \"../core/client\";\nexport type {\n DeclaredKey, FeedbackString, RatingInput, SuggestionInput,\n TokenBundle, StringsResponse, BatchResponse,\n} from \"../core/types\";\nexport { FeedbackError } from \"../core/types\";\n","/**\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.dev */\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 * 0.2.7 — host's project API key (`vrb_live_…`, scope `project:read`).\n * When set, the client can call addon-state at setup time (BEFORE\n * the end-user accepts ToS) using `Authorization: ApiKey …` — mirrors\n * the existing scheme used by the i18n SDK's dev-env loaders. When\n * unset, addon-state is deferred until the user's session bearer is\n * minted via `acceptTos()`.\n */\n apiKey?: string;\n /**\n * #806 / 0.2.7 — when `false`, the client must NOT silently call\n * `POST /v1/feedback/tos` to bootstrap a session on the user's behalf.\n * Set by the plugin when the host opts into `tos: \"skip\"` (the host\n * promises to handle consent externally via `controller.acceptTos()`).\n * `authed()` then throws `FeedbackError(\"not consented\")` instead of\n * auto-accepting — the panel surfaces the existing error state.\n * Default `true` preserves all pre-0.2.7 behaviour.\n */\n autoAcceptTos?: boolean;\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 * 0.2.8 — backend deploy `45190c8` (task 846) adds source-language\n * fields so the panel can render the source-locale text alongside\n * the target-locale text when the user toggles \"Show original\".\n *\n * - `source_text`: the source-locale rendering for this key\n * (== `value` whenever `source_locale === language` — the panel\n * uses that equality to hide the source row and avoid\n * duplication on a single-language project).\n * - `source_locale`: BCP-47 of the source.\n * - `my_suggestion`: the end-user's pending suggestion for this\n * key in this language (or `null`). Pre-fills the suggestion\n * editor + flips the submit path from POST → PATCH.\n */\n source_text: string;\n source_locale: string;\n my_suggestion: MySuggestion | null;\n}\n\n/** 0.2.8 — end-user's pending suggestion for one (key, language).\n * Returned inline on `FeedbackString` so the panel can render an\n * edit-mode editor without a second round-trip. */\nexport interface MySuggestion {\n /** Suggestion UUID; passed to `PATCH /v1/feedback/suggestions/{id}` to\n * update the text. */\n id: string;\n text: string;\n}\n\nexport interface StringsResponse {\n project_id: string;\n language: string;\n strings: FeedbackString[];\n}\n\n/**\n * 0.2.7 — addon state for a project, returned by\n * `GET /v1/projects/{projectId}/feedback-addon/state`. Drives the\n * controller surface (`isActive`, `enabledLanguages`, `sku`,\n * `languagesLimit`) so the host can hide its own CTA on Starter +\n * narrow the widget to enabled languages.\n *\n * Backend contract (camelCase aliases, populate_by_name=true on the\n * server schema, confirmed against backend task 836 SHA d78b939 + the\n * follow-up PR that extends the auth surface to accept project API\n * keys with `project:read` scope):\n * - `isActive`: org.feedback_addon.active\n * - `sku`: the SKU code, or `null` if no active SKU\n * - `enabledLanguages`: the project's allow-list, or literal `\"all\"`\n * when sku === `\"feedback_unlimited\"`; an empty array when no SKU\n * - `languagesLimit`: hard cap for Starter (3), null otherwise\n */\nexport interface FeedbackAddonState {\n isActive: boolean;\n sku: \"feedback_starter\" | \"feedback_unlimited\" | null;\n enabledLanguages: string[] | \"all\";\n languagesLimit: number | null;\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 FeedbackAddonState,\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.2.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 /** Alias of `hasConsented` exposed under the 0.2.7 naming used by the\n * `controller.hasAcceptedTos` getter (the SDK's built-in modal and a\n * host's external ToS page share the same persisted token bundle, so\n * both flip this boolean once a session is bootstrapped). */\n get hasAcceptedTos(): 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 /**\n * 0.2.7 — `GET /v1/projects/{projectId}/feedback-addon/state`.\n *\n * Auth selection (matches backend's dual-acceptance after task 836's\n * follow-up PR — see CONTRACT note in the response type):\n * - If `cfg.apiKey` is set → `Authorization: ApiKey <key>` (so the\n * plugin can fetch state at `setup()` time, before the user has\n * accepted ToS).\n * - Else if a user-session bundle exists → `Authorization: Bearer\n * <access_token>` (the deferred-after-acceptTos path).\n * - Else → throw `FeedbackError(\"addon state requires apiKey or\n * acceptTos\")`. The plugin's setup catches and surfaces a\n * console.warn the first time, then re-attempts on the\n * bundle-mint that follows `acceptTos()`.\n *\n * Best-effort: a transport error returns `null` instead of throwing,\n * so the controller's `isActive` falls back to whatever was last\n * known (or its initial value) without flipping the host's CTA into\n * an inconsistent state on a transient blip.\n */\n async getAddonState(): Promise<FeedbackAddonState | null> {\n let auth: string | undefined;\n if (this.cfg.apiKey) {\n auth = `ApiKey ${this.cfg.apiKey}`;\n } else if (this.tokens) {\n auth = `Bearer ${this.tokens.access_token}`;\n } else {\n throw new FeedbackError(\n \"addon state requires apiKey or acceptTos\",\n );\n }\n const url = `${this.base()}/v1/projects/${this.cfg.projectId}/feedback-addon/state`;\n try {\n const res = await this.fetchImpl(url, {\n method: \"GET\",\n headers: { Authorization: auth },\n });\n if (!res.ok) return null;\n return (await res.json()) as FeedbackAddonState;\n } catch {\n return null;\n }\n }\n\n /** BCP-47 language the widget is rating strings in. */\n get language(): string {\n return this.cfg.language;\n }\n\n /**\n * Re-target the widget's working language at runtime (#806 SeedSower\n * lang-change propagation). Subsequent `getStrings` / `rate` /\n * `suggest` use the new language without rebuilding the client. The\n * server-minted ToS bundle has no `locale` field, so a language flip\n * does NOT invalidate consent — only the next `?language=` query\n * needs updating. The i18n provider plugin (feedback ≥0.2.6)\n * subscribes to `@verbumia/react-i18next` ≥1.0.5's\n * `VerbumiaPluginContext.onLanguageChange` and calls this on every\n * runtime language change so the host never has to wire it manually.\n */\n setLanguage(lang: string): void {\n if (typeof lang !== \"string\" || lang.length === 0) return;\n if (lang === this.cfg.language) return;\n this.cfg = { ...this.cfg, language: lang };\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 * When `cfg.autoAcceptTos === false` (the plugin's `tos: \"skip\"` opt-in\n * / 0.2.7) and no token has been minted yet, throws a `not consented`\n * error instead of silently calling `acceptTos()` — the host promises\n * to handle consent externally via `controller.acceptTos()`. */\n private async authed(\n path: string,\n init: RequestInit,\n retry = true,\n ): Promise<Response> {\n if (!this.tokens) {\n if (this.cfg.autoAcceptTos === false) {\n throw new FeedbackError(\"not consented\");\n }\n await this.acceptTos();\n }\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 // #806 SeedSower P1 (Hermes URLSearchParams polyfill): React Native's\n // bundled URLSearchParams implements ONLY the constructor + toString().\n // `.set` / `.get` / `.append` / `.delete` / `.has` / `.entries` /\n // `.forEach` / `.keys` / `.values` / the iterator all throw on Hermes\n // (documented RN limitation since ~0.50, refused for bundle-size).\n // Build a plain Record<string, string> first, then pass to the\n // constructor — that variant is supported on Node + browser + Hermes\n // + JSC, and produces the same query string. NO mutation after\n // construction anywhere in the SDK.\n const params: Record<string, string> = { language: this.cfg.language };\n if (opts?.keys?.length) {\n params.keys = opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\");\n }\n if (opts?.namespace) params.namespace = opts.namespace;\n if (opts?.limit) params.limit = String(opts.limit);\n const qs = new URLSearchParams(params);\n const res = await this.authed(`/v1/feedback/strings?${qs.toString()}`, {\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 /**\n * 0.2.8 — `PATCH /v1/feedback/suggestions/{id}` (backend task 847,\n * deploy `45190c8`). Used by the panel's edit-mode editor when the\n * end-user already has a pending suggestion for a string (the\n * `FeedbackString.my_suggestion.id` from a prior submit). Submits\n * synchronously rather than going through the rating/suggestion\n * batch queue — the host triggered an explicit edit, not a passive\n * rating, so we want the round-trip to surface before the panel\n * closes. Best-effort failures bubble; the caller catches and\n * shows an error row in the panel.\n *\n * Wire body: `{ text: string }` (backend probe confirmed). NOT the\n * legacy `{ suggested_text }` of the batched POST path.\n */\n async editSuggestion(id: string, text: string): Promise<void> {\n if (!id) throw new FeedbackError(\"editSuggestion: id is required\");\n const trimmed = (text ?? \"\").trim();\n if (!trimmed) throw new FeedbackError(\"editSuggestion: text is required\");\n const res = await this.authed(`/v1/feedback/suggestions/${encodeURIComponent(id)}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text: trimmed }),\n });\n if (!res.ok) throw await this.problem(res, \"failed to update suggestion\");\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 *\n * Optional `namespace` filter (task 618, CONTRACT v6 — ADDITIVE): when\n * set (a single namespace or a list), the resolved set is narrowed to\n * keys whose `namespace` is in the filter. The filter is applied AFTER\n * resolution, so it composes with v5 rendered-scoping as\n * `rendered ∩ namespace`. UNSET / empty ⇒ unchanged v5 behaviour (all\n * resolved keys). Backward-compatible: `resolveKeys(keys)` is unaffected.\n */\nexport function resolveKeys(\n explicit?: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const base =\n explicit && explicit.length\n ? dedupe(explicit)\n : getRegistry()\n ? dedupe(getRegistry()!.snapshot())\n : [];\n return filterByNamespace(base, namespace);\n}\n\n/** Narrow keys to those whose namespace is in `namespace`. An undefined\n * filter, an empty string, or an empty list means \"no filter\" (the\n * additive default — identical to v5). Exported so non-panel callers\n * (e.g. a custom /core integration) can apply the same semantics. */\nexport function filterByNamespace(\n keys: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const list = (Array.isArray(namespace) ? namespace : namespace ? [namespace] : [])\n .map((n) => n.trim())\n .filter(Boolean);\n if (!list.length) return keys;\n const allow = new Set(list);\n return keys.filter((k) => allow.has(k.namespace));\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":[]}
|
package/dist/vue/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ref, defineComponent } from 'vue';
|
|
2
|
-
import { F as FeedbackClient, D as DeclaredKey } from '../client-
|
|
3
|
-
export { B as BatchResponse, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-
|
|
2
|
+
import { F as FeedbackClient, D as DeclaredKey } from '../client-CnEK_2SD.cjs';
|
|
3
|
+
export { B as BatchResponse, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-CnEK_2SD.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* `@verbumia/feedback/vue` — idiomatic Vue 3 adapter over the FROZEN
|
package/dist/vue/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ref, defineComponent } from 'vue';
|
|
2
|
-
import { F as FeedbackClient, D as DeclaredKey } from '../client-
|
|
3
|
-
export { B as BatchResponse, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-
|
|
2
|
+
import { F as FeedbackClient, D as DeclaredKey } from '../client-CnEK_2SD.js';
|
|
3
|
+
export { B as BatchResponse, b as FeedbackError, c as FeedbackString, R as RatingInput, S as StringsResponse, d as SuggestionInput, T as TokenBundle } from '../client-CnEK_2SD.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* `@verbumia/feedback/vue` — idiomatic Vue 3 adapter over the FROZEN
|
package/dist/vue/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verbumia/feedback",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Verbumia End-User Translation Evaluation widget — let your end users rate and suggest translations. React (web) + React Native / Expo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://verbumia.ca",
|
|
@@ -1 +0,0 @@
|
|
|
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.dev */\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.2.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 /**\n * Re-target the widget's working language at runtime (#806 SeedSower\n * lang-change propagation). Subsequent `getStrings` / `rate` /\n * `suggest` use the new language without rebuilding the client. The\n * server-minted ToS bundle has no `locale` field, so a language flip\n * does NOT invalidate consent — only the next `?language=` query\n * needs updating. The i18n provider plugin (feedback ≥0.2.6)\n * subscribes to `@verbumia/react-i18next` ≥1.0.5's\n * `VerbumiaPluginContext.onLanguageChange` and calls this on every\n * runtime language change so the host never has to wire it manually.\n */\n setLanguage(lang: string): void {\n if (typeof lang !== \"string\" || lang.length === 0) return;\n if (lang === this.cfg.language) return;\n this.cfg = { ...this.cfg, language: lang };\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 // #806 SeedSower P1 (Hermes URLSearchParams polyfill): React Native's\n // bundled URLSearchParams implements ONLY the constructor + toString().\n // `.set` / `.get` / `.append` / `.delete` / `.has` / `.entries` /\n // `.forEach` / `.keys` / `.values` / the iterator all throw on Hermes\n // (documented RN limitation since ~0.50, refused for bundle-size).\n // Build a plain Record<string, string> first, then pass to the\n // constructor — that variant is supported on Node + browser + Hermes\n // + JSC, and produces the same query string. NO mutation after\n // construction anywhere in the SDK.\n const params: Record<string, string> = { language: this.cfg.language };\n if (opts?.keys?.length) {\n params.keys = opts.keys.map((k) => `${k.namespace}:${k.key}`).join(\",\");\n }\n if (opts?.namespace) params.namespace = opts.namespace;\n if (opts?.limit) params.limit = String(opts.limit);\n const qs = new URLSearchParams(params);\n const res = await this.authed(`/v1/feedback/strings?${qs.toString()}`, {\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 *\n * Optional `namespace` filter (task 618, CONTRACT v6 — ADDITIVE): when\n * set (a single namespace or a list), the resolved set is narrowed to\n * keys whose `namespace` is in the filter. The filter is applied AFTER\n * resolution, so it composes with v5 rendered-scoping as\n * `rendered ∩ namespace`. UNSET / empty ⇒ unchanged v5 behaviour (all\n * resolved keys). Backward-compatible: `resolveKeys(keys)` is unaffected.\n */\nexport function resolveKeys(\n explicit?: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const base =\n explicit && explicit.length\n ? dedupe(explicit)\n : getRegistry()\n ? dedupe(getRegistry()!.snapshot())\n : [];\n return filterByNamespace(base, namespace);\n}\n\n/** Narrow keys to those whose namespace is in `namespace`. An undefined\n * filter, an empty string, or an empty list means \"no filter\" (the\n * additive default — identical to v5). Exported so non-panel callers\n * (e.g. a custom /core integration) can apply the same semantics. */\nexport function filterByNamespace(\n keys: DeclaredKey[],\n namespace?: string | string[],\n): DeclaredKey[] {\n const list = (Array.isArray(namespace) ? namespace : namespace ? [namespace] : [])\n .map((n) => n.trim())\n .filter(Boolean);\n if (!list.length) return keys;\n const allow = new Set(list);\n return keys.filter((k) => allow.has(k.namespace));\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":[]}
|