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