@everfur/sdk 0.7.0 → 0.8.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 (185) hide show
  1. package/CHANGELOG.md +326 -0
  2. package/README.md +24 -16
  3. package/dist/{Chat-DZpYH8Uh.d.ts → Chat-Cv9EYMYA.d.ts} +3 -3
  4. package/dist/{Chat-bATY4atj.d.cts → Chat-s2vcPbw3.d.cts} +3 -3
  5. package/dist/{DepthViews-qBIoA48w.d.ts → DepthViews-CVxUHOJ-.d.ts} +1 -1
  6. package/dist/{DepthViews-C4DC4Kom.d.cts → DepthViews-Cz7mSW7p.d.cts} +1 -1
  7. package/dist/{DepthViews-dLTizxjr.d.ts → DepthViews-DNRY0Ugm.d.ts} +1 -1
  8. package/dist/{DepthViews-BpHUbjSY.d.cts → DepthViews-DvNixbxg.d.cts} +1 -1
  9. package/dist/{ErrorPolicyPort-D22LUVrK.d.ts → ErrorPolicyPort-DwSnmGcX.d.ts} +1 -1
  10. package/dist/{ErrorPolicyPort-D6CybiqJ.d.cts → ErrorPolicyPort-NFDPe-5w.d.cts} +1 -1
  11. package/dist/{EverfurError-DmELDpXp.d.cts → EverfurError-pEXDbUJ1.d.cts} +9 -1
  12. package/dist/{EverfurError-DmELDpXp.d.ts → EverfurError-pEXDbUJ1.d.ts} +9 -1
  13. package/dist/{EverfurResult-ZtH-YV1h.d.cts → EverfurResult-dtl8ERAw.d.cts} +1 -1
  14. package/dist/{EverfurResult-SGvHKsCB.d.ts → EverfurResult-sKkoXew0.d.ts} +1 -1
  15. package/dist/IntakeMedia-BFZfBLjB.d.cts +116 -0
  16. package/dist/{BookedScreen-DFJcgf1e.d.ts → IntakeMedia-BH0fYDT2.d.cts} +58 -2
  17. package/dist/{BookedScreen-R5X_Zrlh.d.cts → IntakeMedia-Bavl7E4Z.d.ts} +58 -2
  18. package/dist/IntakeMedia-C8GlN_GK.d.ts +116 -0
  19. package/dist/animations/index.cjs +109 -1
  20. package/dist/animations/index.d.cts +6 -6
  21. package/dist/animations/index.d.ts +6 -6
  22. package/dist/animations/index.js +109 -1
  23. package/dist/{attachments-CPRFfoHT.d.ts → attachments-BaslXMdS.d.ts} +3 -3
  24. package/dist/{attachments-CUOGILir.d.cts → attachments-DbFwz4iq.d.cts} +7 -7
  25. package/dist/{attachments-Ce06TWqe.d.cts → attachments-y3EcOX_w.d.cts} +3 -3
  26. package/dist/{attachments-CXAnZcEg.d.ts → attachments-y_aO4hlk.d.ts} +7 -7
  27. package/dist/{bookingCopy-BwVopcoV.d.cts → bookingCopy-4i1R7klo.d.cts} +15 -7
  28. package/dist/{bookingCopy-DFTv9w6J.d.ts → bookingCopy-B3UMITD-.d.ts} +15 -7
  29. package/dist/{branding-ytH4jJq1.d.ts → branding-BYEqU855.d.ts} +1 -1
  30. package/dist/{branding-Bio5MM6g.d.cts → branding-BwEu68C8.d.cts} +1 -1
  31. package/dist/{callRoom-7wrE3RP2.d.ts → callRoom-B1qq_D-4.d.ts} +1 -1
  32. package/dist/{callRoom-B_EqaUzx.d.cts → callRoom-DfZTWFXh.d.cts} +1 -1
  33. package/dist/{casesRepository-CJchNp3x.d.cts → casesRepository-B6Z0XBIb.d.cts} +2 -2
  34. package/dist/{casesRepository-DAVcY3jC.d.ts → casesRepository-SI0zp5YH.d.ts} +2 -2
  35. package/dist/chat/index.cjs +333 -1
  36. package/dist/chat/index.d.cts +12 -12
  37. package/dist/chat/index.d.ts +12 -12
  38. package/dist/chat/index.js +333 -1
  39. package/dist/client/index.cjs +115 -11
  40. package/dist/client/index.d.cts +14 -14
  41. package/dist/client/index.d.ts +14 -14
  42. package/dist/client/index.js +115 -11
  43. package/dist/{config-ZxwCylac.d.ts → config-Cvo47voG.d.ts} +3 -3
  44. package/dist/{config-h5h6TsHL.d.cts → config-v5k6NsK_.d.cts} +3 -3
  45. package/dist/consent/index.cjs +112 -1
  46. package/dist/consent/index.d.cts +9 -9
  47. package/dist/consent/index.d.ts +9 -9
  48. package/dist/consent/index.js +112 -1
  49. package/dist/{context-CtsAgJnc.d.cts → context-BZOtJbBO.d.cts} +9 -9
  50. package/dist/{context-DtPIshVA.d.ts → context-cZ7NZ0sG.d.ts} +9 -9
  51. package/dist/{copy-Cg89Qoal.d.ts → copy-CZBUHbyS.d.ts} +5 -5
  52. package/dist/{copy-CaWaplQc.d.cts → copy-ChJupm1p.d.cts} +5 -5
  53. package/dist/core/index.cjs +140 -13
  54. package/dist/core/index.d.cts +19 -19
  55. package/dist/core/index.d.ts +19 -19
  56. package/dist/core/index.js +140 -13
  57. package/dist/{depth-EXZYEO70.d.cts → depth-CRyU7-pf.d.cts} +8 -7
  58. package/dist/{depth-Bo4bAwcn.d.ts → depth-vNpwOkZ8.d.ts} +8 -7
  59. package/dist/{entitlementRepository-BYtVb1oU.d.cts → entitlementRepository-Bv8nSYRd.d.cts} +4 -4
  60. package/dist/{entitlementRepository-DcbIx8yV.d.ts → entitlementRepository-CUdxbQrP.d.ts} +4 -4
  61. package/dist/{identity-EC5-apbY.d.ts → identity-B8GkH53J.d.ts} +2 -2
  62. package/dist/{identity-b5Vp32Vp.d.cts → identity-BHaMJ3KK.d.cts} +2 -2
  63. package/dist/index.cjs +363 -12
  64. package/dist/index.d.cts +19 -19
  65. package/dist/index.d.ts +19 -19
  66. package/dist/index.js +363 -12
  67. package/dist/{models-DSqA5Ahs.d.ts → models-CrbUwbuS.d.ts} +1 -1
  68. package/dist/{models-Bn2z0NNo.d.cts → models-DzvqEjMm.d.cts} +1 -1
  69. package/dist/notifications/index.cjs +94 -2
  70. package/dist/notifications/index.d.cts +5 -5
  71. package/dist/notifications/index.d.ts +5 -5
  72. package/dist/notifications/index.js +94 -2
  73. package/dist/{pets-DM8QvKLo.d.cts → pets-BI7Ji8hi.d.cts} +2 -2
  74. package/dist/{pets-B_27_agU.d.ts → pets-Dnn-DK1T.d.ts} +2 -2
  75. package/dist/{petsRepository-CuYZZaI5.d.ts → petsRepository-B2lO1sKS.d.ts} +2 -2
  76. package/dist/{petsRepository-BJsJ4cju.d.cts → petsRepository-BP9nhM1R.d.cts} +2 -2
  77. package/dist/photo/index.cjs +131 -1
  78. package/dist/photo/index.d.cts +3 -3
  79. package/dist/photo/index.d.ts +3 -3
  80. package/dist/photo/index.js +131 -1
  81. package/dist/{ports-nXHf50_X.d.ts → ports-BUXF0Rp3.d.ts} +7 -7
  82. package/dist/{ports-ZmJYgd5I.d.cts → ports-C40S2V14.d.cts} +7 -7
  83. package/dist/records/depth/index.cjs +200 -2
  84. package/dist/records/depth/index.d.cts +11 -11
  85. package/dist/records/depth/index.d.ts +11 -11
  86. package/dist/records/depth/index.js +200 -2
  87. package/dist/records/index.cjs +742 -4
  88. package/dist/records/index.d.cts +22 -14
  89. package/dist/records/index.d.ts +22 -14
  90. package/dist/records/index.js +742 -4
  91. package/dist/{requestFunnel-Ve9BshTB.d.ts → requestFunnel-BrOijMcw.d.ts} +5 -5
  92. package/dist/{requestFunnel-C_cZXhIA.d.cts → requestFunnel-BxNVGyGm.d.cts} +5 -5
  93. package/dist/{runtimeTypes-ByKsp8hm.d.ts → runtimeTypes-CvI1ZH8j.d.ts} +7 -7
  94. package/dist/{runtimeTypes-D155OlBW.d.cts → runtimeTypes-DOO3f-W-.d.cts} +7 -7
  95. package/dist/server/events/index.cjs +92 -2
  96. package/dist/server/events/index.d.cts +4 -4
  97. package/dist/server/events/index.d.ts +4 -4
  98. package/dist/server/events/index.js +92 -2
  99. package/dist/server/index.cjs +95 -2
  100. package/dist/server/index.d.cts +5 -5
  101. package/dist/server/index.d.ts +5 -5
  102. package/dist/server/index.js +95 -2
  103. package/dist/televet/booking/index.cjs +394 -4
  104. package/dist/televet/booking/index.d.cts +24 -127
  105. package/dist/televet/booking/index.d.ts +24 -127
  106. package/dist/televet/booking/index.js +394 -4
  107. package/dist/televet/call/index.cjs +71 -1
  108. package/dist/televet/call/index.d.cts +3 -3
  109. package/dist/televet/call/index.d.ts +3 -3
  110. package/dist/televet/call/index.js +71 -1
  111. package/dist/televet/index.cjs +101 -1
  112. package/dist/televet/index.d.cts +25 -8
  113. package/dist/televet/index.d.ts +25 -8
  114. package/dist/televet/index.js +101 -1
  115. package/dist/{bookingTypes-D4pD3tHs.d.ts → televetGlobal-C5dTUACe.d.cts} +34 -1
  116. package/dist/{bookingTypes-D4pD3tHs.d.cts → televetGlobal-CnjrfhT4.d.ts} +34 -1
  117. package/dist/testing/index.cjs +98 -7
  118. package/dist/testing/index.d.cts +3 -3
  119. package/dist/testing/index.d.ts +3 -3
  120. package/dist/testing/index.js +98 -7
  121. package/dist/testing/rn/index.cjs +101 -1
  122. package/dist/testing/rn/index.d.cts +13 -13
  123. package/dist/testing/rn/index.d.ts +13 -13
  124. package/dist/testing/rn/index.js +101 -1
  125. package/dist/testing/web/index.cjs +95 -1
  126. package/dist/testing/web/index.d.cts +14 -14
  127. package/dist/testing/web/index.d.ts +14 -14
  128. package/dist/testing/web/index.js +95 -1
  129. package/dist/{timelineRows-DXpjECfA.d.ts → timelineRows-CEx2TzYB.d.ts} +1 -1
  130. package/dist/{timelineRows-BNF43SXK.d.cts → timelineRows-Dffceql_.d.cts} +1 -1
  131. package/dist/{tokens-BYtbKwFh.d.cts → tokens-Tzo1KBND.d.cts} +43 -2
  132. package/dist/{tokens-BYtbKwFh.d.ts → tokens-Tzo1KBND.d.ts} +43 -2
  133. package/dist/{typeStyle-BqfwxZH4.d.cts → typeStyle-C3IG9Sft.d.ts} +22 -3
  134. package/dist/{typeStyle-DKz02DGU.d.ts → typeStyle-CZ3Tj60N.d.cts} +22 -3
  135. package/dist/{useRecordsDepth-CARTuHKh.d.cts → useRecordsDepth-D6a6-_u_.d.cts} +7 -6
  136. package/dist/{useRecordsDepth-fPSv-GXU.d.ts → useRecordsDepth-DuPUJFDQ.d.ts} +7 -6
  137. package/dist/{useVetVisit-Ds8f1peV.d.cts → useVetVisit-BQX_aTJs.d.cts} +2 -2
  138. package/dist/{useVetVisit-BsjoDO-n.d.ts → useVetVisit-RPBOabl8.d.ts} +2 -2
  139. package/dist/video/index.cjs +122 -1
  140. package/dist/video/index.d.cts +3 -3
  141. package/dist/video/index.d.ts +3 -3
  142. package/dist/video/index.js +122 -1
  143. package/dist/{view-Dc-JlFkf.d.ts → view-BSmN5RWf.d.ts} +5 -5
  144. package/dist/{view-DGPaVno8.d.cts → view-UFwCG1Pn.d.cts} +5 -5
  145. package/dist/web/consent/index.cjs +111 -1
  146. package/dist/web/consent/index.d.cts +9 -9
  147. package/dist/web/consent/index.d.ts +9 -9
  148. package/dist/web/consent/index.js +111 -1
  149. package/dist/web/index.cjs +325 -14
  150. package/dist/web/index.d.cts +22 -22
  151. package/dist/web/index.d.ts +22 -22
  152. package/dist/web/index.js +325 -14
  153. package/dist/web/notifications/index.cjs +95 -2
  154. package/dist/web/notifications/index.d.cts +7 -7
  155. package/dist/web/notifications/index.d.ts +7 -7
  156. package/dist/web/notifications/index.js +95 -2
  157. package/dist/web/records/depth/index.cjs +169 -2
  158. package/dist/web/records/depth/index.d.cts +11 -11
  159. package/dist/web/records/depth/index.d.ts +11 -11
  160. package/dist/web/records/depth/index.js +169 -2
  161. package/dist/web/records/index.cjs +689 -4
  162. package/dist/web/records/index.d.cts +34 -37
  163. package/dist/web/records/index.d.ts +34 -37
  164. package/dist/web/records/index.js +689 -4
  165. package/dist/web/records/upload/index.cjs +3 -0
  166. package/dist/web/records/upload/index.d.cts +23 -0
  167. package/dist/web/records/upload/index.d.ts +23 -0
  168. package/dist/web/records/upload/index.js +3 -0
  169. package/dist/web/televet/booking/index.cjs +423 -4
  170. package/dist/web/televet/booking/index.d.cts +26 -74
  171. package/dist/web/televet/booking/index.d.ts +26 -74
  172. package/dist/web/televet/booking/index.js +423 -4
  173. package/dist/web/televet/call/index.cjs +28 -1
  174. package/dist/web/televet/call/index.d.cts +2 -2
  175. package/dist/web/televet/call/index.d.ts +2 -2
  176. package/dist/web/televet/call/index.js +28 -1
  177. package/dist/web/televet/index.cjs +102 -4
  178. package/dist/web/televet/index.d.cts +29 -10
  179. package/dist/web/televet/index.d.ts +29 -10
  180. package/dist/web/televet/index.js +102 -4
  181. package/package.json +20 -4
  182. package/web/records/upload/native-blocked.cjs +12 -0
  183. package/web/records/upload/package.json +8 -0
  184. package/dist/televetGlobal-CEk0_cW9.d.cts +0 -34
  185. package/dist/televetGlobal-Cs9Wbqsy.d.ts +0 -34
