@everfur/sdk 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +703 -4
  2. package/README.md +63 -23
  3. package/consent/package.json +8 -0
  4. package/dist/Chat-BZZ-Py5l.d.ts +128 -0
  5. package/dist/Chat-Dq4fffIO.d.cts +128 -0
  6. package/dist/DepthViews-BG3E25MD.d.ts +16 -0
  7. package/dist/DepthViews-B_hIypI6.d.cts +19 -0
  8. package/dist/DepthViews-CL75jyRA.d.cts +16 -0
  9. package/dist/DepthViews-CkYri51q.d.ts +19 -0
  10. package/dist/ErrorPolicyPort-CNf4uQZP.d.cts +22 -0
  11. package/dist/ErrorPolicyPort-CwbfmxzJ.d.ts +22 -0
  12. package/dist/{EverfurResult-D92-uL82.d.cts → EverfurResult-DN9pL2Ab.d.cts} +11 -10
  13. package/dist/{EverfurResult-D92-uL82.d.ts → EverfurResult-DN9pL2Ab.d.ts} +11 -10
  14. package/dist/{PhotoController-BItt5M7u.d.cts → PhotoController-3l7MT9jA.d.cts} +1 -1
  15. package/dist/{PhotoController-D8zMTdcW.d.ts → PhotoController-vzY5bfY_.d.ts} +1 -1
  16. package/dist/animations/index.cjs +1 -1997
  17. package/dist/animations/index.d.cts +35 -35
  18. package/dist/animations/index.d.ts +35 -35
  19. package/dist/animations/index.js +1 -1972
  20. package/dist/attachments-BorG422I.d.cts +146 -0
  21. package/dist/attachments-BrUWQYNK.d.ts +257 -0
  22. package/dist/attachments-D_SECjr1.d.cts +257 -0
  23. package/dist/attachments-DpldrvIW.d.ts +146 -0
  24. package/dist/bookingCopy-CqtDYvCR.d.ts +732 -0
  25. package/dist/bookingCopy-SREvkG19.d.cts +732 -0
  26. package/dist/branding-CsRdeNwK.d.cts +566 -0
  27. package/dist/branding-CsRdeNwK.d.ts +566 -0
  28. package/dist/callCopy-DoQKG3-L.d.cts +40 -0
  29. package/dist/callCopy-DoQKG3-L.d.ts +40 -0
  30. package/dist/casesRepository-DwtZVsbj.d.cts +51 -0
  31. package/dist/casesRepository-v6QczS9N.d.ts +51 -0
  32. package/dist/chat/index.cjs +1 -1170
  33. package/dist/chat/index.d.cts +32 -53
  34. package/dist/chat/index.d.ts +32 -53
  35. package/dist/chat/index.js +1 -1167
  36. package/dist/client/index.cjs +9 -2315
  37. package/dist/client/index.d.cts +80 -16
  38. package/dist/client/index.d.ts +80 -16
  39. package/dist/client/index.js +9 -2215
  40. package/dist/{config-CiJ0PVBB.d.ts → config--Wu1Umhd.d.cts} +64 -26
  41. package/dist/{config-BSjBdxrZ.d.cts → config-C7eb_IMD.d.ts} +64 -26
  42. package/dist/consent/index.cjs +1 -0
  43. package/dist/consent/index.d.cts +33 -0
  44. package/dist/consent/index.d.ts +33 -0
  45. package/dist/consent/index.js +1 -0
  46. package/dist/context-CL3lyuy1.d.cts +159 -0
  47. package/dist/context-CahAEha8.d.ts +159 -0
  48. package/dist/copy-DdVFDL01.d.ts +198 -0
  49. package/dist/copy-UeUJVk8X.d.cts +198 -0
  50. package/dist/core/index.cjs +13 -3178
  51. package/dist/core/index.d.cts +164 -29
  52. package/dist/core/index.d.ts +164 -29
  53. package/dist/core/index.js +13 -3166
  54. package/dist/depth-D5HwcvKC.d.cts +533 -0
  55. package/dist/depth-DvPwPuT_.d.ts +533 -0
  56. package/dist/entitlementRepository-B7egEdW6.d.ts +43 -0
  57. package/dist/entitlementRepository-BpJ7tiwl.d.cts +43 -0
  58. package/dist/{identity-Brl-lDd6.d.cts → identity-C2-08BZW.d.cts} +20 -7
  59. package/dist/{identity-DK9zORrG.d.ts → identity-CEi9GRFZ.d.ts} +20 -7
  60. package/dist/{ids-CJ1S6adf.d.cts → ids-B2GAAifq.d.cts} +1 -1
  61. package/dist/{ids-CJ1S6adf.d.ts → ids-B2GAAifq.d.ts} +1 -1
  62. package/dist/index.cjs +12 -4494
  63. package/dist/index.d.cts +30 -121
  64. package/dist/index.d.ts +30 -121
  65. package/dist/index.js +12 -4470
  66. package/dist/models-B-Uh2LTf.d.ts +214 -0
  67. package/dist/models-D2EUxPZY.d.cts +214 -0
  68. package/dist/notifications/index.cjs +2 -0
  69. package/dist/notifications/index.d.cts +17 -0
  70. package/dist/notifications/index.d.ts +17 -0
  71. package/dist/notifications/index.js +2 -0
  72. package/dist/optionalModule-DbJmsq5f.d.cts +21 -0
  73. package/dist/optionalModule-DbJmsq5f.d.ts +21 -0
  74. package/dist/pets-DJv2sj2H.d.ts +92 -0
  75. package/dist/pets-JVCXgRgR.d.cts +92 -0
  76. package/dist/petsRepository-B_gKbAzJ.d.cts +142 -0
  77. package/dist/petsRepository-Bk6GDwLs.d.ts +142 -0
  78. package/dist/photo/index.cjs +1 -2189
  79. package/dist/photo/index.d.cts +6 -6
  80. package/dist/photo/index.d.ts +6 -6
  81. package/dist/photo/index.js +1 -2186
  82. package/dist/ports-BN6RHF9W.d.cts +48 -0
  83. package/dist/ports-BN6RHF9W.d.ts +48 -0
  84. package/dist/ports-BY2ph0_y.d.cts +376 -0
  85. package/dist/ports-D7YW8qz9.d.ts +376 -0
  86. package/dist/profile-CSs1wlXT.d.cts +93 -0
  87. package/dist/profile-CSs1wlXT.d.ts +93 -0
  88. package/dist/records/depth/index.cjs +2 -0
  89. package/dist/records/depth/index.d.cts +13 -0
  90. package/dist/records/depth/index.d.ts +13 -0
  91. package/dist/records/depth/index.js +2 -0
  92. package/dist/records/index.cjs +3 -2839
  93. package/dist/records/index.d.cts +137 -203
  94. package/dist/records/index.d.ts +137 -203
  95. package/dist/records/index.js +3 -2833
  96. package/dist/requestFunnel-BjDpqqvm.d.cts +110 -0
  97. package/dist/requestFunnel-DFMGcluw.d.ts +110 -0
  98. package/dist/{resolve-Dq_4_agU.d.ts → resolve-D4Ywz5OS.d.cts} +10 -4
  99. package/dist/{resolve-Dq_4_agU.d.cts → resolve-D4Ywz5OS.d.ts} +10 -4
  100. package/dist/{runtime-Bvr8yqXm.d.cts → runtimeTypes-gZEJu-da.d.cts} +104 -132
  101. package/dist/{runtime-BIYzf9cK.d.ts → runtimeTypes-w5KqBMcs.d.ts} +104 -132
  102. package/dist/server/events/index.cjs +2 -0
  103. package/dist/server/events/index.d.cts +550 -0
  104. package/dist/server/events/index.d.ts +550 -0
  105. package/dist/server/events/index.js +2 -0
  106. package/dist/server/index.cjs +2 -533
  107. package/dist/server/index.d.cts +73 -18
  108. package/dist/server/index.d.ts +73 -18
  109. package/dist/server/index.js +2 -530
  110. package/dist/species-BXAIMh7I.d.cts +9 -0
  111. package/dist/species-BXAIMh7I.d.ts +9 -0
  112. package/dist/televet/booking/index.cjs +1 -0
  113. package/dist/televet/booking/index.d.cts +294 -0
  114. package/dist/televet/booking/index.d.ts +294 -0
  115. package/dist/televet/booking/index.js +1 -0
  116. package/dist/televet/call/index.cjs +1 -0
  117. package/dist/televet/call/index.d.cts +141 -0
  118. package/dist/televet/call/index.d.ts +141 -0
  119. package/dist/televet/call/index.js +1 -0
  120. package/dist/televet/index.cjs +1 -0
  121. package/dist/televet/index.d.cts +142 -0
  122. package/dist/televet/index.d.ts +142 -0
  123. package/dist/televet/index.js +1 -0
  124. package/dist/testing/index.cjs +5 -823
  125. package/dist/testing/index.d.cts +18 -4
  126. package/dist/testing/index.d.ts +18 -4
  127. package/dist/testing/index.js +5 -820
  128. package/dist/testing/rn/index.cjs +1 -449
  129. package/dist/testing/rn/index.d.cts +18 -29
  130. package/dist/testing/rn/index.d.ts +18 -29
  131. package/dist/testing/rn/index.js +1 -444
  132. package/dist/testing/web/index.cjs +1 -0
  133. package/dist/testing/web/index.d.cts +132 -0
  134. package/dist/testing/web/index.d.ts +132 -0
  135. package/dist/testing/web/index.js +1 -0
  136. package/dist/timelineRows-C15jxFFH.d.ts +19 -0
  137. package/dist/timelineRows-odO8g9r2.d.cts +19 -0
  138. package/dist/typeStyle-CKqVYD6L.d.cts +111 -0
  139. package/dist/typeStyle-DmoyacDG.d.ts +111 -0
  140. package/dist/uploadTransport-D0M0T4hN.d.ts +38 -0
  141. package/dist/uploadTransport-DKJHs3Yj.d.cts +38 -0
  142. package/dist/useRecordsDepth-C6rjEaLn.d.cts +237 -0
  143. package/dist/useRecordsDepth-crhBllQK.d.ts +237 -0
  144. package/dist/useVetVisit-Bc84hWUU.d.cts +187 -0
  145. package/dist/useVetVisit-CjmMa896.d.ts +187 -0
  146. package/dist/video/index.cjs +1 -1941
  147. package/dist/video/index.d.cts +4 -4
  148. package/dist/video/index.d.ts +4 -4
  149. package/dist/video/index.js +1 -1938
  150. package/dist/view-Bxwxp4xD.d.ts +310 -0
  151. package/dist/view-DyitEO0m.d.cts +310 -0
  152. package/dist/visitIntent-D7_yVp1I.d.cts +8 -0
  153. package/dist/visitIntent-D7_yVp1I.d.ts +8 -0
  154. package/dist/web/consent/index.cjs +1 -0
  155. package/dist/web/consent/index.d.cts +34 -0
  156. package/dist/web/consent/index.d.ts +34 -0
  157. package/dist/web/consent/index.js +1 -0
  158. package/dist/web/index.cjs +14 -0
  159. package/dist/web/index.d.cts +218 -0
  160. package/dist/web/index.d.ts +218 -0
  161. package/dist/web/index.js +14 -0
  162. package/dist/web/notifications/index.cjs +2 -0
  163. package/dist/web/notifications/index.d.cts +213 -0
  164. package/dist/web/notifications/index.d.ts +213 -0
  165. package/dist/web/notifications/index.js +2 -0
  166. package/dist/web/records/depth/index.cjs +2 -0
  167. package/dist/web/records/depth/index.d.cts +12 -0
  168. package/dist/web/records/depth/index.d.ts +12 -0
  169. package/dist/web/records/depth/index.js +2 -0
  170. package/dist/web/records/index.cjs +4 -0
  171. package/dist/web/records/index.d.cts +285 -0
  172. package/dist/web/records/index.d.ts +285 -0
  173. package/dist/web/records/index.js +4 -0
  174. package/dist/web/televet/booking/index.cjs +1 -0
  175. package/dist/web/televet/booking/index.d.cts +349 -0
  176. package/dist/web/televet/booking/index.d.ts +349 -0
  177. package/dist/web/televet/booking/index.js +1 -0
  178. package/dist/web/televet/call/index.cjs +1 -0
  179. package/dist/web/televet/call/index.d.cts +142 -0
  180. package/dist/web/televet/call/index.d.ts +142 -0
  181. package/dist/web/televet/call/index.js +1 -0
  182. package/dist/web/televet/index.cjs +1 -0
  183. package/dist/web/televet/index.d.cts +136 -0
  184. package/dist/web/televet/index.d.ts +136 -0
  185. package/dist/web/televet/index.js +1 -0
  186. package/notifications/package.json +8 -0
  187. package/package.json +266 -10
  188. package/records/depth/package.json +8 -0
  189. package/server/events/device-blocked.cjs +15 -0
  190. package/server/events/package.json +9 -0
  191. package/televet/booking/package.json +8 -0
  192. package/televet/call/package.json +8 -0
  193. package/televet/package.json +8 -0
  194. package/testing/web/native-blocked.cjs +12 -0
  195. package/testing/web/package.json +8 -0
  196. package/web/consent/native-blocked.cjs +12 -0
  197. package/web/consent/package.json +8 -0
  198. package/web/native-blocked.cjs +12 -0
  199. package/web/notifications/native-blocked.cjs +12 -0
  200. package/web/notifications/package.json +8 -0
  201. package/web/package.json +8 -0
  202. package/web/records/depth/native-blocked.cjs +12 -0
  203. package/web/records/depth/package.json +8 -0
  204. package/web/records/native-blocked.cjs +12 -0
  205. package/web/records/package.json +8 -0
  206. package/web/televet/booking/native-blocked.cjs +12 -0
  207. package/web/televet/booking/package.json +8 -0
  208. package/web/televet/call/native-blocked.cjs +12 -0
  209. package/web/televet/call/package.json +8 -0
  210. package/web/televet/native-blocked.cjs +12 -0
  211. package/web/televet/package.json +8 -0
  212. package/dist/ChatController-CKdBvPj2.d.ts +0 -146
  213. package/dist/ChatController-CpUMvvZf.d.cts +0 -146
  214. package/dist/FilePort-BabWrv7I.d.cts +0 -22
  215. package/dist/FilePort-BabWrv7I.d.ts +0 -22
  216. package/dist/petsRepository-BEGb97M9.d.cts +0 -326
  217. package/dist/petsRepository-Bu18r2kK.d.ts +0 -326
  218. package/dist/requestFunnel-DuUH-kAe.d.cts +0 -28
  219. package/dist/requestFunnel-dio5OmR9.d.ts +0 -28
