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