package/CHANGELOG.md CHANGED
@@ -10,6 +10,332 @@ changes that has not been earned by any production integration yet.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.8.0] - 2026-10-02
14
+
15
+ ### Added
16
+
17
+ - **`identitySignatureRequired` is a named error code.** The pets routes' 403 for a health field on a session your
18
+ backend did not sign (`registerPet` or `updatePet` with health fields, or `getPetProfile`, while anonymous)
19
+ settled as `unknown`. It is now `identitySignatureRequired` (category `authentication`, not retryable), shown to an
20
+ end user with the existing access-denied sentence. Documented in 11-ERRORS. (#155)
21
+ - **`EverfurError.retryAfterSeconds`.** The wait a refusal named, in seconds: the body's `retry_after_seconds`, or a
22
+ `Retry-After` header the transport folds into it. (#155)
23
+ - **`@everfur/sdk/web/records/upload`**: `createWebRecordsUploadTransport` on its own entry, for the `uploadTransport`
24
+ on a provider config at the app root (1_295 bytes minified, against 85_208 for the same import from
25
+ `@everfur/sdk/web/records`, and 388_582 before the tree-shaking change under Changed). `@everfur/sdk/web/records` still exports it. (#158)
26
+ - **`TelevetBookingController.releaseHold(slotId)`** (optional on the interface, so a host's own controller
27
+ still type-checks): `DELETE /widget/v1/televet/slots/{slot_id}/hold`, served by EFBackend from #1865. The
28
+ server releases only this member's own hold and answers the same either way. A server without the route (a
29
+ dark 404) settles `ok`, and the hold lapses as before. (#144)
30
+ - Colour tokens `highlight`, `onHighlight`, `mark`, `controlEdge`, `structureEdge` and `primaryEdge` on
31
+ `EverfurColorTokens`; `themeAdjustments` and `EverfurThemeAdjustment`; the `SlotSurrounds` type the slot guard now
32
+ takes; and `ThemeSeams.isDevelopment`. Additive: the appearance contract
33
+ (`contract/appearance/appearance_v1.json`) and the branding wire are unchanged. (#145)
34
+ - **`consentText`: the records consent gate shows the partner's own consent text (owner decision 2026-10-02).**
35
+ Every records surface that takes `consentVersion` now takes `consentText` beside it (React Native:
36
+ `EverfurRecordsFlow`, `EverfurRecordsDashboard`, `EverfurRecordDetail`, `EverfurRecordDocument`; web: the same
37
+ plus `EverfurRecordsPage`; the iframe embed: the `consentText` init option and `data-consent-text`, an additive
38
+ `ef:init` field). The gate renders it as plain text, line breaks kept, above `Allow`, which stays its own button.
39
+ The visible `DRAFT COPY` line that held its place is gone: Everfur supplies no consent wording, so a surface
40
+ given `consentVersion` without `consentText` shows only `Consent required` and `Allow`, and a development build
41
+ logs one `[everfur]` console warning naming the missing prop. Optional in the types (a minor addition), because
42
+ `consentVersion` is optional too. Documented in 09-GUIDE-RECORDS-AND-CONSENT, 05-SDK-INTEGRATION,
43
+ 13-WEB-INTEGRATION and the quickstart; the examples pass both. The Everfur CLI's docs mirror resyncs itself. (#168)
44
+
45
+ ### Changed
46
+
47
+ - **A partner's bundler can now tree-shake the web entries.** Importing one thing from `@everfur/sdk/web` used to
48
+ cost the whole entry: a page importing only `petRef` carried 354_011 minified bytes, exactly what `EverfurChat`
49
+ costs, because the build kept function names with a call after every function and its minifier deleted every
50
+ `/* @__PURE__ */` annotation. The published files are now minified without renaming anything and keep their pure
51
+ annotations, so a page that imports `petRef` alone carries 29_035 bytes (esbuild, minified), and the partner demo's
52
+ app root went from 686_156 to 203_196. Function and component names are unchanged in stack traces and React
53
+ DevTools. `tests/supply-chain/consumerTreeShake.test.ts` keeps it that way. (#158)
54
+
55
+ ### Fixed
56
+
57
+ #### Packaging and client
58
+
59
+ - **Importing `@everfur/sdk/web` from a Next.js server component works.** It failed with the cryptic
60
+ `(0, c.createContext) is not a function`: the provider and hooks ran in the server graph. Every `web/*` entry and
61
+ `testing/web` now opens with `"use client"` (`scripts/add-use-client.mjs`, run by `build`), as React libraries
62
+ mark their client entries; `./server`, `./client`, `./core` and the React Native entries do not. Server rendering
63
+ of the client components is unchanged (pinned by rendering the built provider with `renderToString`). (#163)
64
+ - **One session mint per page load, not three.** Every runtime's identity called your `getToken` for itself, so
65
+ React StrictMode's second runtime (and any second `EverfurProvider`) minted again in parallel: the production
66
+ integration test of 0.7.1 counted three `POST` session mints on every load. Concurrent mints for the same
67
+ tenant (publishable key and API base URL) and the same user now share one `getToken` call. Only the call in
68
+ flight is shared: the next mint after it settles calls `getToken` again, so the expiry your minter names and
69
+ a sign-out mean what they did. (#140)
70
+ - **A pet is registered once per burst, not once per caller.** A sample app counted every pet registered twice per
71
+ navigation: React StrictMode runs the chat's `pet` registration twice, and a host registering on load sends the
72
+ same body beside it. Concurrent identical `registerPet` calls (same tenant, identity, pet and body, from one
73
+ runtime or several, and from the chat's `pet` prop) now share one `POST /widget/v1/pets/{pet_ref}` and its
74
+ result, as session mints do (#140). Only the request in flight is shared; the next call sends again. (#160)
75
+ - **The birth date rule is documented.** The same report met `validationFailed` with no request id on
76
+ `birthDate: '2026-09-01'`. A recent date, today's included, is accepted (now pinned by a test); the refusal a
77
+ birth date meets before any request is the pair rule (a birth date always goes with `birthDatePrecision`), which
78
+ was documented nowhere. 09-GUIDE now states it and where the reason is (`error.wireMessage`). (#160)
79
+ - **The anonymous-session budget's 429 is told apart.** A new anonymous identity per page load (the session id is
80
+ memory-only) meets the API's budget of 20 new anonymous identities per IP per 5 minutes, and the 429 read as a
81
+ send throttle. The API now sends `Retry-After` and `reason_code: anonymous_session_limit` on it; the SDK settles
82
+ it as `rateLimited` with that `reasonCode` and `retryAfterSeconds`, and (through the header fold) does not retry
83
+ into a wait longer than its retry budget. Documented in 10-TESTING and 11-ERRORS. (#155)
84
+
85
+ #### Theme
86
+
87
+ - **Everfur's colours no longer leak through a partner palette (web and React Native).** A production integration
88
+ test of 0.7.1 themed through `appearance` (teal primary, warm white background) still showed Everfur's mint pet
89
+ avatar, the pastel green `Good to know` banner, a pink picked day chip and `Now` pill, and a green current step on
90
+ the request timeline. Each now paints from the theme: the picked chips, the `Now` pill, the open history row and a
91
+ pet's initials disc from the new `highlight` / `onHighlight` tokens (a tint of the partner's accent, else its
92
+ primary), the avatar ring and the current step from `mark`, and the `Good to know` and `Monitor at home` banners
93
+ from the brand's wash, outline and ink (`Vet care suggested soon` from the palette's caution tokens). Everfur's
94
+ own light look is unchanged: every new token is the compiled value it stands in for, and the banners and the
95
+ initials disc keep the app's authored paint there. (#145)
96
+ - **The dark palette is dark-aware everywhere (`night` preset).** The calm urgency banners were self-contained
97
+ pastel chips (THEME-01), a glaring mint or amber slab on the dark ground, and the picked chip and `Now` pill stayed
98
+ light pink. They now take the dark palette's tokens: a dark wash under light ink, and the pink's dusk twin
99
+ (`#605051`) as the dark `highlight`. (#145)
100
+ - **Non-text contrast (WCAG 1.4.11).** The text guard is joined by a guard on the outlines that show a control or a
101
+ structural mark: the new `controlEdge` (outlined pills, such as the chat header's Vet entry), `structureEdge` (the
102
+ steps still to come on a request timeline and its rail, an informational banner's edge) and `mark` reach 3:1 on a
103
+ partner palette, and `primaryEdge` edges the send disc, the owner's bubble and a done step when a pale primary does
104
+ not stand out from its ground. A slot with an opaque fill that melts into the ground around it (a white
105
+ `userBubble` on a white chat) is given a `borderColor` that reads at 3:1 there, and a partner's own `borderColor`
106
+ on such a slot is pushed until it does. Everfur's compiled palettes keep their authored hairlines. (#145)
107
+ - **The guard no longer changes a partner's colours in silence.** `themeAdjustments(input)` lists every partner
108
+ colour the contrast guard replaced (field, role, requested, painted), and the provider writes that list to the
109
+ console once in a development build (`__DEV__` on React Native, the bundler's `process.env.NODE_ENV` on the web). (#145)
110
+ - **A partner brand that already reads keeps its outline's hue (teal `#0F766E`).** The non-text guard pushed an
111
+ outline that did not read (the brand's 20% hairline) toward black or white, so a teal partner's outlined pills
112
+ (the chat header's Vet entry among them) were painted grey and a development build reported
113
+ `primary_color (control outline): #0F766E -> #7C8683`, although the teal reads at 3:1 on its warm white and on
114
+ white. When the brand itself reads at 3:1 on every control ground the outline is now strengthened toward the brand
115
+ (`#49968E` on `#FFFBF5`) and nothing is reported; a brand that does not read is pushed and reported as before. (#170)
116
+
117
+ #### Records
118
+
119
+ - **A refused signature upload no longer fails the clinic request (web and React Native).** A production sandbox
120
+ test found the signature upload's direct S3 POST refused from a partner's page (the prod bucket's CORS listed only
121
+ Everfur origins), so every web clinic request failed at signing. When that POST itself fails (a network or CORS
122
+ refusal, or S3 answering non-2xx) the built-in pad now sends the same PNG inline as `signature_png_base64`, which
123
+ the create API has always accepted. A refused initiate, an untrusted policy host, a failed complete and a cancel
124
+ still fail as before. Web: the pad gains `capture(controller, signal)`, which the built-in forms use;
125
+ `register` is unchanged and still strict. React Native: the multi-clinic composer keeps the inline PNG when its
126
+ registration's S3 POST fails. (#162)
127
+ - **The clinic-release consent screen no longer reads "I, , authorize ..." (web and React Native).** The served
128
+ authorization sentence is the owner's own signed scope, and with no name typed yet it rendered with a blank
129
+ where the name goes. It now waits for a name: nothing stands in for it while the field is empty, and it
130
+ appears as the name is typed. Found by the production integration test of 0.7.1. (#139)
131
+ - **"Please add your signature before submitting." no longer greets the consent screen (web).** #127 moved the
132
+ line on the in-place clinic request forms; the consent screen the picker opens still showed it as soon as a
133
+ clinic was chosen. It now appears only when the signature is the one thing keeping `Send request` closed. (#139)
134
+ - **The React Native multi-clinic composer scrolls, closes and sends (React Native).** On an iPhone 17 simulator
135
+ (Expo 57, 2026-10-01) `EverfurClinicRequestBatch` was about 1112 pt of unscrolled content on an 874 pt screen: once
136
+ a clinic was added, Send and Cancel were out of reach, there was no close control at the top, the Search control
137
+ looked like a second text field, and the keyboard could not be dismissed. It now has a header with the composer's
138
+ own `Cancel`, the form in a keyboard-avoiding ScrollView that dismisses the keyboard on a drag or a tap outside a
139
+ control, and `Send` pinned in a footer above the home indicator. The search is one field whose return key
140
+ searches; Search is an outline pill. No new strings. (#143)
141
+ - **React Native records draw their icons (React Native).** The records pictograms rendered as empty tinted discs
142
+ (the empty states, the upload screen) and squares (the dashboard's action rows). They now draw the consumer app's
143
+ Feather icons with Views, as the chat glyphs do: `file-text`, `list`, `inbox` and `refresh-cw` on the action rows;
144
+ `search`, `inbox`, `activity`, `clock`, `tool` and `wifi-off` per empty state; `upload-cloud` on the upload screen.
145
+ They are drawn in the brand ink and hidden from assistive technology. (#149)
146
+ - **The records footnote sentence shows once per screen (web and React Native).** On `Visits & records` inside
147
+ `EverfurRecordsPage` the production integration test of 0.7.1 saw "Everfur summarizes what your vet recorded.
148
+ It is informational, not diagnostic." three times: the server's disclaimer band (the same words), the record
149
+ screen's footnote and the page's footnote. A screen the page shows now draws no footnote of its own under the
150
+ page's, and a server disclaimer that says exactly what the footnote says is not drawn again. A server
151
+ disclaimer with any other words is still rendered verbatim. (#142)
152
+ - **The records consent gate says what `Allow` agrees to, and its button is reachable on React Native.** Both
153
+ production integration tests of 0.7.1 saw only "Consent required" and `Allow`. The gate now shows a line for
154
+ what the member consents to between them. **That line is a visible `DRAFT COPY [[...]]` placeholder**: no
155
+ approved consent text exists and the server serves none, so the wording is the owner's decision (documented in
156
+ 09-GUIDE-RECORDS-AND-CONSENT.md). On React Native the gate's container was `accessible`, which grouped it into
157
+ one element named "Consent required" and hid the `Allow` button from a screen reader; the ask is now visible
158
+ text announced on mount, and `Allow` is its own button. (#150)
159
+ - **Web records forms and screens speak to assistive technology (accessibility).** A records flow opened on a given
160
+ screen (`initialScreen`) now moves focus to that screen's heading on mount, as every later screen change already
161
+ did; before, the host control that opened it unmounted and focus fell to the page's `<body>`. The clinic email
162
+ errors on the consent screen, a failed request's recovery and the clinic search's add-by-email form are bound to
163
+ their fields (`aria-invalid` + `aria-describedby`). The clinic search says how many clinics it found through a
164
+ polite status region (a DRAFT COPY slot until the owner approves a sentence). The upload card's heading is an
165
+ `h3` under the screen's `h2` instead of skipping to `h4`. (#161)
166
+ - **The records share sheet's audience chips wrap (web).** At 390px the `Boarding` chip was clipped on the right. (#145)
167
+ - **Every records control says what it shows (web and React Native, WCAG 2.5.3 Label in Name).** Owner decision
168
+ 2026-10-02: a control's accessible name is its visible text, so a speech user can activate it by reading it. The
169
+ records screens and the chat history named about twenty controls in other words (`Choose a file` was spoken
170
+ "Choose records to upload", `I already have the records` "Upload records you already have", `Cancel request`
171
+ "Cancel this records request"); each now takes its visible text as its name, busy labels included. A control
172
+ repeated in a list keeps its visible name and gets its row as context: `Copy link` is described by its share row
173
+ (`aria-describedby` on the web, `accessibilityHint` on React Native) and each request row's `Ask about this
174
+ request` has the row's source as its hint. The clinic picker's add-by-email toggle states open or closed through
175
+ `aria-expanded` and `accessibilityState.expanded` instead of a second name. No visible copy changed. (#169)
176
+ - **The single-clinic consent form lets the keyboard go and keeps `Send request` above it (React Native).** Its
177
+ ScrollView and the clinic results dismiss the keyboard on a drag (interactive on iOS, on-drag on Android) and keep
178
+ taps on controls. The pinned `Send request` footer lifts by the keyboard measured from the avoiding view's own
179
+ top, as the multi-clinic composer's does (#143); before, it sat half under the keyboard. (#170)
180
+ - **The search glyph has its handle (React Native).** The clinic empty state's and the chat history's search glyph
181
+ drew its handle detached from the ring, and the clinic picker's search bar drew a bare ring. Both now draw
182
+ Feather's `search`: a ring with the handle growing out of it, down and to the right. (#170)
183
+
184
+ #### Chat
185
+
186
+ - **Chat shows the pet a host registers on load, without waiting for a send (web and React Native).** The pet
187
+ list behind the chat header, the empty-state title and the starter chips was read once on mount, so a host
188
+ that calls `registerPet` as the page loads (the documented shape) raced it: the production integration test of
189
+ 0.7.1 opened chat before the write landed and saw "What can we help your pet with today?", "Mixed Breed" and a
190
+ `?` avatar until the member sent a message. A successful `registerPet`, `updatePet` or `deletePet` now makes
191
+ `useEverfurPets` (and so chat) read the list again. (#141)
192
+ - **Chat conversations are saved under the chat's pet again (web and React Native).** Since 0.7.0 the first
193
+ send creates the conversation (#129), and it created it with the turn's pet, which `useEverfurChat().send`
194
+ never names: every conversation was created unscoped and saved with `pet_ref` null, so the pet's history
195
+ drawer read "No chats yet" right after a chat (mobile integration test of 0.7.1). A send that names no pet now
196
+ uses the chat's own pet scope; a send that names one keeps it, and an unscoped chat is unchanged. (#148)
197
+ - **React Native chat lets the keyboard go, and the urgency banner compacts while typing (React Native).** Once
198
+ open, the chat keyboard could not be dismissed, and the pinned urgency banner kept its full height while the
199
+ member typed (about 40% of what was left of the screen). The thread and the starter-prompt ScrollViews now
200
+ dismiss the keyboard on a drag (interactive on iOS, on-drag on Android). While the keyboard is open the banner is
201
+ compact: tighter padding and the reason held to two lines (a screen reader still reads it whole). The tier label,
202
+ the vet pill, poison control and the dismiss stay, so an emergency keeps its way to care. (#147)
203
+ - **The web chat holds up in forced-colors mode (Windows High Contrast) (accessibility).** The header's New chat and
204
+ History glyphs vanished, the starter chips and the Send disc lost their edges, and the brand-ink upload and mic
205
+ glyphs fell below contrast. Every chat glyph now draws in `currentColor` with its ink as the SVG's CSS `color`
206
+ (and `forced-color-adjust: auto`, since browsers otherwise preserve an SVG's own colour there), so the mode
207
+ repaints it in the system colours; the starter chips, the header's icon buttons, the composer's glyph controls
208
+ and the Send disc carry a 1px transparent edge that only the mode draws. The normal look is unchanged. (#159)
209
+ - **The starter chips follow a partner's palette and the dark palette (React Native).** Off Everfur's own light look
210
+ the four starter chips stayed Everfur's blue, green, amber and pink pastels, which glared on the night ground. They
211
+ now paint the theme's `highlight` under `onHighlight`. Everfur's own look keeps its pastels and black ink. (#170)
212
+ - **The starter chips follow a partner's palette and the dark palette (web).** The web chat's starter chips kept
213
+ Everfur's blue, green, amber and pink pastels under every partner preset and glared on the night ground. Off
214
+ Everfur's own light look they now paint the theme's `highlight` under `onHighlight` (4.5:1), the rule the React
215
+ Native chips follow since #170. Everfur's own look keeps its pastels and black ink. (#172)
216
+
217
+ #### Vet visits
218
+
219
+ - **The default vet visit can offer the pet's own clinic and your emergency-vet locator (web and React Native).**
220
+ `primaryVet` and `onFindEmergencyVet` reached the emergency warning only through `EverfurTelevet`; the in-app
221
+ visit Everfur provides by default (`EverfurTelevetHost`) rendered it without them, so the warning offered ASPCA
222
+ Animal Poison Control and no way to the member's clinic. The web host now passes both through, as it does
223
+ `onPickMedia`. The React Native host took no props at all; it now takes the same four (`onAddToCalendar`,
224
+ `onPickMedia`, `primaryVet`, `onFindEmergencyVet`), and `@everfur/sdk/televet` exports their types. (#157)
225
+ - **The vet booking asks each question once (web and React Native).** Every booking read kept its own state, so the
226
+ dialog asked its eight or nine questions again on every open, the state policy twice per open (the flow and its
227
+ `where` step), and ten open/close cycles ran a member into the partner plane's rate limit (2026-10-01 audit). The
228
+ read-only reads (the roster, the slots, the soonest opening, the state policy, the cancellation terms, the visits)
229
+ now share one answer per question per signed-in session: a question already in flight is joined, a settled answer
230
+ is reused for 30 seconds (the visits list a member comes back to is still read again), and a hold, booking,
231
+ move, cancel or released hold drops the answers it changed. The entitlement read is joined but never reused. A
232
+ rate-limited answer is kept for the server's own wait, so a reopened dialog shows the rate-limit state it already
233
+ has instead of spending another request. A 429 or 503 whose wait rides only in the `Retry-After` header is now
234
+ honoured like one whose body names it. (#166)
235
+ - **A test tenant's vet entry no longer reads the real vet roster or slots.** The booking container prefetched the
236
+ real roster, bios, photos and bookable slots even when it then drew the visit simulator. On a test tenant those
237
+ reads now wait for the allowance answer and never start when the simulator applies. A live tenant reads them at
238
+ once, as before; a test tenant with an allowance still books real visits. (#162)
239
+ - **A rate-limited booking read says so, and offers "Try again" when it can work (web and React Native).** After
240
+ about eight booking opens in eight minutes the production integration test of 0.7.1 got 429 on the vet roster
241
+ and the state policy, and the step showed only "Could not load" with a "Try again" that could not succeed.
242
+ The pet, vet and time steps now show the SDK's existing rate-limit state ("Slow down a moment" / "You’ve made
243
+ a lot of requests just now. You can try again shortly.", the records empty state's words) and offer "Try
244
+ again" once the server's `retry_after_seconds` has passed (60 seconds when it names none). Any other failure
245
+ shows the step's own card, as before. The steps take an optional `error` prop for this. (#146)
246
+ - **An unbooked vet visit hold is given back (web and React Native).** Reaching the booking review takes a
247
+ ten-minute hold, and nothing released it: closing the dialog left the time off every grid until the hold ran
248
+ out, and the same member coming back for it read "That time was just taken." about their own hold (production
249
+ integration test of 0.7.1). The booking flow now releases its hold when it closes or unmounts without booking,
250
+ and when another time replaces it. Back from review still keeps the hold, so the same time picked again goes
251
+ straight back to review on it. A hold the flow booked, or is booking, is never released. (#144)
252
+ - **No fee line on a partner's cancel receipt (web).** A partner member owes no cancellation fee (owner decision
253
+ 2026-10-01: a late cancel is used from the partner's allowance, and the server reports `fee_recorded: false` on
254
+ every partner cancel), yet the visits screen showed "No late cancellation fee was recorded for this visit." on
255
+ every receipt. That line now renders only when the server reports a recorded fee, so a fee that was recorded is
256
+ still named. No copy changed. (#153)
257
+ - **React Native booking follows the palette.** The progress bar (the booking chrome's done segments) and the
258
+ booked and joined rings now take `mark` instead of Everfur's green, the picked day `highlight` instead of the pink,
259
+ the primary button's edge `primaryEdge` (a pale partner primary, the disabled `Continue` included, keeps its
260
+ shape), and the full-screen booking modal sets the status bar to light content over a dark ground (the `night`
261
+ preset left the iOS clock dark on dark). (#145)
262
+ - **The booking emergency panel is as tall as its content (web and React Native).** The early emergency step grew
263
+ to the dialog's height, a tall red box with nothing in its lower half. (#145)
264
+ - **The vet visit dialog states `aria-modal` (web).** It is a `<dialog>` shown with `showModal()`, so the browser
265
+ already treats it as modal and the page behind it as inert; it now says so in the attribute too, as the records
266
+ dialogs (the share sheet's among them) do, for tools that read attributes and for a DOM without `showModal`. (#164)
267
+ - **The web booking's choices and times are walked with the arrow keys (accessibility).** The option cards (pet,
268
+ where, the emergency check, a cancellation's reason) and the time grid made every choice its own tab stop. Each
269
+ radio group is now one tab stop whose arrows move the pick and wrap, as native radios do; the time grid is one tab
270
+ stop walked with Left, Right, Up, Down, Home and End, where focus never holds a slot (Enter or a click does), and
271
+ a chip stays focusable while its hold is in flight. The review summary is a valid description list again (its
272
+ "Change" sits inside the value it changes), and the visit dialog is named by the heading on screen rather than
273
+ by a title no screen shows. (#156)
274
+ - **The web booking's emergency warning comes into view, and a busy button keeps the focus (accessibility).** At
275
+ 200% zoom (640x450) "Yes, at least one" opened the emergency warning below the step's scroll area: it was
276
+ announced, but a sighted member saw only a Continue that had gone dark. The warning is now scrolled into view
277
+ (`block: 'nearest'`, instant under reduced motion), and the React Native step scrolls to it the same way. A booking
278
+ button that went busy or blocked used the native `disabled` attribute, which drops focus to the partner page's
279
+ `<body>` mid-booking; it is now `aria-disabled`, stays focusable, swallows the press, points `aria-describedby` at
280
+ the reason where the screen shows one, and speaks the deck's own busy words ("Booking…", "Moving…") through a
281
+ polite status region. The step's scroller now contains the review's visually hidden consent inputs, which had
282
+ stretched the visit dialog's own scroll height and scrolled the whole dialog when one took focus. (#154)
283
+
284
+ ### Documentation
285
+
286
+ - **The docs say what the UI and the wire do (production sandbox and Next.js integration tests, 2026-10-01).**
287
+ The vet booking is eight steps with a pet chooser and seven for a member with one pet, not always eight. The chat
288
+ stream's `metadata` frame arrives BEFORE `done`, which repeats its title and follow-up questions; 10-TESTING
289
+ said a frame arrives after `done` and the quickstart said a later `metadata` frame can follow. The examples
290
+ script no longer says a sandbox uses an ordinary `pk_live_` key on staging, and the onboarding page no longer says
291
+ most sandboxes come from self-serve sign-up, which is not open to partners yet. (#165)
292
+ - **Current facts in the README and the vet visit guide.** The README named `0.3.0` and called vet visits "not
293
+ yet enabled", and 16-TELEVET-VISITS.md opened with "draft ... not yet enabled for any partner"; vet visits run
294
+ in production for the partners Everfur enables them for. The README quickstart's `getToken` used a relative
295
+ URL, which does not resolve in a React Native bundle (00-QUICKSTART.md already said so). The camera note said
296
+ to add vision-camera's config plugin: `4.x` has one, `5.x` has none and needs `react-native-nitro-modules` and
297
+ `react-native-nitro-image`. The React Native call setup now names all of Daily's declared peers
298
+ (`@react-native-async-storage/async-storage`, `react-native-background-timer`, `react-native-get-random-values`
299
+ beside the WebRTC build) and the config plugin's one-Expo-SDK peer, with the `--legacy-peer-deps` install and
300
+ the `overrides` entry that keeps the next install and `npm ci` passing. `registerEverfurDaily`'s doc comment on
301
+ React Native now imports it from `@everfur/sdk/televet/call`, where it is exported. Found by the 2026-10-01
302
+ production integration tests of 0.7.1 (web and mobile). (#151)
303
+ - **Test mode is a production sandbox with a `pk_test_` key.** 00-QUICKSTART, 10-TESTING, 12-CLI-AND-MCP and the
304
+ README describe test mode as a sandbox tenant with a `pk_test_` key on the production host (an older sandbox may
305
+ still carry `pk_live_`; staging is early access), the go-live pair, the console's live/test switch, the hosted MCP
306
+ reconnect and the test/staging/production plan environments; 02, 08, 11 and 14 no longer say that no deployment
307
+ accepts `pk_test_`. The operator guide (04-ONBOARDING-A-CUSTOMER) covers going live by pairing. Every heading and
308
+ anchor is unchanged. (#101)
309
+
310
+ ### Tests and tooling
311
+
312
+ - **The records bundle budget is re-measured.** The 2026-10-01 records fixes (#139, #142, #143, #149) each passed
313
+ on their own head and together crossed the React Native records closure's line by 349 gzip bytes, all records
314
+ feature code; the closure still holds no chat, vet visit, video, photo or animations module. The line is the CI
315
+ measurement plus 5%, as every budget entry is. (#152)
316
+ - **Chat's first reads are pinned beside the entitlement verdict.** A test holds the verdict unanswered over the
317
+ real transport and asserts the conversation head, the suggested prompts and the pets requests are already out, so
318
+ a later gate on the verdict cannot add that hop. (#167)
319
+ - **The consent gate's `Allow` is pinned reachable on the dashboard, and the theme guard's development warning is
320
+ pinned to a runtime switch of appearance.** No ancestor of `Allow` on the React Native dashboard is `accessible`;
321
+ switching to another adjusted preset at runtime writes that preset's line once, and a look the guard leaves alone
322
+ writes nothing. (#170)
323
+
324
+ ## [0.7.1] - 2026-10-01
325
+
326
+ ### Fixed
327
+
328
+ - **`RecordShareAudience` is now exported (type-only) from `@everfur/sdk/records/depth` and
329
+ `@everfur/sdk/web/records/depth`**, the audience `RecordsDepthClient.createShare(petRef, { audience })` takes.
330
+ Before, both depth API reports carried an api-extractor warning pinned to a hashed build file
331
+ (`dist/depth-<hash>.d.ts`), so `npm run api:check` failed on every version bump until the reports were
332
+ regenerated.
333
+ - **`EverfurTelevetHost` takes `onPickMedia` (web).** The intake step has had a picker seam since booking photos
334
+ shipped (`onPickMedia` on `EverfurTelevet`), but the default in-page visit never reached it: the host and
335
+ `EverfurTelevetDialog` rendered the flow without a picker, so a partner on the button, chat or its own host
336
+ could not offer photos even with booking media served. The host now passes it through, as it does
337
+ `onAddToCalendar`. Absent, the intake step has no attachment row, as before.
338
+
13
339
  ## [0.7.0] - 2026-10-01
14
340
 
15
341
  ### Added
package/README.md CHANGED
@@ -10,12 +10,15 @@ features without shipping an app update.
10
10
  a blank screen, and cannot be force-enabled client-side.
11
11
  - The app never holds a secret. See [The security model](#the-security-model).
12
12
 
13
- > `0.3.0`, MIT licensed, public on npm. Requires React Native `>=0.74` (or React `>=18` with `react-dom`
13
+ > `0.8.0`, MIT licensed, public on npm. Requires React Native `>=0.74` (or React `>=18` with `react-dom`
14
14
  > for the web entry), and Node `>=18` on the backend that mints sessions.
15
15
 
16
- **Getting keys:** Everfur keys are provisioned by the Everfur team during partner onboarding; there is
17
- no self-serve signup yet. Contact [support@everfur.com](mailto:support@everfur.com) to get set up. The
18
- publishable key is designed to ship inside your app; the secret key is not.
16
+ <!-- DRAFT COPY slot PD1-2 (owner): the README's invitation to get started (self-serve sandbox sign-up), in the owner's words. The factual paragraph below stands without it. -->
17
+ **Getting keys:** a sandbox (test-mode) tenant can be created from the partner console's sandbox page
18
+ (`https://everfur.com/partners/console/sandbox`) where Everfur has opened sandbox sign-up; otherwise the
19
+ Everfur team creates it. Live tenants and their keys are provisioned by the Everfur team. Contact
20
+ [support@everfur.com](mailto:support@everfur.com) to get set up. The publishable key is designed to ship
21
+ inside your app; the secret key is not.
19
22
 
20
23
  ## Install
21
24
 
@@ -36,8 +39,9 @@ Peer dependencies by capability (install only what you use):
36
39
 
37
40
  The camera peer is optional: if you never mount `./video` or `./photo`, you do not need it.
38
41
  `EverfurRecords` accepts the host's picker and upload transport, so use the native modules that fit your
39
- app (including Expo SDK 57) without an SDK peer-dependency constraint. `react-native-vision-camera` also
40
- needs its config plugin in `app.json` and a camera usage description.
42
+ app (including Expo SDK 57) without an SDK peer-dependency constraint. `react-native-vision-camera` needs a
43
+ camera usage description; `4.x` also ships an Expo config plugin (add it to `app.json` plugins), while `5.x`
44
+ ships none and instead needs its own peers `react-native-nitro-modules` and `react-native-nitro-image`.
41
45
 
42
46
  ### Bundler resolution
43
47
 
@@ -70,8 +74,8 @@ Two ways, one wire contract, documented in
70
74
  is [`examples/frame-embed.html`](https://sdk.everfur.com/v1/docs/index.html#web--the-snippet).
71
75
 
72
76
  Chat and records are the web surfaces (records on its own subpath and its own frame document, available
73
- when Everfur enables records for your account); photo and video remain React Native for now. Vet visits (not yet enabled)
74
- have their own entry on both hosts, `@everfur/sdk/web/televet` and `@everfur/sdk/televet`, and a visit opens
77
+ when Everfur enables records for your account); photo and video remain React Native for now. Vet visits (available when Everfur enables them for your
78
+ account) have their own entry on both hosts, `@everfur/sdk/web/televet` and `@everfur/sdk/televet`, and a visit opens
75
79
  INSIDE your app by default: booking, the confirmation and the call run in an Everfur modal (React Native) or
76
80
  dialog (web) that `VetVisitButton` and `EverfurChat` mount themselves; the hosted page is an explicit opt-in,
77
81
  `prefer="redirect"`. The booking screens (`@everfur/sdk/televet/booking`, `@everfur/sdk/web/televet/booking`)
@@ -93,13 +97,14 @@ export default function App() {
93
97
  return (
94
98
  <EverfurProvider
95
99
  config={{
96
- publishableKey: process.env.EXPO_PUBLIC_EVERFUR_PUBLISHABLE_KEY!, // pk_live_... safe to ship
100
+ publishableKey: process.env.EXPO_PUBLIC_EVERFUR_PUBLISHABLE_KEY!, // pk_test_... or pk_live_... safe to ship
97
101
  apiBaseUrl: 'https://api.everfur.com/api/v1',
98
102
  }}
99
103
  // Omit `user` entirely for anonymous mode (publishable key alone, general answers).
100
104
  user={{
101
105
  userRef: userRef('your-stable-user-id'),
102
- getToken: () => fetch('/everfur-session').then((r) => r.text()),
106
+ // An absolute URL: a relative path does not resolve in a React Native bundle.
107
+ getToken: () => fetch('https://your-backend.example.com/everfur/session', { method: 'POST' }).then((r) => r.text()),
103
108
  }}
104
109
  activePet={petRef('pet-ref-123')}
105
110
  >
@@ -140,23 +145,25 @@ import { petRef } from '@everfur/sdk';
140
145
  ## The security model
141
146
 
142
147
  The one rule: **the app never holds a secret.** Two provisioning credentials, kept apart, plus a read-only
143
- docs key for tooling. Test mode is a sandbox tenant on the staging API with an ordinary `pk_live_` key
144
- (`livemode: false` in its entitlements); no deployed environment accepts a `pk_test_` key.
148
+ docs key for tooling. Test mode is a sandbox tenant with a `pk_test_` key on the production API
149
+ (`livemode: false` in its entitlements); the API accepts a `pk_test_` key only for a sandbox tenant, so a test
150
+ key never reaches live data. An older sandbox may still carry a `pk_live_` key, which keeps working, so read
151
+ `livemode`, not the prefix. Staging (`https://api-staging.everfur.com/api/v1`) is for early access.
145
152
 
146
153
  | Credential | Where it lives | What it does |
147
154
  | --- | --- | --- |
148
- | Publishable key `pk_live_...` | In the app (`EXPO_PUBLIC_...`). Safe to ship. | Names your tenant. Cannot mint a session or grant a feature by itself. |
155
+ | Publishable key `pk_test_...` (sandbox) or `pk_live_...` (live) | In the app (`EXPO_PUBLIC_...`). Safe to ship. | Names your tenant. Cannot mint a session or grant a feature by itself. |
149
156
  | Partner secret key `sk_partner_...` | **Only** on your server. Never in the app, its bundle, env, git, or logs. | Mints session tokens. Revocable and rotatable without an app release. |
150
157
  | Session token (bearer) | App memory, short-lived. | Authenticates one end user; re-minted via your backend on a 401. Signed, holds no secret, 15 minutes by default (1 minute to 1 hour). |
151
158
  | Docs key `dk_partner_...` | Only in the MCP server or CLI configuration on a developer machine. | Read-only: fetches the Everfur contract from `GET /partners/contract` and opens no other route. |
152
159
 
153
160
  Your backend exchanges the two provisioning credentials for a short-lived session by calling
154
- `POST /widget/v1/sessions` with `x-everfur-partner-key: pk_live_...` + `x-everfur-partner-secret-key:
155
- sk_partner_...`. The SDK ships a **server-only** helper (`mintPartnerSession`, imported from
161
+ `POST /widget/v1/sessions` with `x-everfur-partner-key: pk_...` + `x-everfur-partner-secret-key:
162
+ sk_partner_...`, both from the same tenant. The SDK ships a **server-only** helper (`mintPartnerSession`, imported from
156
163
  `@everfur/sdk/server`) so you do not hand-roll that request. That helper is deliberately absent from every
157
164
  React-Native-resolvable bundle: a device can never import it, so the secret has no path onto the phone.
158
165
 
159
- **Do:** ship `pk_live_` in the app; keep `sk_partner_` in a server secret manager; mint sessions on your
166
+ **Do:** ship the publishable key (`pk_test_` or `pk_live_`) in the app; keep `sk_partner_` in a server secret manager; mint sessions on your
160
167
  backend; serve everything over HTTPS.
161
168
  **Never:** put `sk_partner_` in the app; mint sessions on the device; try to force-enable a feature
162
169
  client-side (the server re-checks and returns `403`).
@@ -185,6 +192,7 @@ off-state, never a blank region.
185
192
  | `@everfur/sdk/animations` | server-driven pet-state media surface |
186
193
  | `@everfur/sdk/web` | the browser entry: `EverfurProvider`, `CapabilityGate`, `EverfurChat`, `useEverfurChat`, the theme hook, rendered with `react-dom`; withheld from React Native bundles the way `./server` is |
187
194
  | `@everfur/sdk/web/records` | records for a browser host: `EverfurRecordsPage` (the Everfur web app's records page, complete on its own: request from a clinic, upload, request status, the full record with vaccinations, all inside your page), the named screens, `EverfurRecords` (the React Native-shaped flow), `useEverfurRecords`, `createWebRecordsUploadTransport`, `fileToHandle`; its own entry, withheld from React Native like `./web`; available when Everfur enables records for the account. Frame mode serves it with `surface: 'records'` (see [chapter 13](https://sdk.everfur.com/v1/docs/index.html#web--2-frame-mode-the-loader-and-the-frame)) |
195
+ | `@everfur/sdk/web/records/upload` | `createWebRecordsUploadTransport` alone, for the `uploadTransport` on the provider config at your app root; the same function `@everfur/sdk/web/records` exports, without the records entry |
188
196
  | `@everfur/sdk/server` | **server-only** `mintPartnerSession` (no React Native resolution; never bundled on-device) |
189
197
  | `@everfur/sdk/server/events` | **server-only, Node only** partner events (preview): `constructEvent` to verify webhooks, `sendPartnerEvent`; see [chapter 15](https://sdk.everfur.com/v1/docs/index.html#sec-events) |
190
198
  | `@everfur/sdk/testing`, `@everfur/sdk/testing/rn`, `@everfur/sdk/testing/web` | test seams (`MockTransport`, `useEverfurTestEntitlements`, ...) for your own tests |
@@ -2,9 +2,9 @@ import { ReactNode } from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
3
  import { P as PetRef, C as ConversationId } from './ids-B2GAAifq.js';
4
4
  import { F as FileHandle } from './ports-BN6RHF9W.js';
5
- import { a as ChatCheckinTarget } from './ports-nXHf50_X.js';
6
- import { W as WidgetPet, P as PetProfileInput } from './petsRepository-CuYZZaI5.js';
7
- import { U as UrgencyDisplayLevel, L as LifecyclePort, E as EverfurChatDraftStore } from './attachments-CXAnZcEg.js';
5
+ import { a as ChatCheckinTarget } from './ports-BUXF0Rp3.js';
6
+ import { W as WidgetPet, P as PetProfileInput } from './petsRepository-B2lO1sKS.js';
7
+ import { U as UrgencyDisplayLevel, L as LifecyclePort, E as EverfurChatDraftStore } from './attachments-y_aO4hlk.js';
8
8
 
9
9
  /** Where a picked image comes from: the host's photo library, or its camera. */
10
10
  type AttachmentSource = 'photo' | 'camera';
@@ -2,9 +2,9 @@ import { ReactNode } from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
3
  import { P as PetRef, C as ConversationId } from './ids-B2GAAifq.cjs';
4
4
  import { F as FileHandle } from './ports-BN6RHF9W.cjs';
5
- import { a as ChatCheckinTarget } from './ports-ZmJYgd5I.cjs';
6
- import { W as WidgetPet, P as PetProfileInput } from './petsRepository-BJsJ4cju.cjs';
7
- import { U as UrgencyDisplayLevel, L as LifecyclePort, E as EverfurChatDraftStore } from './attachments-CUOGILir.cjs';
5
+ import { a as ChatCheckinTarget } from './ports-C40S2V14.cjs';
6
+ import { W as WidgetPet, P as PetProfileInput } from './petsRepository-BP9nhM1R.cjs';
7
+ import { U as UrgencyDisplayLevel, L as LifecyclePort, E as EverfurChatDraftStore } from './attachments-DbFwz4iq.cjs';
8
8
 
9
9
  /** Where a picked image comes from: the host's photo library, or its camera. */
10
10
  type AttachmentSource = 'photo' | 'camera';
@@ -1,5 +1,5 @@
1
1
  import { CSSProperties, ReactNode } from 'react';
2
- import { f as RecordsScreenBaseProps } from './useRecordsDepth-fPSv-GXU.js';
2
+ import { f as RecordsScreenBaseProps } from './useRecordsDepth-DuPUJFDQ.js';
3
3
 
4
4
  interface EverfurRecordsDepthProps extends Pick<RecordsScreenBaseProps, 'petRef' | 'petName' | 'onNavigate' | 'onBack'> {
5
5
  readonly style?: CSSProperties;
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
- import { f as RecordsScreenBaseProps } from './useRecordsDepth-CARTuHKh.cjs';
3
+ import { f as RecordsScreenBaseProps } from './useRecordsDepth-D6a6-_u_.cjs';
4
4
 
5
5
  interface EverfurRecordsDepthProps extends Pick<RecordsScreenBaseProps, 'petRef' | 'petName' | 'onNavigate' | 'onBack'> {
6
6
  readonly style?: StyleProp<ViewStyle>;
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
- import { f as RecordsScreenBaseProps } from './useRecordsDepth-fPSv-GXU.js';
3
+ import { f as RecordsScreenBaseProps } from './useRecordsDepth-DuPUJFDQ.js';
4
4
 
5
5
  interface EverfurRecordsDepthProps extends Pick<RecordsScreenBaseProps, 'petRef' | 'petName' | 'onNavigate' | 'onBack'> {
6
6
  readonly style?: StyleProp<ViewStyle>;
@@ -1,5 +1,5 @@
1
1
  import { CSSProperties, ReactNode } from 'react';
2
- import { f as RecordsScreenBaseProps } from './useRecordsDepth-CARTuHKh.cjs';
2
+ import { f as RecordsScreenBaseProps } from './useRecordsDepth-D6a6-_u_.cjs';
3
3
 
4
4
  interface EverfurRecordsDepthProps extends Pick<RecordsScreenBaseProps, 'petRef' | 'petName' | 'onNavigate' | 'onBack'> {
5
5
  readonly style?: CSSProperties;
@@ -1,4 +1,4 @@
1
- import { b as EverfurErrorCode, E as EverfurError } from './EverfurError-DmELDpXp.js';
1
+ import { b as EverfurErrorCode, E as EverfurError } from './EverfurError-pEXDbUJ1.js';
2
2
 
3
3
  /**
4
4
  * Recovery policy. A host may narrow retry ceilings (e.g. a metered plan) but can never widen the SDK's
@@ -1,4 +1,4 @@
1
- import { b as EverfurErrorCode, E as EverfurError } from './EverfurError-DmELDpXp.cjs';
1
+ import { b as EverfurErrorCode, E as EverfurError } from './EverfurError-pEXDbUJ1.cjs';
2
2
 
3
3
  /**
4
4
  * Recovery policy. A host may narrow retry ceilings (e.g. a metered plan) but can never widen the SDK's
@@ -1,5 +1,5 @@
1
1
  /** The frozen error taxonomy. Additive-only under the dated-contract policy. */
2
- type EverfurErrorCode = 'authExpired' | 'authRejected' | 'sessionRequired' | 'accessDenied' | 'paymentRequired' | 'quotaExceeded' | 'rateLimited' | 'validationFailed' | 'sdkUpdateRequired' | 'conflict' | 'idempotencyConflict' | 'idempotencyInFlight' | 'conversationNotFound' | 'petNotFound' | 'resourceNotFound' | 'streamTimeout' | 'mediaUploadFailed' | 'moderationBlocked' | 'upstreamLlmTimeout' | 'internalError' | 'notImplemented' | 'serviceUnavailable' | 'streamIncomplete' | 'unknown';
2
+ type EverfurErrorCode = 'authExpired' | 'authRejected' | 'sessionRequired' | 'accessDenied' | 'paymentRequired' | 'quotaExceeded' | 'rateLimited' | 'validationFailed' | 'sdkUpdateRequired' | 'conflict' | 'idempotencyConflict' | 'idempotencyInFlight' | 'conversationNotFound' | 'petNotFound' | 'resourceNotFound' | 'streamTimeout' | 'mediaUploadFailed' | 'moderationBlocked' | 'upstreamLlmTimeout' | 'internalError' | 'notImplemented' | 'serviceUnavailable' | 'streamIncomplete' | 'identitySignatureRequired' | 'unknown';
3
3
  /** Every code, frozen, for iteration in conformance / property tests. */
4
4
  declare const EVERFUR_ERROR_CODES: readonly EverfurErrorCode[];
5
5
  /**
@@ -137,6 +137,12 @@ interface EverfurError {
137
137
  * snake_case token is kept; anything else is dropped rather than carried.
138
138
  */
139
139
  readonly reasonCode?: string;
140
+ /**
141
+ * The wait the server named, in seconds, when it named one: the body's `retry_after_seconds`, or a `Retry-After`
142
+ * header on a 429 or 503. The anonymous-session budget's 429 (`reasonCode` `anonymous_session_limit`) carries
143
+ * it; the SDK does not retry past it.
144
+ */
145
+ readonly retryAfterSeconds?: number;
140
146
  /** raw wire text; telemetry / logs only, NEVER rendered. NON-ENUMERABLE. */
141
147
  readonly wireMessage?: string;
142
148
  /** Opaque parsed wire payload for debugging (untrusted, unvalidated). NON-ENUMERABLE. */
@@ -169,6 +175,8 @@ interface MakeEverfurErrorOptions {
169
175
  readonly reasonCode?: string | null;
170
176
  /** Opaque parsed wire payload. Carried NON-ENUMERABLY for debugging; never rendered. */
171
177
  readonly raw?: unknown;
178
+ /** The wait the response named, in seconds. Kept only when finite and not negative. */
179
+ readonly retryAfterSeconds?: number | null;
172
180
  }
173
181
  declare function makeEverfurError(code: EverfurErrorCode, opts?: MakeEverfurErrorOptions): EverfurError;
174
182
 
@@ -1,5 +1,5 @@
1
1
  /** The frozen error taxonomy. Additive-only under the dated-contract policy. */
2
- type EverfurErrorCode = 'authExpired' | 'authRejected' | 'sessionRequired' | 'accessDenied' | 'paymentRequired' | 'quotaExceeded' | 'rateLimited' | 'validationFailed' | 'sdkUpdateRequired' | 'conflict' | 'idempotencyConflict' | 'idempotencyInFlight' | 'conversationNotFound' | 'petNotFound' | 'resourceNotFound' | 'streamTimeout' | 'mediaUploadFailed' | 'moderationBlocked' | 'upstreamLlmTimeout' | 'internalError' | 'notImplemented' | 'serviceUnavailable' | 'streamIncomplete' | 'unknown';
2
+ type EverfurErrorCode = 'authExpired' | 'authRejected' | 'sessionRequired' | 'accessDenied' | 'paymentRequired' | 'quotaExceeded' | 'rateLimited' | 'validationFailed' | 'sdkUpdateRequired' | 'conflict' | 'idempotencyConflict' | 'idempotencyInFlight' | 'conversationNotFound' | 'petNotFound' | 'resourceNotFound' | 'streamTimeout' | 'mediaUploadFailed' | 'moderationBlocked' | 'upstreamLlmTimeout' | 'internalError' | 'notImplemented' | 'serviceUnavailable' | 'streamIncomplete' | 'identitySignatureRequired' | 'unknown';
3
3
  /** Every code, frozen, for iteration in conformance / property tests. */
4
4
  declare const EVERFUR_ERROR_CODES: readonly EverfurErrorCode[];
5
5
  /**
@@ -137,6 +137,12 @@ interface EverfurError {
137
137
  * snake_case token is kept; anything else is dropped rather than carried.
138
138
  */
139
139
  readonly reasonCode?: string;
140
+ /**
141
+ * The wait the server named, in seconds, when it named one: the body's `retry_after_seconds`, or a `Retry-After`
142
+ * header on a 429 or 503. The anonymous-session budget's 429 (`reasonCode` `anonymous_session_limit`) carries
143
+ * it; the SDK does not retry past it.
144
+ */
145
+ readonly retryAfterSeconds?: number;
140
146
  /** raw wire text; telemetry / logs only, NEVER rendered. NON-ENUMERABLE. */
141
147
  readonly wireMessage?: string;
142
148
  /** Opaque parsed wire payload for debugging (untrusted, unvalidated). NON-ENUMERABLE. */
@@ -169,6 +175,8 @@ interface MakeEverfurErrorOptions {
169
175
  readonly reasonCode?: string | null;
170
176
  /** Opaque parsed wire payload. Carried NON-ENUMERABLY for debugging; never rendered. */
171
177
  readonly raw?: unknown;
178
+ /** The wait the response named, in seconds. Kept only when finite and not negative. */
179
+ readonly retryAfterSeconds?: number | null;
172
180
  }
173
181
  declare function makeEverfurError(code: EverfurErrorCode, opts?: MakeEverfurErrorOptions): EverfurError;
174
182
 
@@ -1,4 +1,4 @@
1
- import { E as EverfurError } from './EverfurError-DmELDpXp.cjs';
1
+ import { E as EverfurError } from './EverfurError-pEXDbUJ1.cjs';
2
2
 
3
3
  /**
4
4
  * The settle type. An async public operation returns this instead of throwing for an EXPECTED failure.
@@ -1,4 +1,4 @@
1
- import { E as EverfurError } from './EverfurError-DmELDpXp.js';
1
+ import { E as EverfurError } from './EverfurError-pEXDbUJ1.js';
2
2
 
3
3
  /**
4
4
  * The settle type. An async public operation returns this instead of throwing for an EXPECTED failure.