package/CHANGELOG.md CHANGED
@@ -8,9 +8,708 @@ The line is deliberately pre-1.0: the API is stable enough to build against and
8
8
  is guarded by api-extractor golden reports across all eleven subpaths, but 1.0.0 is a promise about breaking
9
9
  changes that has not been earned by any production integration yet.
10
10
 
11
+ ## [0.4.0] - 2026-09-21
12
+
13
+ ### Changed: a press of `VetVisitButton` opens the visit IN YOUR APP, not the system browser
14
+
15
+ **Who is affected: every React Native integration that has registered an in-app flow. Nobody else.** If you
16
+ upgrade and change nothing, the button mints the handoff and opens the hosted visit exactly as 0.3.0 did.
17
+ That is the floor, and it is tested: the member always reaches a vet, and the only thing this release can
18
+ change is where.
19
+
20
+ **Why 0.4.0 and not 0.3.1.** Semver's rule for a pre-1.0 line is that the MINOR is the compatibility signal,
21
+ and the observable default behaviour of a published, shipping component changes here. No type was removed and
22
+ no signature narrowed, so a 0.3.0 integration still COMPILES against 0.4.0 unchanged, which is why this is not
23
+ a major. But "compiles unchanged" is the wrong test for a default: a partner who pins `^0.3.0` and takes this
24
+ automatically would get a different destination on a control their users already press. A minor bump is what
25
+ makes that an upgrade a partner chooses rather than one that arrives.
26
+
27
+ **What to do.** One line in your app entry, and every `VetVisitButton` in the app opens your screen:
28
+
29
+ ```ts
30
+ import { registerEverfurTelevetFlow } from '@everfur/sdk/televet';
31
+
32
+ registerEverfurTelevetFlow(() => navigationRef.navigate('EverfurBooking'));
33
+ ```
34
+
35
+ - **New: `registerEverfurTelevetFlow`, `hasEverfurTelevetFlow`, `presentEverfurTelevetFlow`** on
36
+ `@everfur/sdk/televet`. A registry rather than an import, for the same bundler reason `registerEverfurDaily`
37
+ is one: the booking screens enter your graph through YOUR import of `@everfur/sdk/televet/booking`, so a
38
+ handoff-only integration is not billed for a scheduler it never renders. The `televet` subpath is still
39
+ inside its 13_600 gzip budget, and the supply-chain suite still proves that chat, records and televet carry
40
+ none of the booking closure.
41
+ - **New: `prefer` on `VetVisitButton`.** `'in-app'` (the default) opens the registered flow; `'redirect'` is
42
+ the explicit opt-in back to the browser handoff and ignores the registry entirely, which is what you want
43
+ for a surface you deliberately keep hosted.
44
+ - **Unchanged: `onStartInApp`.** It still works and now takes precedence over the registry, so an integration
45
+ that already wired it does not change twice in one upgrade.
46
+ - **Unchanged: `visitRef`.** A tapped push that names an existing visit still opens THAT visit through the
47
+ handoff. There is no in-app surface for one yet, and sending an owner into a fresh booking funnel because
48
+ they tapped a reminder would be the wrong screen.
49
+ - **A presenter that throws is not a dead button.** An unmounted navigation ref or a renamed route is caught,
50
+ the press falls through to the hosted visit, and your engineer is told through the console that the
51
+ navigation did not run.
52
+
53
+ **The in-app CALL needs native modules, and Expo Go cannot run it.** Booking in-app needs nothing native.
54
+ JOINING the consult in-app is `@everfur/sdk/televet/call`, which runs on the same Daily native modules the
55
+ Everfur consumer app uses: `@daily-co/react-native-daily-js`, `@daily-co/react-native-webrtc` and, on Expo,
56
+ the config plugin `@daily-co/config-plugin-rn-daily-js`. It needs a native rebuild (a JS-only OTA cannot add
57
+ a native module), camera and microphone permissions, and one `registerEverfurDaily(...)` line. **Expo Go
58
+ cannot load it at any version** - that is a property of Expo Go, not a missing package, so the way out is a
59
+ development build (`expo prebuild`, then run the native app) rather than another install. Until then the
60
+ call surface reports itself unavailable and `VetVisitButton` remains the way through.
61
+
62
+ ### Fixed: the native-call setup message names Expo Go
63
+
64
+ `EVERFUR_DAILY_SETUP`, handed to `onUnavailable` and warned once, listed the three packages and said "rebuild
65
+ the native app". An Expo Go developer believes they have no native app to rebuild and would repeat the one
66
+ step that cannot work, so the message now names Expo Go and the development build directly. Developer-facing
67
+ console and callback text only; no member-facing copy changed.
68
+
69
+ ### Still gated: the ported booking screens
70
+
71
+ `isTelevetBookingCopyReady()` continues to answer **false**, so `EverfurTelevetBooking`,
72
+ `EverfurTelevetReschedule` and `TelevetBookedScreen` still refuse to render a ported string and hand over to
73
+ the hosted visit. Registering a flow changes the DESTINATION; it does not open that gate. See
74
+ "the copy gate" in `16-TELEVET-VISITS.md` for what opens it.
75
+
76
+ ## [0.3.0] - 2026-09-21
77
+
78
+ ### Fixed: the deliverability gate now imports every web entry instead of only finding it
79
+
80
+ `npm run verify:pack` installs the packed tarball into a scratch React web project and, from this release,
81
+ IMPORTS all seven browser entries there (`@everfur/sdk/web`, `/web/records`, `/web/records/depth`,
82
+ `/web/televet`, `/web/consent`, `/web/notifications`, `/testing/web`) rather than asserting that a file
83
+ exists. A bundle that resolves and then throws on import used to pass; it now fails the gate.
84
+
85
+ The same gate read a piece of user-facing copy as an import. Its bare-specifier extractor matched the word
86
+ "from" at the end of a sentence (`{title:"Where this reading came from",close:"Close reading details"}`),
87
+ captured `,close:` as a package name, could not resolve it, and therefore SKIPPED executing
88
+ `@everfur/sdk/web/records` altogether. Captures are now required to be legal module specifiers, which no
89
+ fragment of prose is.
90
+
91
+ ### Changed: `EverfurChat` and `EverfurRecords` are the consumer app's screens, screen for screen
92
+
93
+ The chat and records surfaces (React Native, `@everfur/sdk/web` and the two frames) were rebuilt to match the
94
+ Everfur consumer app: no new dependency, no icon font, no clipboard, picker or camera module (those stay host
95
+ seams). Documented in `05-SDK-INTEGRATION.md` (What `EverfurChat` carries), `13-WEB-INTEGRATION.md` and
96
+ `08-API-REFERENCE.md`.
97
+
98
+ - **Chat, built in.** The pet header with its switcher (two or more pets from `GET /widget/v1/pets`, with pet
99
+ discs), the quick prompts, citation cards under a reply, the footer under every settled message (its time,
100
+ Copy, the `Helpful` / `Neutral` / `Not helpful` thumbs recorded through
101
+ `POST /widget/v1/messages/{message_id}/feedback`, the reasons sheet behind a thumbs-down), the urgency banner
102
+ with the calm `general` tier, the saved-conversation drawer (search, unread, load-on-scroll), the follow-up
103
+ check-in card, the typing indicator and the app's motion. `ChatMessage` gains `createdAt` (ISO 8601: the wire
104
+ `created_at` on a history row, the SDK clock on a live turn).
105
+ - **New `EverfurChat` props.** React Native: `onCopy` (your clipboard; without it no Copy control), `attachments`
106
+ (`{ pick(source), sources?, maxPerMessage? }`: your image picker; the queue mints the presigned policy at
107
+ `POST /widget/v1/uploads/initiate`, posts the bytes and sends `image_s3_keys`), `onPetChange`, `onVetPrep`,
108
+ `onFindVet`, `renderVetAction`. Web: `attachments` (on by default, `false` turns it off, `{ maxPerMessage }`),
109
+ `dictation` (Web Speech API, on by default, `false` is the kill switch; a custom iframe needs
110
+ `allow="microphone"`), `onPetChange`, `onVetPrep`, `onFindVet`, `renderVetAction`. The chat entry now carries
111
+ the televet chat-entry probe (the app's Find-a-vet CTA and vet-prep chip), which renders nothing while the
112
+ tenant's entry is closed; `EverfurChatAttachments` and `UrgencyDisplayLevel` are exported from the root.
113
+ - **Fonts per face.** `fonts: { regular?, medium?, bold?, mono? }` on the `theme` prop names one registered face
114
+ per weight, the way the app does, and every surface sets the family alone (never a synthetic `fontWeight` on a
115
+ named face). The SDK bundles no font. On the web, `font_urls` may name a woff2 per face on the Everfur SDK CDN
116
+ only; a font you serve is `@font-face` plus `fonts`.
117
+ - **Records.** `EverfurRecords` is the consumer records flow (dashboard, request, full record with the owner's
118
+ fact editor and share sheet, document, clinic picker, clinic-release consent, upload). The fact editor sends
119
+ the correction under the original value's typed slot (`value_num` as a number, `value_date`, `value_bool`,
120
+ else `value_text`); `RecordFactEditInput.correctedFields` is `Record<string, FactFieldValue>`.
121
+ - **Frame CSP.** The chat frame's `connect-src` names the media bucket's exact upload origin beside the API
122
+ (the staging publisher sets staging's), never an S3 wildcard.
123
+ - **Bundle lines.** The chat closures are raised to their measured size plus 5% (RN chat 43_000, `web/index.js`
124
+ 74_000, `frame.js` 138_000 gzip bytes); the records lines are unchanged.
125
+
126
+ ### Added: push and email to your members (`@everfur/sdk/notifications`, `@everfur/sdk/web/notifications`, `@everfur/sdk/server`)
127
+
128
+ Everfur now sends the notifications its own app would show a member (a records request moving, a record ready,
129
+ a visit booked, cancelled, completed, missed, rescheduled or due for a reminder, a follow-up check-in, a
130
+ vaccination due) to partner members itself, by push through your own provider and by email, beside the webhook
131
+ to your server. Everything ships dark behind `partner.notifications_enabled` (every route answers `404` until
132
+ Everfur turns it on for your client). The SDK never imports a push library: you obtain the token, it does the
133
+ rest. Documented in `docs/partner-integration/17-NOTIFICATIONS.md`.
134
+
135
+ - **New subpath `@everfur/sdk/notifications`** (React Native; `@everfur/sdk/web/notifications` is the same
136
+ surface for a page, with no web push in this version: pass `token: null` and use the preferences). Its own
137
+ entry, so the chat, records and televet closures are byte-identical to the previous build.
138
+ - **`useEverfurNotifications({ token, provider, platform, installationId? })`** registers the token for the
139
+ signed-in user on login (`POST /widget/v1/me/devices`), registers again when the token changes (unregistering the
140
+ previous device), keeps the device id per user in the SDK's per-user storage, and unregisters BEFORE the bearer
141
+ drops on `logout()`, `setUser(null)` and a user switch. `status` is `signed_out | no_token | registering |
142
+ registered | unavailable | failed | unregistered`; `register()`, `unregister()` and `setPreferences({ pushEnabled,
143
+ emailEnabled })` settle to an `EverfurResult`. On React Native a retryable failure is retried on the next
144
+ foreground. `useEverfurNotificationsClient()` is the same client for imperative use.
145
+ - **`createNotificationsClient(runtime)`**, bound from the subpath and not a runtime method: `registerDevice`,
146
+ `unregisterDevice`, `setPreferences`. A dark tenant is the ordinary not-found error; the route's own reasons
147
+ surface as `reasonCode`: `provider_unsupported` and `token_invalid` (`validationFailed`),
148
+ `device_cipher_unavailable` (`serviceUnavailable`, retryable). `NOTIFICATIONS_REASON_MESSAGES` and
149
+ `explainNotificationsReason(error)` give a DEVELOPER-facing sentence per reason (`credentials_missing` included);
150
+ `displayMessage` stays the copy a person sees. Supporting types: `NotificationsClient`, `NotificationsRuntime`,
151
+ `RegisterDeviceInput`, `RegisteredDevice`, `PushProvider`, `DevicePlatform`, `NotificationPreferencesInput`,
152
+ `NotificationPreferences`, `NotificationsReasonCode`, `EverfurNotificationsOptions`, `EverfurNotificationsState`,
153
+ `EverfurNotificationsStatus`.
154
+ - **`parseEverfurNotification(payload)`**, pure and total: reads the `data.everfur` routing block out of an
155
+ expo-notifications `Notification` or `NotificationResponse`, a Firebase `RemoteMessage` or a raw data map (a JSON
156
+ string through FCM) and returns `{ eventId, eventType, target }` with `target.kind` one of `record_request`,
157
+ `pet`, `visit`, `case`, or `null` for anything Everfur would not send (the sender's allow-list, mirrored).
158
+ `EVERFUR_NOTIFICATION_EVENT_TYPES`, `EVERFUR_NOTIFICATION_TARGET_KINDS`, `EVERFUR_NOTIFICATION_VERSION`,
159
+ `EverfurNotification`, `EverfurNotificationTarget`, `EverfurNotificationTargetKind`, `EverfurNotificationEventType`.
160
+ - **`setPartnerMemberNotificationProfile(client, { userRef, email, consent })`** on `@everfur/sdk/server`
161
+ (`PUT /partners/members/{user_ref}/notification-profile`, secret key): supplies a member's email with the consent
162
+ you collected (`{ givenAt, method: 'partner_attested', version }`, required) and settles to the MASKED profile.
163
+ Server-only by design; never on a device subpath (pinned by the supply-chain gate). Types
164
+ `PartnerMemberEmailConsent`, `PartnerMemberEmailConsentRecord`, `PartnerMemberNotificationProfile`,
165
+ `SetPartnerMemberNotificationProfileInput`.
166
+ - **`onBeforeLogout(hook)` on `EverfurRuntime`** (`@everfur/sdk/core`), the seam the hook uses: runs before a
167
+ signed-in user leaves, with a grace auth that still signs the request; `logout()` awaits it up to
168
+ `LOGOUT_HOOK_BUDGET_MS` (3 s) and never fails on it. `BeforeLogoutContext`, `BeforeLogoutHook`. The per-user KV
169
+ allowlist gains its first key, `everfur.notifications.device`, swept on logout after the hooks.
170
+ - `MockTransport` knows the three member notification routes.
171
+
172
+ ### Changed: the theme is resolved from four layers, and your `theme` prop now wins per field
173
+
174
+ - **Precedence reversed, per field (owner decision, 18 September 2026).** The resolved theme merges the compiled
175
+ default, the tenant's console branding (`render_hints.branding`, wire `schema_version: 2`), your `theme` prop and
176
+ the user's device preferences. A field the tenant lists in `locked_fields` (plus `hide_powered_by`, always) is
177
+ server-wins; every other cosmetic field is client-wins: your value overrides the console value and the console
178
+ fills what you leave unset. Before, the server won every field. `slots` in `locked_fields` locks every slot,
179
+ `slots.<name>` one. A branding with an unknown `schema_version` is ignored whole (default plus your `theme`).
180
+ `resolveTheme` documents the rule; `ResolveThemeInput` gains `textScale`, `scaleTypeSizes` and `highContrast`.
181
+ - **Dark palette derivation.** On the dark palette a colour with no `*_color_dark` now shows a DERIVED variant
182
+ (`deriveDarkVariant`, the byte-for-byte twin of the console's algorithm, pinned by shared golden vectors) rather
183
+ than the light colour. An explicit `primary_color_dark` / `accent_color_dark` still wins. `logo_url_dark` swaps
184
+ the logo on the dark palette.
185
+
186
+ ### Added: paint slots, the user's text size, high contrast, a preview helper
187
+
188
+ - **`slots` on the `theme` prop (and on the wire):** `composerInput`, `sendButton`, `userBubble`,
189
+ `assistantBubble`, `recordCard`, `vetVisitButton`, `primaryButton`, `link`, each accepting `backgroundColor`,
190
+ `borderColor`, `borderRadius`, `textColor` (the console's snake_case keys are read too). Applied by the React
191
+ Native and web chat, records, record depth and vet visit surfaces at those sites; structure and copy unchanged.
192
+ A slot's text colour is contrast-guarded against the background it supplies. `theme.slots` carries the resolved
193
+ map; `THEME_SLOT_NAMES`, `THEME_SLOT_KEYS`, `EverfurThemeSlotName`, `EverfurThemeSlot`, `EverfurThemeSlots`,
194
+ `EverfurThemeSlotInput`, `EverfurThemeSlotsInput` are exported.
195
+ - **Text size.** `type.scale` (the user's effective text scale) and `type.maxScale` (the cap, from
196
+ `accessibility_max_font_scale`, default 1.3, range 1 to 2) on the theme. React Native reads
197
+ `PixelRatio.getFontScale()` and every SDK `Text` / `TextInput` carries the cap as `maxFontSizeMultiplier`; the web
198
+ reads the root font size and the resolved `type.size` carries the scale (inline, no `<style>`).
199
+ - **High contrast.** `prefers-contrast: more` (web) resolves through `deriveHighContrast`: text and borders pushed
200
+ toward 7:1, translucent washes replaced with solid fills; `theme.highContrast` reports it. React Native 0.74 has
201
+ no signal for it and renders the normal palette.
202
+ - **`previewTheme(theme, hints, options?)`** on the root, `web` and `client` entries: the console's helper to
203
+ resolve a draft exactly as the device would.
204
+ - **`EverfurThemeInput`** is now a typed interface (every field named, unknown keys still tolerated), shared by
205
+ the React Native and web providers, the runtime and the frame loader (`Everfur.init({ theme })`); the frame
206
+ protocol carries the nested `slots` map (an additive change: `FRAME_PROTOCOL_VERSION` stays 1, and the rule for
207
+ when it moves is documented in `protocol.ts`).
208
+ - `@everfur/sdk/client` also exports `contrastRatio`, `parseThemeSlots`, `mergeThemeSlots`, `guardSlotContrast`,
209
+ `deriveDarkVariant`, `deriveHighContrast`, `LOCKABLE_THEME_FIELDS`, `THEME_SCHEMA_VERSIONS`,
210
+ `isKnownBrandingSchema`, `lockedFieldsOf`, `TEXT_SCALE_FLOOR`, `DEFAULT_MAX_FONT_SCALE`.
211
+
212
+ ### Added: login warm-up, so the token exchange adds no loading step of its own
213
+
214
+ - **`prepare(opts?)` on `useEverfur()` (and `EverfurRuntime`).** Call it from your own login flow: it mints the
215
+ session token, resolves the entitlement verdict into the store every gate reads and, with `{ petRef }`, primes
216
+ that pet's chat (conversation and suggested prompts, held for the first mount). It registers no pet. Concurrent
217
+ calls coalesce onto one run, a settled `ready` is returned again without I/O until the user or pet changes, and a
218
+ `partial` or `failed` run is retried on the next call. It never throws: it settles to a `PrepareResult`
219
+ (`status: 'ready' | 'partial' | 'failed'`, `durationMs`, and `session`, `entitlements`, `chat` as the same
220
+ `EverfurResult` shapes the surfaces settle to). Types: `PrepareOptions`, `PrepareResult`, `PrepareStatus`.
221
+ - **`warmUp` on `EverfurProvider`:** `'onMount'` runs `prepare({ petRef: activePet })` from the provider on mount and
222
+ on every user or pet change; the default `'manual'` issues nothing, so an unchanged integration behaves as before.
223
+ - **Pre-warmed mounts have no loading phase.** `useEverfurChat` and `EverfurChat` read a controller the registry
224
+ already holds for their scope synchronously on the first render (`CapabilityRegistry.peekChat`), so after
225
+ `prepare({ petRef })` the first commit is the transcript or the empty state with its prompts: no bootstrapping
226
+ frame, no delayed spinner. A cold mount is unchanged (the busy view, the spinner after 300 ms). `CapabilityGate`
227
+ and `useCapability` already read the resolved verdict synchronously; a verdict `prepare()` resolved renders the
228
+ children on the first commit with no skeleton.
229
+ - **`renderPending`** on `EverfurChat` and `EverfurRecords` (React Native and web), forwarded to their
230
+ `CapabilityGate`: draw your own placeholder, or nothing, where Everfur would draw its skeleton while the verdict
231
+ is pending, and, for chat, where it would draw its bootstrapping view. Absent, the defaults are unchanged.
232
+ - **Proactive token refresh.** `getToken` may now return `{ token, expiresAt }` (`MintedToken`; `expiresAt` in
233
+ epoch seconds as `mintPartnerSession` returns them, epoch milliseconds, a `Date` or an ISO string) as well as the
234
+ plain string. When the expiry is known the runtime re-mints about 60 seconds before it (at half the lifetime for a
235
+ shorter token), silently, under any turn in flight: no request pays a 401 round trip and no stream is restarted.
236
+ The reactive path (401, re-mint, replay once) stays as the fallback. `createIdentityProvider` takes an optional
237
+ `onMinted(expiresAtMs)` third argument.
238
+ - **`sdk.prepare.timing`** on the `telemetry` port, once per warm-up run: `{ durationMs, status }`, the
239
+ login-to-ready number.
240
+ - `ChatBootstrapResult.cause`: the typed error behind `error` or `warning`, null when the conversation was primed.
241
+ - `EntitlementPoller.prime()`: settle one verdict fetch (the poll already in flight when there is one), plus
242
+ `onFetch` / `onSettled` hooks on its deps.
243
+
244
+ ### Fixed: a cold records mount no longer flashes the off-state before the first verdict
245
+
246
+ - The entitlement store started resolved at the compiled floor, so until the first poll landed `EverfurRecords`
247
+ (and every capability the floor does not grant) rendered the fail-closed off-state ("not available on your
248
+ account") and then retracted it; the skeleton `useCapability` documented never happened. The store is now
249
+ `isPending` while the FIRST verdict of a scope is being fetched (and again after every user or pet re-scope), and
250
+ `decideCapability` pends only what the decision set leaves undecided: a required key absent from the set shows
251
+ the gate's skeleton (or `renderPending`), a key the floor grants (chat) renders at once, a key explicitly denied
252
+ (a disabled runtime) is the off-state at once. Any settlement clears it: a verdict that leaves a capability off,
253
+ or a fetch that fails, is the fail-closed off-state, never a skeleton left up. A runtime that never fetches (an
254
+ injected transport without `startEntitlementPolling`) keeps its floor as before, and a verdict `prepare()`
255
+ resolved before mount still renders on the first commit. `EntitlementSnapshot.isPending` and
256
+ `DecideOptions.isPending` carry the new meaning; the gate is unchanged.
257
+
258
+ ### Changed: webhook event types match the wire
259
+
260
+ - **`record_request.updated` carries `simple_status`, not `status`.** The PREVIEW type named a `status` field the
261
+ platform never emitted; the producer (`records_partner_events.py`, pinned by its tests) writes `simple_status`
262
+ (the seven-value records status model, typed `RecordRequestSimpleStatus`) and `simple_status_detail`. The type
263
+ now matches, and gains the fields the producer added: `user_ref`, `action_needed_label`, `can_retry_send`,
264
+ `can_update_clinic_email`, `can_revoke`, `can_convert_to_upload` and `record_updated_at`. A handler reading
265
+ `.status` off this payload was reading `undefined`; there is no alias because the field did not exist.
266
+ - **Visit events name the visit `visit_ref`** (the PREVIEW type said `partner_visit_ref`, which the producer never
267
+ wrote), with `pet_ref` (nullable), `status` (`EverfurVisitStatus`), `scheduled_at` and `occurred_at`.
268
+ - **Every payload carries its `object` tag** (`member`, `visit`, `record_request`, `record`, `webhook_endpoint`,
269
+ ...), and `webhook_endpoint.test` names its `endpoint_id`. `record.ready` gains `user_ref`.
270
+
271
+ ### Added: six webhook event types (preview, dark on the server)
272
+
273
+ - **`pet.vaccination.due`** (`PetVaccinationDuePayload`: `vaccine` `rabies` | `dhpp`, `due_at` `YYYY-MM-DD`,
274
+ `stage` `due_in_14d` | `due_in_3d` | `overdue`), **`visit.rescheduled`** (`previous_scheduled_at`),
275
+ **`visit.reminder`** (`stage` `24h` | `30m`), **`visit.followup_sent`** (`sent_at`), **`chat.follow_up_due`**
276
+ (`case_ref`, `conversation_id` or null, `due_at`) and **`chat.urgency.flagged`** (`conversation_id`,
277
+ `message_id`, `urgency_level`; the platform writes no `pet_ref` on this one). All in
278
+ `EVERFUR_WEBHOOK_EVENT_TYPES` and `EverfurWebhookPayloadMap`, so `constructEvent` narrows each on `type`. Types:
279
+ `EverfurVaccine`, `EverfurVaccinationDueStage`, `EverfurVisitReminderStage`, `EverfurVisitStatus`,
280
+ `VisitRescheduledPayload`, `VisitReminderPayload`, `VisitFollowupSentPayload`, `ChatFollowUpDuePayload`,
281
+ `ChatUrgencyFlaggedPayload`, `RecordRequestSimpleStatus`.
282
+
283
+ ### Added: the consumer request fields, the request list and the step rail
284
+
285
+ - **`RecordsRequestView`** gains the fields the consumer request wire carries: `wireStatus` (the server's own
286
+ status vocabulary, structural only), `source`, `actionNeededLabel`, `failureReason`, `failureCode`,
287
+ `recoveryStage`, `showClinicPhone`, `clinicName`, `clinicEmail`, `clinicPhone`, `convertedToUploadAt` and
288
+ `requestGroupId`. All optional on the type; the mapper always fills them.
289
+ - **`EverfurRecords` (React Native and web) shows every request for the pet, newest first**, one card each: the
290
+ clinic name, the action-needed sentence in place of the status label when the request needs the member, the
291
+ Everfur app's step-wise rail for the request (ported from the consumer app's `requestTimeline.ts`, titles and
292
+ details verbatim), a tap-to-call card when the server gates the clinic phone on, and the recovery actions.
293
+ Exports on both records subpaths: `buildRequestTimeline`, `REQUEST_TIMELINE_TITLES`, `selectRequestList`,
294
+ `selectPrimaryLabel`, `selectClinicCall`, `selectRequestTitle`; types `RequestTimelineEvent`,
295
+ `RequestTimelineEventKey`, `RequestTimelineEventState`, `ClinicCall`.
296
+ - `selectPopulatedBlocks` now counts any request as timeline content (the rail exists the moment a request does).
297
+
298
+ ### Added: several documents in one pick
299
+
300
+ - **`onPickDocument` may resolve an array of `FileHandle`** (`RecordsDocumentPicker`), and the web file input is
301
+ `multiple`. **`uploadDocuments(requestId, files)`** on `useEverfurRecords` uploads them one at a time on one
302
+ request, continues past a failed file, and skips a non-PDF or oversize file with a reason before any request
303
+ is made; **`uploadQueue`** (`UploadQueueItem`, `UploadSkipReason`) is the row per file both surfaces render with
304
+ the consumer's own labels and skipped-files banner. `uploadDocument` (one file) is unchanged.
305
+
306
+ ### Added: recent clinics and the request group
307
+
308
+ - **`recentClinics({ limit })`** on `RecordsClinicController` (`GET /widget/v1/records/clinics/recent`,
309
+ `RecordsRecentClinicsInput`): the clinics this member requested from before, shown as `Recently used` in both
310
+ clinic pickers while no search is active.
311
+ - **`getRequestGroup(id)`** on `RecordsClinicController` (`GET /widget/v1/records/request-groups/{id}`, dark
312
+ behind `partner.records_multi_clinic_enabled`): the batch rollup as `RecordsRequestGroupView` (`total`,
313
+ `responded`, `pending`, `members` mapped like requests).
314
+
315
+ ### Added: the multi-clinic composer
316
+
317
+ - **`EverfurClinicRequestBatch`** on `@everfur/sdk/records` and `@everfur/sdk/web/records`
318
+ (`EverfurClinicRequestBatchProps`, `ClinicBatchBaseProps`, `ClinicDraft`, `RegisterSignaturePng`): up to ten
319
+ clinics from search, the recent list or a typed name, one owner name, one signature captured at Send and
320
+ registered once (its id sent for every clinic), the per-clinic outcome with the consumer's sentences, then the
321
+ group rollup. **`useEverfurRecordsClinicBatch()`** is its client over the provider's session; the batch
322
+ client and its types are re-exported on both records subpaths. `CaptureRecordsSignature` may now return
323
+ `{ signaturePngBase64 }` as well as `{ signatureId }` (`CapturedRecordsSignature`).
324
+
325
+ ### Added: a built-in signature pad for React Native
326
+
327
+ - **`SignaturePad`** on `@everfur/sdk/records`: draw with the finger or type the name, with no native dependency.
328
+ `EverfurClinicRequest` and `EverfurClinicRequestBatch` use it when the host passes no `onCaptureSignature`.
329
+ The drawn strokes (or the typed name, in a compact bitmap font) become a one-bit PNG through the SDK's own
330
+ encoder (`encodeSignaturePng`, `rasterizeStrokes`, `rasterizeTypedName`, `SIGNATURE_CANVAS` on
331
+ `@everfur/sdk/core`): a few hundred bytes, sent inline for one clinic and registered once
332
+ (`registerRnSignature`, a `data:` form part, no file-system peer) for a batch.
333
+
334
+ ### Added: record depth on its own subpaths (preview, dark on the server)
335
+
336
+ - **`@everfur/sdk/records/depth`** and **`@everfur/sdk/web/records/depth`**: `EverfurRecordsTimeline`,
337
+ `EverfurRecordDepth` (lab work, visits, physical exams, visit notes) and `EverfurDocumentContributions`, with
338
+ `useRecordsDepthRead`, `useEverfurRecordsDepthClient`, `buildTimelineRows` and the mappers. The client is
339
+ **`createRecordsDepthClient`** on `@everfur/sdk/core` (`getRecordDepth`, `getTimeline`,
340
+ `getDocumentContributions`). Everything answers not found (or `available:false` on the record read) until
341
+ Everfur switches `partner.records_full_depth_enabled` on; a withheld answer renders nothing. Types:
342
+ `RecordDepthView`, `RecordsTimelineView`, `RecordDocumentContributionsView`, `RecordLabMarker`,
343
+ `RecordLabPoint`, `RecordEncounter`, `RecordClinicianNote`, `RecordClinicianNoteSection`,
344
+ `RecordPhysicalExam`, `RecordExamSystem`, `RecordsTimelineVisit`, `RecordsTimelineVaccine`,
345
+ `RecordsTimelineWeight`, `RecordsTimelineMedication`, `RecordsDepthClient`, `TimelineRow`,
346
+ `TimelineRowKind`, `UseRecordsDepthState`, `RecordsDepthStatus`.
347
+
348
+ ### Fixed: follow-up prompts follow each reply
349
+
350
+ - **`suggestedPrompts` now carries the reply's own follow-ups.** The done frame's `follow_up_questions` were
351
+ parsed (`DoneFrame.followUpQuestions`) and then dropped, so `EverfurChat` on both hosts kept offering the
352
+ bootstrap prompts after every reply. After a reply, `ChatSnapshot.suggestedPrompts` (and the hook's
353
+ `suggestedPrompts`) is that reply's list, trimmed and de-duplicated like the bootstrap set; a reply without any
354
+ falls back to the bootstrap prompts. A bootstrap that settles after the first reply becomes the fallback rather
355
+ than replacing what the reply offered.
356
+
357
+ ### Added: follow-up check-ins in the thread (preview, dark on the server)
358
+
359
+ - **`ChatMessage.proactiveOriginRef` and `ChatMessage.checkin`.** The platform's follow-up check-in ("How are
360
+ those ears doing?") arrives as an assistant message carrying `proactive_origin_ref`
361
+ (`checkin:<case_id>:<YYYY-MM-DD>`); history used to drop it. The raw ref stays on the message and `checkin`
362
+ (`ChatCheckin`: `caseId`, `answer`, `resolutionText`) names the answerable case. A ref that is not a well-formed
363
+ check-in leaves an ordinary message.
364
+ - **`EverfurChat` renders a check-in as an answerable card** on React Native and the web: the question verbatim,
365
+ the consumer app's Better / Same / Worse answers, the in-flight answer busy with the row disabled, an answered
366
+ card settled with the confirmation and the server's own resolution line, a failed tap's safe `displayMessage`
367
+ with the row still answerable. The answer is recorded, never posted into the thread. A surface with no pet in
368
+ scope shows the question without answers, because the route is pet-scoped.
369
+ - **`answerCheckin(caseId, answer, opts?)`** on `ChatController` and `useEverfurChat`: posts
370
+ `POST /widget/v1/pets/{pet_ref}/cases/{case_id}/checkin` (`{ response }`) for the scoped pet (or `opts.petRef`)
371
+ and reflects the outcome on the message. A stale, closed or foreign case is the ordinary not-found error;
372
+ without a pet it settles to `validationFailed` before any I/O.
373
+ - **`createCasesRepository(auth)`** on `@everfur/sdk/client` (`CasesRepository`: `list(petRef)`,
374
+ `checkin(petRef, caseId, answer)`), the typed client for `GET /widget/v1/pets/{pet_ref}/cases` and the check-in
375
+ route, with `CHECKIN_ANSWERS`. Types: `CheckinAnswer`, `CheckinOutcome` (`case`, `ctaDeepLink`, set only on a
376
+ WORSE escalation), `WatchCase`. The routes are dark until Everfur switches the partner flag on; the wire shapes
377
+ follow the consumer `memory_watch_case_router.py` and are to be reconciled against the widget serializer.
378
+ - **`petRef`** on `useEverfurChat`'s state: the pet the surface is scoped to, null when unscoped.
379
+
380
+ ### Added: unread (preview, dark on the server)
381
+
382
+ - **`ChatConversation.unreadCount`** from the list's `unread_message_count` (null when the wire does not carry
383
+ it, which is not "all read"), **`ChatSnapshot.unreadCount`** for the active thread (the count the list last
384
+ reported for it, 0 once read), and **`markRead()`** on `ChatController` and `useEverfurChat`:
385
+ `POST /widget/v1/conversations/{id}/read`, idempotent, zeroes the count; a missing or foreign thread is the
386
+ ordinary not-found error. `resumeConversation` marks a thread read when the list had reported it unread; a
387
+ thread the list never named sends no receipt.
388
+
389
+ ### Added: server pet writes (preview, dark on the server)
390
+
391
+ - **`upsertPartnerPet`, `updatePartnerPet` and `deletePartnerPet`** on `@everfur/sdk/server`, next to
392
+ `mintPartnerSession`: create, change and delete a member's pet profile from your backend with your secret key
393
+ (`PUT`, `PATCH` and `DELETE /partners/members/{user_ref}/pets/{pet_ref}`), no user session needed.
394
+ `upsertPartnerPet` creates the member on first use; the other two never do. A restricted key needs
395
+ `pets.profile.update` (upsert, update) or `pets.profile.delete`. Every call answers not found until Everfur
396
+ switches the server pet API on. Types: `PartnerServerClient`, `PartnerPetTarget`, `PartnerPetProfile`,
397
+ `PartnerPet`.
398
+
399
+ ### Added: multi-clinic records request
400
+
401
+ - **`createRecordsClinicBatchClient(deps)`** on `@everfur/sdk/core`: the typed client for
402
+ `POST /widget/v1/records/clinic-requests/batch`, one signed owner action for one pet and 1 to 10 clinics,
403
+ each with its own clinic, signature and idempotency key, answered per clinic (`created` with the mapped
404
+ request, or `failureCode` and `failureReason`) under a shared `requestGroupId`. Published on `core` rather
405
+ than the records subpaths, whose closure is at its size budget; it rides the same auth, funnel and error
406
+ policy as `useEverfurRecordsClinic`. The route is dark until Everfur switches
407
+ `partner.records_multi_clinic_enabled` on. Types: `CreateClinicRequestBatchInput`, `RecordsClinicBatchItem`,
408
+ `RecordsClinicBatch`, `RecordsClinicBatchResult`, `RecordsClinicBatchClient`, `RECORDS_CLINIC_BATCH_MAX`.
409
+
410
+ ### Added: React Native records upload
411
+
412
+ - **`createRnRecordsUploadTransport(options?)`** on the root `@everfur/sdk`: the React Native `UploadTransport`
413
+ for records. It posts the picked document from its local file `uri` to the presigned upload with `fetch` and
414
+ `FormData` (`{ uri, name, type }`), with no native dependency. Pass it as `uploadTransport` on `EverfurConfig`.
415
+ It is exported from the root rather than `@everfur/sdk/records` because that subpath's bundle budget has no
416
+ room for it. `onProgress` reports completion only.
417
+ - **`EverfurRecords` (`@everfur/sdk/records`) takes an `uploadTransport` prop**, overriding the config one for
418
+ that surface. `useEverfurRecords` takes the same `uploadTransport` option. The upload control is enabled
419
+ only when both `onPickDocument` and a transport are wired.
420
+ - **`RecordsDocumentPicker`** (type, on `@everfur/sdk` and `@everfur/sdk/records`): the contract
421
+ `onPickDocument` implements with the host app's own picker. Resolve `null` on cancel, otherwise a
422
+ `FileHandle` with the local `uri`, `mimeType: 'application/pdf'` and `sizeBytes`. The SDK does not depend on
423
+ a document picker.
424
+
425
+ ### Added: records test mode (sandbox tenants only)
426
+
427
+ - **`useEverfurRecordsSandbox()`** on `@everfur/sdk/testing/rn` and `@everfur/sdk/testing/web` returns a
428
+ `RecordsSandboxController`: `simulateClinicReply(requestId, 'records' | 'no_records' | 'declined')` and
429
+ `publishSampleRecord(requestId)`, each settling to `{ result: 'applied' | 'already_applied' | 'not_applicable',
430
+ request }`. They call `POST /widget/v1/records/sandbox/requests/{request_id}/clinic-reply` and
431
+ `.../publish-sample`, which answer only a sandbox tenant's signed user for that user's own request; anywhere
432
+ else the call settles to the ordinary not-found error. Types: `RecordsSandboxController`,
433
+ `RecordsSandboxClinicReply`, `RecordsSandboxResult`, `RecordsSandboxActionResult`. Kept off the production
434
+ records entries.
435
+
436
+ ### Added: vet visit test mode (sandbox tenants only)
437
+
438
+ - **`useEverfurVetVisitSandbox()`** on `@everfur/sdk/testing/rn` and `@everfur/sdk/testing/web` returns a
439
+ `VetVisitSandboxController`: `createVisit(petRef)` and `advanceVisit(visitRef, 'booked' | 'cancelled' |
440
+ 'completed' | 'no_show')`, each settling to `{ result: 'applied' | 'already_applied' | 'not_applicable', visit }`.
441
+ They call `POST /widget/v1/televet/sandbox/visits` and `.../visits/{visit_ref}/advance`, which answer only a
442
+ sandbox tenant's signed user for that user's own pets and visits; anywhere else the call settles to the ordinary
443
+ not-found error. A simulated visit has no payment, vet or appointment, and sends the real `visit.*` webhooks
444
+ with `livemode: false`. Types: `VetVisitSandboxController`, `VetVisitSandboxStatus`, `VetVisitSandboxResult`,
445
+ `VetVisitSandboxVisit`, `VetVisitSandboxActionResult`. Kept off the production televet entries.
446
+
447
+ ### Added: pet update, pet delete and user erasure
448
+
449
+ - **`updatePet(pet, patch)`**, **`deletePet(pet)`** and **`eraseUserData()`** on the `useEverfur()` handle
450
+ (and `EverfurRuntime`), next to `registerPet`, and on `PetsRepository` (`@everfur/sdk/client`).
451
+ `updatePet` sends `PATCH /widget/v1/pets/{pet_ref}`: an omitted field is unchanged and `null` clears it
452
+ (new type `PetProfilePatch`); an empty change is refused before the request. `deletePet` sends
453
+ `DELETE /widget/v1/pets/{pet_ref}`. `eraseUserData` sends `DELETE /widget/v1/me`, once per call with a
454
+ 60 second deadline; a failure (503) is retryable and a repeat call resumes the erasure. It does not sign the
455
+ user out. An unknown pet settles to the not-found error.
456
+
457
+ ### Changed: version
458
+
459
+ - `package.json` and `SDK_VERSION` (the `X-Everfur-SDK-Version` header) are `0.3.0`.
460
+
461
+ ### Changed: potentially breaking for exhaustive switches
462
+
463
+ - `RecordsRequestStatus` gains `'requested'` and `'awaiting_clinic'`, the two clinic-request stages before a
464
+ document arrives. The wire change is additive, but a host `switch` that is exhaustive over the old seven
465
+ members stops compiling (or falls through at runtime). Give such a switch a default branch.
466
+ - `CapabilityName` gains `'recordsClinic'` (see below). The same note applies to an exhaustive switch over it.
467
+
468
+ ### Added: vet visit return, state check and Chat entry (preview, dark on the server)
469
+
470
+ - `useVetVisit` and `VetVisitButton` (`@everfur/sdk/televet`, `@everfur/sdk/web/televet`) take three optional
471
+ settings. Each is off on the Everfur side until Everfur switches it on for your tenant, and none changes a
472
+ visit started without it.
473
+ - `returnTo`: the key of a return destination your tenant registered with Everfur (never a URL). The handoff
474
+ then carries an opaque `flowId`, `open()` settles with `{ opened: true, flowId }` and `onOpened` receives
475
+ it. `parseVetVisitReturn(url)` reads the flow id back off your destination URL and returns nothing about
476
+ the visit, the pet or the account.
477
+ - `usState`: the user's US state. `available` (and the button) waits for Everfur to confirm a visit is
478
+ available there. The answer is only available or not.
479
+ - `entry: 'chat'`: the vet entry inside Chat, for the pet the chat is about (`petRef` is required). It is the
480
+ same button, label, hosted visit and checkout, shown only while Everfur has the Chat entry switched on.
481
+ Nothing is written back into the chat.
482
+ - `VetVisitController.createHandoff(petRef, options?)` gains the optional `options`, and the interface gains
483
+ the optional `checkAvailability(state)` and `chatEntryOpen()`. An injected controller without them keeps
484
+ working; wherever the hook needs their answer it treats the entry as unavailable.
485
+ - `onOpened` now receives `flowId: string | null`. A callback that takes no argument is unaffected.
486
+ - Types `VetVisitAvailability`, `VetVisitEntry`, `VetVisitHandoffOptions` and `VetVisitReturn`, and
487
+ `VetVisitHandoff.flowId`, are exported from both televet subpaths.
488
+
489
+ ### Added: chat history
490
+
491
+ - `useEverfurChat` and the chat controller gain `listConversations(options?)` (optionally filtered by
492
+ `petRef`), `getMessages(id, options?)`, `resumeConversation(id)` and `loadOlderMessages()`, plus
493
+ `historyCursor` and `isLoadingHistory`. They call the existing `GET /widget/v1/conversations` and
494
+ `GET /widget/v1/conversations/{id}/messages` routes. Cursors are opaque and older pages load backwards.
495
+ - A supplied `conversationId` now loads that thread instead of starting from an empty transcript.
496
+ - The prebuilt React Native and web chat surfaces gain a history panel. Nothing is fetched until it is opened.
497
+ On the web it is a dialog that keeps Tab inside it and returns focus to its trigger when it closes.
498
+ - Switching the user, the pet or the thread hides the previous transcript on the same render, and late
499
+ results or callbacks from the previous scope are dropped. Unsent drafts are kept per user, pet and thread.
500
+ - The urgency of the final assistant reply, and of saved history, is shown with the existing consumer labels.
501
+ It never opens a vet visit or an emergency action by itself.
502
+ - Types `ChatConversation`, `ChatConversationListOptions`, `ChatConversationPage`, `ChatHistoryOptions` and
503
+ `ChatMessagePage` are exported from the root, `@everfur/sdk/chat` and `@everfur/sdk/web`.
504
+
505
+ ### Added: clinic record requests and request recovery (preview)
506
+
507
+ - **`EverfurClinicRequest`** (`@everfur/sdk/records` and `@everfur/sdk/web/records`) and
508
+ **`useEverfurRecordsClinic`** (a `RecordsClinicController`): search the clinic directory, show the
509
+ authorization text and version the server serves, and send a signed request to a clinic for one pet.
510
+ Nothing is sent on mount, and the owner signs only after pressing Send. The web surface draws or types the
511
+ signature and uploads it through the two-phase signature upload; React Native asks the host's
512
+ `onCaptureSignature` for a registered signature id, so no native dependency is added. One request keeps
513
+ one idempotency key across uncertain retries, and non-idempotent writes are never retried automatically.
514
+ - **New capability `recordsClinic`** (`records.clinic.create`). Contacting a clinic is a separate grant from
515
+ owner uploads, so the clinic form renders the deliberate off state unless it is granted, while
516
+ `EverfurRecords` keeps following `records`.
517
+ - Request views gain the server's own recovery decisions: `canRetrySend`, `canUpdateClinicEmail`,
518
+ `canRevoke` and `canConvertToUpload`, with `dispatchStatus`, `reminderCount`, `lastReminderAt`,
519
+ `waitingExpectation`, `simpleStatus` and `simpleStatusDetail`. The prebuilt records surfaces offer an
520
+ action only when the server allows it. After a request is converted to an upload, the next upload fills
521
+ that same request.
522
+
523
+ ### Fixed and changed: partner events inbound (preview)
524
+
525
+ - **Fixed:** `sendPartnerEvent` posts to `POST {apiBaseUrl}/partners/events`, the route the platform serves. It
526
+ used `/partner-events/v1/inbound`, which does not exist.
527
+ - **Changed (breaking for this preview API):** `PartnerInboundEvent` is a union keyed on `type`. Each type's
528
+ `data` is typed to the platform's closed schema through `PartnerInboundEventOf`, `PartnerInboundEventDataMap`
529
+ and the seven `...Data` interfaces, and `schemaVersion` is `1`.
530
+ - **Changed:** `sendPartnerEvent` throws `EverfurConfigError` before sending for an idempotency key that is not
531
+ 1 to 255 visible ASCII characters, a secret key that is not `sk_partner_` or `rk_partner_`, a pet event without
532
+ `petRef` or a member event with one, a `petRef` that breaks the user_ref rule, an `occurredAt` string without an
533
+ explicit offset, `data` that cannot be serialized, or a body over 32 KB.
534
+ - **Added:** `PartnerEventReceipt` carries `type`, `schemaVersion`, `userRef`, `petRef`, `occurredAt` and
535
+ `livemode`.
536
+ - Docs: chapter 15 documents the served inbound route, each type's fields, the receipt, every error code with
537
+ its normalized `result.error.code`, and the retry hazard of omitting the idempotency key. It also corrects
538
+ the outbound list path to `GET /api/v1/partners/events`. Chapter 09 gains "Records in Chat".
539
+
540
+ ### Added: error reason codes
541
+
542
+ - `EverfurError.reasonCode` (optional): the server's stable `reason_code` sub-reason when a response carries one,
543
+ for example `pet_not_registered` or `species_unsupported` on the records routes. Safe to branch on. Only a
544
+ lowercase snake_case token is kept.
545
+
546
+ ### Changed
547
+
548
+ - The contract snapshot is re-vendored from the platform's public projection (digest `008a6b67...`). The
549
+ wire codes `insufficientCapability`, `idempotency_key_reused`, `idempotency_key_invalid`,
550
+ `idempotency_key_required`, `payload_too_large`, `event_cursor_invalid`, `event_filter_invalid`,
551
+ `event_payload_invalid` and `event_type_unknown` now fold to `accessDenied`, `idempotencyConflict` or
552
+ `validationFailed`. `televet.visit.create` is declared by the contract, so the SDK no longer carries it
553
+ as a pending key.
554
+ - Published bundles are minified with function and class names and legal comments kept. Source maps are
555
+ still built privately and still excluded from the package.
556
+
557
+ ### Added: vet visits (dark)
558
+
559
+ - **`@everfur/sdk/web/televet`** and **`@everfur/sdk/televet`**: `VetVisitButton`, `useVetVisit` and
560
+ `vetVisitErrorReasonOf`. A tap mints a single-use handoff (`POST /widget/v1/televet/handoffs`, signed
561
+ session only) and opens the hosted Everfur visit: a tab opened synchronously inside the click on the web,
562
+ the system browser (or a host `openUrl`) on React Native. No retry, and the URL never reaches telemetry.
563
+ Behind the new `televet` capability (`televet.visit.create`), which no tenant is granted yet, so the button
564
+ renders nothing; it also renders nothing without a `user`, since the route refuses a publishable-key caller.
565
+ The web entry is separate from `@everfur/sdk/web`, which carries none of it.
566
+ - `CapabilityName` gains `'televet'`. `requires('televet')` names `televet.visit.create`, which the public
567
+ contract now declares.
568
+ - Docs chapter 16, vet visits (draft for owner approval).
569
+
570
+ ### Added: records in frame mode, and the records web docs
571
+
572
+ - **`surface: 'records'` on `Everfur.init`** (and `data-surface="records"` on the script tag) opens the
573
+ records surface in frame mode. It has its own frame document and bundle on the CDN (`records.html`,
574
+ `records-frame.js`, beside `frame.html` and `frame.js` of the same build): the same fence, the same
575
+ private channel and the same token channel, with the upload host (the media bucket the API presigns to)
576
+ in the document's `connect-src` beside the API. The chat document keeps its narrower policy and carries
577
+ none of the records code (its bundle ceiling is unchanged). `consentVersion` (`data-consent-version`)
578
+ travels with the init so the frame can render the Allow control; `activePet` names the pet whose
579
+ records the frame shows, and `setActivePet` moves it. Without a user the frame shows the sign-in state,
580
+ without a pet the empty state, and until Everfur enables records for the account the off-state; none of
581
+ these issues a records request.
582
+ - **Protocol:** `ef:init` gains optional `surface` (`chat` or `records`; absent means chat) and
583
+ `consentVersion`; a frame document asked for the surface it does not serve refuses with the new
584
+ `surfaceMismatch` code before the fence is asked, and the loader treats it as fail-closed (the frame is
585
+ hidden). The protocol version stays 1: loader and frames ship as one build.
586
+ - **CDN build:** `EVERFUR_UPLOAD_ORIGIN` (`scripts/cdn-build-env.mjs`) names the one bucket origin the
587
+ records document may POST to; production's by default, the staging publisher sets staging's. It must be
588
+ an exact https origin, never a wildcard or the bare S3 service host. Both publish workflows verify and
589
+ publish the two new artifacts.
590
+ - Docs: chapter 09 gains the web section, chapter 13 the `@everfur/sdk/web/records` in-page section and
591
+ the records frame snippet, and "React Native only" no longer covers records. `examples/minimal-web-records.tsx`
592
+ is the copy-paste shape, type-checked by `docs:check`. The prod CDN publish of the records document waits
593
+ for the media bucket CORS rule for `sdk.everfur.com` (EFBackend INFRA-1); staging is unaffected.
594
+
595
+ ### Added: records on the web (preview)
596
+
597
+ - **`@everfur/sdk/web/records`**, an opt-in subpath: `EverfurRecords` (the four-state DOM surface: consent,
598
+ upload, request timeline, record view), `useEverfurRecords` (the hook React Native already uses),
599
+ `createWebRecordsUploadTransport` for `EverfurConfig.uploadTransport`, and `fileToHandle` / `pickDocument`.
600
+ Its own entry so a chat-only page downloads none of it. The subpath maps the `react-native` condition to
601
+ null like `./web`. The surface renders the gate's off state until Everfur enables records for the account,
602
+ and needs a signed-in user. Its upload control is disabled until the host sets
603
+ `uploadTransport: createWebRecordsUploadTransport()` on the config, and it comes back after a failed
604
+ upload so the next pick fills the same request rather than creating a second one.
605
+ - **`FileHandle.blob`** (optional, additive): the bytes of a browser `File`. The type of an uploaded
606
+ document is read from its first bytes (`%PDF-`), not from the file name, so a renamed image is refused
607
+ before any request is made. No object URL is created for a picked document.
608
+
609
+ ### Added: partner events on `@everfur/sdk/server/events` (preview)
610
+
611
+ - **`constructEvent(payload, signatureHeader, secrets, options?)`** verifies an Everfur webhook delivery and
612
+ returns the typed event. It checks the `Everfur-Signature: t=<unix>,v1=<hex>` header (HMAC-SHA256 over
613
+ `t.` plus the raw body, keyed by the whole `whsec_` secret) in constant time, accepts any `v1` against any
614
+ of several secrets so a rotation never drops a delivery, and refuses a timestamp more than 300 seconds off
615
+ in either direction. Refusals throw `EverfurWebhookVerificationError` with a typed `code`
616
+ (`malformed_header`, `timestamp_outside_tolerance`, `no_matching_signature`, `malformed_payload`); a call
617
+ that can never succeed (no secret, a parsed body) throws `EverfurConfigError`. Pinned by the platform's own
618
+ fixed signature vectors. `generateTestSignatureHeader` signs a body the same way for a partner's tests.
619
+ - **Typed events:** `EverfurWebhookEvent` covers `member.linked`, `member.unlinked`, `visit.booked`,
620
+ `visit.cancelled`, `visit.completed`, `visit.no_show` (keyed by `partner_visit_ref`), `record_request.updated`, `record.ready` and
621
+ `webhook_endpoint.test`; `isEverfurWebhookEvent` separates a type added after this version shipped. Payload
622
+ fields are preview.
623
+ - **`sendPartnerEvent(client, event, idempotencyKey?)`** (preview) sends one event to
624
+ `POST /partner-events/v1/inbound` with the partner secret key and an `Idempotency-Key` (generated and
625
+ returned when omitted), and settles to an `EverfurResult`. The route is not served yet.
626
+ - **New subpath `@everfur/sdk/server/events`.** Server-only and Node-only (it loads Node's `crypto`), blocked
627
+ for React Native like `./server`. `@everfur/sdk/server` is unchanged and still loads no `node:` built-in, so
628
+ `mintPartnerSession` keeps running on edge runtimes.
629
+ - Docs: new chapter 15, Events and webhooks; chapter 09 no longer says polling is the only mechanism forever.
630
+
631
+ ## [0.2.0] - 2026-09-14
632
+
633
+ ### Added: the web
634
+
635
+ - **`@everfur/sdk/web`**, the browser entry: `EverfurProvider`, `CapabilityGate`, `EverfurChat`,
636
+ `useEverfurChat` and the theme hook, rendered with `react-dom` (peer, 18 or newer). The provider
637
+ lifecycle, the context, the theme resolution and the headless chat hook are one host-agnostic React layer
638
+ shared with React Native; the DOM leaves are new. Styling is inline from the theme tokens through the
639
+ CSSOM, so a strict `style-src` on the host page cannot strip it and a host stylesheet cannot collide with
640
+ it; motion runs on the Web Animations API and goes static under `prefers-reduced-motion`; brightness
641
+ follows `prefers-color-scheme`. The subpath withholds the `react-native` export condition and ships a
642
+ guard that throws on a resolver that ignores conditions, the way `./server` does.
643
+ - **The chat surface on the DOM.** Enter sends and Shift+Enter breaks the line (an IME Enter passes
644
+ through); the transcript stays pinned to the newest message unless the reader scrolled up; a tapped
645
+ prompt returns focus to the composer; one persistent status region announces "Assistant is typing" and
646
+ then the settled reply once (the transcript is deliberately not a live region); errors are alerts; every
647
+ control is 44 px and keeps the UA focus ring. Assistant replies render as text nodes with `strong` /
648
+ `em` / `code` runs, never as HTML.
649
+ - **Frame mode.** A React-free loader (`https://sdk.everfur.com/v1/everfur.js`, 2.5 KB gzip, the
650
+ `Everfur` global, an async pre-load queue and data-attribute auto-init) puts the surface in an iframe on
651
+ `sdk.everfur.com`. The frame refuses to initialise unless the Everfur API confirms that the embedding
652
+ page's origin is registered for the key the page names (`GET /widget/v1/embed-config`, exact match,
653
+ asked of the API and never read from the page); it fails closed when the API cannot be asked. After the
654
+ handshake, configuration, commands and short-lived session tokens travel on a private `MessageChannel`
655
+ only the loader holds, so other scripts on the page can neither read the conversation nor speak to the
656
+ frame; the frame never holds a partner secret, and the host's `getToken` answers the frame's token
657
+ requests. The frame document carries its own Content Security Policy and is never blank (a waiting card
658
+ from first paint). Requires an API with the embed-config route. `npm run build:cdn` emits the artifacts;
659
+ the release workflow publishes them to `/<version>/` (immutable) and `/v1/` (the channel) after every
660
+ gate, behind a reviewed environment. `sdk-staging.everfur.com` tracks `main`: the same artifacts built
661
+ for staging (the frame talks to the staging API, the loader opens the staging frame), published by
662
+ `publish-staging.yml` on every push. Chapter 14 covers a Shopify storefront: the Everfur app's theme
663
+ block and Shopify vouching for the signed-in customer through the app proxy.
664
+ - `X-Everfur-SDK-Platform` names the host (`react-native` | `web`) on every request; the platform is a
665
+ facade seam, never partner-configurable.
666
+ - `@everfur/sdk/testing/web`: `useEverfurTestEntitlements` for a web harness (the twin of `./testing/rn`,
667
+ now one shared hook); withheld from React Native bundles like `./web`.
668
+ - `examples/minimal-web.tsx` and `examples/frame-embed.html`, both pinned by the build; chapter
669
+ `docs/partner-integration/13-WEB-INTEGRATION.md`; `RELEASING.md` and `.github/workflows/release.yml`
670
+ (npm and the CDN, from one tag, after every gate).
671
+
672
+ ### Changed
673
+
674
+ - `react-native` and `react-native-safe-area-context` are declared **optional** peers (`react-dom` joins them),
675
+ so npm 7+ no longer installs React Native into a web project that installs the package. The React Native
676
+ entry points still need both. **Upgrading a React Native app from 0.1.x:** if `react-native-safe-area-context`
677
+ is not in your app's own `package.json` (npm installed it for you as a required peer), add it:
678
+ `npx expo install react-native-safe-area-context` in an Expo project, `npm i react-native-safe-area-context`
679
+ otherwise. Until you do, Metro cannot resolve it. An app that followed the 0.1.x quickstart already lists it.
680
+ - **Potentially breaking: "powered by everfur" is server-only.** `hide_powered_by` / `hidePoweredBy` in the
681
+ client `theme` prop is ignored; only the server branding (which carries the partner plan's decision) can
682
+ hide the attribution.
683
+ - Server branding: the chat renders the branding `logo_url` above the transcript (web and React Native), and
684
+ the web SDK loads the branding `font_url` when it is an allowlisted woff2 on `sdk.everfur.com` or
685
+ `sdk-staging.everfur.com`. The parser reads four more keys: `surface_style` (`white` / `cream` / `subtle`),
686
+ `radius_scale` (`sharp` / `default` / `round`), `primary_color_dark` and `accent_color_dark` (used on the
687
+ dark palette). A server `brightness` no longer drops the client `theme` prop's other fields. The frame
688
+ document's policy admits fonts from its own origin (`font-src 'self'`).
689
+
690
+ ### Fixed
691
+
692
+ - User message bubbles draw their text in the foreground computed for the primary fill they are painted with
693
+ (`onPrimary`), not the accent's, so a branding whose primary and accent differ stays legible.
694
+
695
+ - The contract error table knows the session mints' `authRejected` (the non-retryable 401, never a token
696
+ refresh) and `rateLimited` (429) by name; the vendored registry and generated contract are synced to the
697
+ platform, where they had been added, instead of both folding to `unknown`.
698
+ - `SDK_VERSION` (the `X-Everfur-SDK-Version` header) said `0.1.0` while `0.1.1` shipped; a test now pins it
699
+ to `package.json`.
700
+ - The frozen clinical disclaimer copy lives once (`src/react/disclaimer/copy.ts`) and both host bands import
701
+ it; the coverage guard pins that no band carries wording of its own.
702
+
703
+ ### Internal
704
+
705
+ - `src/react/` is the host-agnostic React layer (provider core with `HostSeams`, context, theme core,
706
+ `useCapability`, `useEverfurChat`); `src/rn/` and `src/web/` are thin facades over it. Layer boundaries
707
+ are enforced by lint (`react` may not import a host; `web` may not import `react-native`; `rn` may not
708
+ import `web`) and by the supply-chain suites (host isolation, `./web` blocked at resolve time for RN).
709
+
11
710
  ## [0.1.1] - 2026-09-03
12
711
 
13
- ### Fixed — privacy
712
+ ### Fixed: privacy
14
713
 
15
714
  - **An anonymous logout left mounted surfaces on the previous visitor's session.** `logout()` from an
16
715
  already-anonymous state compared `userRef` to `userRef`, saw null-to-null, and took its same-identity
@@ -23,7 +722,7 @@ changes that has not been earned by any production integration yet.
23
722
 
24
723
  ## [0.1.0] - 2026-08-23
25
724
 
26
- ### Fixed — correctness
725
+ ### Fixed: correctness
27
726
 
28
727
  - **Anonymous mode could never send a message.** The SDK never sent `x-everfur-session-id`, and the server
29
728
  derives the anonymous identity from it, minting a fresh one per request. A conversation was created as one
@@ -46,7 +745,7 @@ changes that has not been earned by any production integration yet.
46
745
  - Neither `POST /conversations` nor the SSE send is auto-replayed: the widget lane does not read
47
746
  `Idempotency-Key`, so a retry was a second row, not a dedup.
48
747
 
49
- ### Fixed — security
748
+ ### Fixed: security
50
749
 
51
750
  - **`@everfur/sdk/server` is no longer resolvable from a React Native bundle.** The subpath merely omitted a
52
751
  `react-native` export condition, which is not a block; it now maps to `null`. This is the module carrying
@@ -69,7 +768,7 @@ changes that has not been earned by any production integration yet.
69
768
  exported and documented while reaching no request in the SDK. Both narrow only: neither can make a spent
70
769
  daily quota retryable nor resurrect a terminal code, and an SDK per-call cap still outranks them.
71
770
 
72
- ### Changed — potentially breaking
771
+ ### Changed: potentially breaking
73
772
 
74
773
  - **`DisabledLevel` gained an `sdk_config` member.** A consumer with an exhaustive `switch` over that union
75
774
  will no longer compile. This is the one deliberate widening in this set.