@everfur/sdk 0.1.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 (89) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/animations/package.json +8 -0
  5. package/chat/package.json +8 -0
  6. package/client/package.json +8 -0
  7. package/core/package.json +8 -0
  8. package/dist/CameraPort-Cv31Pz7g.d.cts +29 -0
  9. package/dist/CameraPort-Cv31Pz7g.d.ts +29 -0
  10. package/dist/ChatController-CKdBvPj2.d.ts +146 -0
  11. package/dist/ChatController-CpUMvvZf.d.cts +146 -0
  12. package/dist/EverfurResult-D92-uL82.d.cts +240 -0
  13. package/dist/EverfurResult-D92-uL82.d.ts +240 -0
  14. package/dist/FilePort-BabWrv7I.d.cts +22 -0
  15. package/dist/FilePort-BabWrv7I.d.ts +22 -0
  16. package/dist/PhotoController-BItt5M7u.d.cts +43 -0
  17. package/dist/PhotoController-D8zMTdcW.d.ts +43 -0
  18. package/dist/TelemetryPort-BDNr00hu.d.cts +12 -0
  19. package/dist/TelemetryPort-BDNr00hu.d.ts +12 -0
  20. package/dist/animations/index.cjs +1997 -0
  21. package/dist/animations/index.d.cts +517 -0
  22. package/dist/animations/index.d.ts +517 -0
  23. package/dist/animations/index.js +1972 -0
  24. package/dist/cacheEpoch-DknKn3S0.d.cts +36 -0
  25. package/dist/cacheEpoch-DknKn3S0.d.ts +36 -0
  26. package/dist/chat/index.cjs +1170 -0
  27. package/dist/chat/index.d.cts +59 -0
  28. package/dist/chat/index.d.ts +59 -0
  29. package/dist/chat/index.js +1167 -0
  30. package/dist/client/index.cjs +2317 -0
  31. package/dist/client/index.d.cts +65 -0
  32. package/dist/client/index.d.ts +65 -0
  33. package/dist/client/index.js +2217 -0
  34. package/dist/config-BSjBdxrZ.d.cts +501 -0
  35. package/dist/config-CiJ0PVBB.d.ts +501 -0
  36. package/dist/core/index.cjs +3175 -0
  37. package/dist/core/index.d.cts +70 -0
  38. package/dist/core/index.d.ts +70 -0
  39. package/dist/core/index.js +3163 -0
  40. package/dist/identity-Brl-lDd6.d.cts +91 -0
  41. package/dist/identity-DK9zORrG.d.ts +91 -0
  42. package/dist/ids-CJ1S6adf.d.cts +46 -0
  43. package/dist/ids-CJ1S6adf.d.ts +46 -0
  44. package/dist/index.cjs +4488 -0
  45. package/dist/index.d.cts +156 -0
  46. package/dist/index.d.ts +156 -0
  47. package/dist/index.js +4464 -0
  48. package/dist/petsRepository-BEGb97M9.d.cts +326 -0
  49. package/dist/petsRepository-Bu18r2kK.d.ts +326 -0
  50. package/dist/photo/index.cjs +2189 -0
  51. package/dist/photo/index.d.cts +43 -0
  52. package/dist/photo/index.d.ts +43 -0
  53. package/dist/photo/index.js +2186 -0
  54. package/dist/projection-CeIUsbSk.d.cts +8 -0
  55. package/dist/projection-CeIUsbSk.d.ts +8 -0
  56. package/dist/records/index.cjs +2840 -0
  57. package/dist/records/index.d.cts +224 -0
  58. package/dist/records/index.d.ts +224 -0
  59. package/dist/records/index.js +2834 -0
  60. package/dist/requestFunnel-DuUH-kAe.d.cts +28 -0
  61. package/dist/requestFunnel-dio5OmR9.d.ts +28 -0
  62. package/dist/resolve-Dq_4_agU.d.cts +86 -0
  63. package/dist/resolve-Dq_4_agU.d.ts +86 -0
  64. package/dist/runtime-BgQnA594.d.cts +349 -0
  65. package/dist/runtime-CBA-LvdM.d.ts +349 -0
  66. package/dist/server/index.cjs +533 -0
  67. package/dist/server/index.d.cts +48 -0
  68. package/dist/server/index.d.ts +48 -0
  69. package/dist/server/index.js +530 -0
  70. package/dist/testing/index.cjs +825 -0
  71. package/dist/testing/index.d.cts +113 -0
  72. package/dist/testing/index.d.ts +113 -0
  73. package/dist/testing/index.js +822 -0
  74. package/dist/testing/rn/index.cjs +449 -0
  75. package/dist/testing/rn/index.d.cts +49 -0
  76. package/dist/testing/rn/index.d.ts +49 -0
  77. package/dist/testing/rn/index.js +444 -0
  78. package/dist/video/index.cjs +1941 -0
  79. package/dist/video/index.d.cts +66 -0
  80. package/dist/video/index.d.ts +66 -0
  81. package/dist/video/index.js +1938 -0
  82. package/package.json +311 -0
  83. package/photo/package.json +8 -0
  84. package/records/package.json +8 -0
  85. package/server/device-blocked.cjs +15 -0
  86. package/server/package.json +9 -0
  87. package/testing/package.json +8 -0
  88. package/testing/rn/package.json +8 -0
  89. package/video/package.json +8 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@everfur/sdk`.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions are semver.
6
+
7
+ This is the first release. The line is deliberately pre-1.0: the API is stable enough to build against and
8
+ is guarded by api-extractor golden reports across all eleven subpaths, but 1.0.0 is a promise about breaking
9
+ changes that has not been earned by any production integration yet.
10
+
11
+ ## [0.1.0] - 2026-08-23
12
+
13
+ ### Fixed — correctness
14
+
15
+ - **Anonymous mode could never send a message.** The SDK never sent `x-everfur-session-id`, and the server
16
+ derives the anonymous identity from it, minting a fresh one per request. A conversation was created as one
17
+ user and the message sent as another, so every send settled `conversationNotFound`. One stable v4 id is
18
+ now minted per runtime and sent on both the unary and streaming paths.
19
+ - **Sign-out did not sign out.** `identity.clear()` only dropped the token cache, so a capability holding a
20
+ pre-logout `AuthContext` re-entered the partner's `getToken` and minted a fresh token for the signed-out
21
+ user. It is now terminal. The anonymous branch no longer carries the signed-out `userRef`, and the session
22
+ id rotates on logout.
23
+ - **A misconfigured provider was a white screen.** `EverfurProvider` threw synchronously from its own render,
24
+ above every error boundary a partner can install. Config now settles into a disabled runtime: children
25
+ render, every capability shows its off-state at `disabled.level === 'sdk_config'`, and the transport
26
+ refuses.
27
+ - **The runtime is rebuilt when the config identity changes or it has been disposed.** An asynchronously
28
+ supplied `publishableKey` never took effect, and React StrictMode's double-mount permanently killed it.
29
+ - **Records polled every 4 seconds forever.** `MAX_POLLS` never bound, because the counter lived inside an
30
+ effect keyed on an object rebuilt each tick. Measured 300 polls against a cap of 30.
31
+ - **Chat message bounds are measured in code points**, as the server measures them; UTF-16 units counted
32
+ every emoji twice and refused messages at half the real limit.
33
+ - Neither `POST /conversations` nor the SSE send is auto-replayed: the widget lane does not read
34
+ `Idempotency-Key`, so a retry was a second row, not a dedup.
35
+
36
+ ### Fixed — security
37
+
38
+ - **`@everfur/sdk/server` is no longer resolvable from a React Native bundle.** The subpath merely omitted a
39
+ `react-native` export condition, which is not a block; it now maps to `null`. This is the module carrying
40
+ `sk_partner_`.
41
+ - `mintPartnerSession` validates its base URL instead of concatenating it, so a cleartext
42
+ `EVERFUR_API_BASE_URL` can no longer put the partner secret key on the wire.
43
+ - Config failure messages echo a redacted origin, never the raw base URL, which could carry userinfo.
44
+ - A secret key pasted into `publishableKey` is refused.
45
+
46
+ ### Added
47
+
48
+ - `registerPet` on the runtime and the handle, plus `createPetsRepository` on `@everfur/sdk/client`. Records
49
+ and consent are unreachable for any pet the partner has not registered, and there was previously no way to
50
+ register one from the SDK.
51
+ - The clinical disclaimer is mounted on every pet-health surface, not only chat.
52
+ - `npm run docs:check` type-checks every documented snippet.
53
+ - `EverfurRequestBase` and `MultipartBody` are exported, so a `TransportPort` wrapper can extend the request
54
+ shape without depending on the union (see the `EverfurRequest` note below).
55
+ - `EverfurConfig.errorPolicy`'s `shouldRetry` and `maxAttempts` are now actually consulted. They were typed,
56
+ exported and documented while reaching no request in the SDK. Both narrow only: neither can make a spent
57
+ daily quota retryable nor resurrect a terminal code, and an SDK per-call cap still outranks them.
58
+
59
+ ### Changed — potentially breaking
60
+
61
+ - **`DisabledLevel` gained an `sdk_config` member.** A consumer with an exhaustive `switch` over that union
62
+ will no longer compile. This is the one deliberate widening in this set.
63
+ - **`AuthContext.sessionId` is now required.** Building the context without one silently reintroduced the
64
+ anonymous-identity defect, so the type now catches it.
65
+ - **`EverfurRequest` is now a union, not an interface.** It was
66
+ `EverfurRequestBase & ({ body?: unknown; multipart?: undefined } | { body?: undefined; multipart: MultipartBody })`,
67
+ so carrying both a JSON body and a multipart body is unrepresentable rather than merely discouraged: the
68
+ combination produces a `content-type` without the platform-generated boundary and the server cannot parse
69
+ the request. Two ordinary patterns on the `TransportPort` seam stop compiling, both with a one-line fix:
70
+ - `interface X extends EverfurRequest` fails with TS2312 (an interface cannot extend a union). Extend the
71
+ newly exported `EverfurRequestBase` instead.
72
+ - `inner.request({ ...req, body: next })` inside a transport wrapper fails, because TypeScript cannot tell
73
+ which arm `req` is. Narrow first: `if (req.multipart === undefined) inner.request({ ...req, body: next })`.
74
+ - `RecordsRepository` dropped the `idem` parameters its only implementation ignored.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Everfur Health, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @everfur/sdk (React Native)
2
+
3
+ Drop Everfur's AI vet assistant and pet-health features (records, gait/video, photo checkup, pet-state
4
+ media) into a React Native app. What each of your customers and users can see is decided **server-side** by
5
+ Everfur's entitlement control plane and rendered by the SDK, so you gate features without shipping an app
6
+ update.
7
+
8
+ - Hexagonal, framework-free core with a thin RN layer; every surface settles (no thrown domain errors).
9
+ - Server-authoritative entitlements: a feature you are not entitled to renders a deliberate off-state, never
10
+ a blank screen, and cannot be force-enabled client-side.
11
+ - The app never holds a secret. See [The security model](#the-security-model).
12
+
13
+ > `0.1.0`, MIT licensed, public on npm. Requires React Native `>=0.74`, and Node `>=18` on the backend
14
+ > that mints sessions.
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.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm i @everfur/sdk
24
+ # Always-required peers:
25
+ npm i react react-native react-native-safe-area-context
26
+ ```
27
+
28
+ Peer dependencies by capability (install only what you use):
29
+
30
+ | Capability | Extra peer deps | Notes |
31
+ | --- | --- | --- |
32
+ | chat, entitlements | none | works with react + react-native alone |
33
+ | records | `expo-file-system`, `expo-image-manipulator` | document upload + client-side image prep |
34
+ | video (gait), photo (checkup) | `react-native-vision-camera` | **use `4.x` on RN 0.74**; `5.x` requires the new architecture |
35
+
36
+ The camera and file-system peers are optional: if you never mount `./video`, `./photo`, or `./records`,
37
+ you do not need them. `react-native-vision-camera` also needs its config plugin in `app.json` and a camera
38
+ usage description.
39
+
40
+ ### Bundler resolution
41
+
42
+ No Metro configuration is required. Every subpath (`@everfur/sdk/chat`, `/records`, `/video`, `/photo`,
43
+ `/animations`, `/client`, `/core`, `/testing`, `/testing/rn`) resolves on a stock install. Modern bundlers
44
+ read the package `exports` map; Metro 0.80.x, which ships with Expo 51, defaults to
45
+ `unstable_enablePackageExports: false` and never reads it, so the package also ships classic root-level
46
+ resolution shims for the same subpaths. You do not need to turn package exports on, and turning them on does
47
+ not change what you get.
48
+
49
+ `@everfur/sdk/server` is the one exception, deliberately: it is your **backend** module and carries the
50
+ `sk_partner_` session minter. If it is ever pulled into an app bundle it throws on import rather than
51
+ shipping the minter to a device. Import it from your server only.
52
+
53
+ ## Quickstart
54
+
55
+ Wrap your tree once, high, then drop features in behind a gate. This is the exact shape the reference app
56
+ uses:
57
+
58
+ ```tsx
59
+ import { EverfurProvider, EverfurChat, petRef, userRef } from '@everfur/sdk';
60
+
61
+ export default function App() {
62
+ return (
63
+ <EverfurProvider
64
+ config={{
65
+ publishableKey: process.env.EXPO_PUBLIC_EVERFUR_PUBLISHABLE_KEY!, // pk_live_... safe to ship
66
+ apiBaseUrl: 'https://api.everfur.com/api/v1',
67
+ }}
68
+ // Omit `user` entirely for anonymous mode (publishable key alone, general answers).
69
+ user={{
70
+ userRef: userRef('your-stable-user-id'),
71
+ getToken: () => fetch('/everfur-session').then((r) => r.text()),
72
+ }}
73
+ activePet={petRef('pet-ref-123')}
74
+ >
75
+ {/* Every prebuilt widget self-gates. CapabilityGate is for YOUR surrounding UI - a tab, a nav
76
+ entry - not for wrapping a widget that already gates itself. */}
77
+ <EverfurChat petRef={petRef('pet-ref-123')} />
78
+ </EverfurProvider>
79
+ );
80
+ }
81
+ ```
82
+
83
+ `getToken` calls **your** backend for a short-lived session token (never a secret in the app):
84
+
85
+ ```ts
86
+ async function getToken(): Promise<string> {
87
+ const res = await fetch('https://your-backend.example.com/everfur/session', {
88
+ method: 'POST',
89
+ headers: { authorization: `Bearer ${yourAppsUserToken}` },
90
+ });
91
+ const { sessionToken } = await res.json();
92
+ return sessionToken;
93
+ }
94
+ ```
95
+
96
+ The SDK calls `getToken` lazily and re-calls it once after a `401`, so a fresh token is always used without
97
+ you managing expiry. Records lives on its own subpath:
98
+
99
+ ```tsx
100
+ import { EverfurRecords } from '@everfur/sdk/records';
101
+ import { petRef } from '@everfur/sdk';
102
+
103
+ <EverfurRecords petRef={petRef('pet-ref-123')} />
104
+ ```
105
+
106
+ ## The security model
107
+
108
+ The one rule: **the app never holds a secret.** Two credentials, kept apart.
109
+
110
+ | Credential | Where it lives | What it does |
111
+ | --- | --- | --- |
112
+ | 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. |
113
+ | 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. |
114
+ | Session token (bearer) | App memory, short-lived. | Authenticates one end user; re-minted via your backend on a 401. |
115
+
116
+ Your backend exchanges the two provisioning credentials for a short-lived session by calling
117
+ `POST /widget/v1/sessions` with `x-everfur-partner-key: pk_live_...` + `x-everfur-partner-secret-key:
118
+ sk_partner_...`. The SDK ships a **server-only** helper (`mintPartnerSession`, imported from
119
+ `@everfur/sdk/server`) so you do not hand-roll that request. That helper is deliberately absent from every
120
+ React-Native-resolvable bundle: a device can never import it, so the secret has no path onto the phone.
121
+
122
+ **Do:** ship `pk_live_` in the app; keep `sk_partner_` in a server secret manager; mint sessions on your
123
+ backend; serve everything over HTTPS.
124
+ **Never:** put `sk_partner_` in the app; mint sessions on the device; try to force-enable a feature
125
+ client-side (the server re-checks and returns `403`).
126
+
127
+ ## How feature access works
128
+
129
+ Access is decided server-side and delivered to the SDK (`GET /widget/v1/entitlements`, fail-closed). Each
130
+ surface sits behind `CapabilityGate`; a capability you are not entitled to renders a deliberate off-state,
131
+ and it cannot be force-enabled client-side because the API re-checks on every request. A coarse feature
132
+ (`chat`, `records`, `video`, ...) is enabled only when every dotted sub-feature resource it requires is
133
+ granted (for example `records` needs both `records.upload.create` and `records.record.read`). You set these
134
+ per customer and per user in Everfur's admin control plane; the app renders the verdict.
135
+
136
+ Every SDK surface renders all four states (loading, empty, error, populated); a denied surface renders its
137
+ off-state, never a blank region.
138
+
139
+ ## Public entry points
140
+
141
+ | Import | Contents |
142
+ | --- | --- |
143
+ | `@everfur/sdk` | `EverfurProvider`, `CapabilityGate`, `EverfurChat`, `useEverfurChat`, `CapabilityName`, error/result types, branded id helpers (`petRef`, `userRef`, `conversationId`) |
144
+ | `@everfur/sdk/records` | `EverfurRecords`, records hooks + types |
145
+ | `@everfur/sdk/video` | gait / live-scan surface |
146
+ | `@everfur/sdk/photo` | photo checkup surface |
147
+ | `@everfur/sdk/animations` | server-driven pet-state media surface |
148
+ | `@everfur/sdk/server` | **server-only** `mintPartnerSession` (no React Native resolution; never bundled on-device) |
149
+ | `@everfur/sdk/testing`, `@everfur/sdk/testing/rn` | test seams (`MockTransport`, ...) for your own tests |
150
+
151
+ `@everfur/sdk/client` and `@everfur/sdk/core` expose the framework-free layers for advanced integrators;
152
+ most apps only need the root entry plus the capability subpaths.
153
+
154
+ ## Reference app
155
+
156
+ A full, runnable integration (a fictional "Pawtrail" app plus a minimal mint backend that holds the secret)
157
+ lives at [`examples/minimal-integration.tsx`](examples/minimal-integration.tsx) in this repository — it is
158
+ type-checked by `npm run docs:check`, so it cannot rot. It shows anonymous mode, the two-credential session
159
+ mint, pet registration and sign-out. (A separate `everfur-integration-example-rn` app exists internally but
160
+ is not published anywhere a partner can reach, so do not rely on references to it.) It gates every
161
+ capability, and is the copy-paste source for the snippets above.
@@ -0,0 +1,8 @@
1
+ {
2
+ "//": "GENERATED by scripts/subpath-shims.mjs from the package.json \"exports\" map. Do not edit by hand; run `npm run build`.",
3
+ "react-native": "../dist/animations/index.js",
4
+ "module": "../dist/animations/index.js",
5
+ "main": "../dist/animations/index.cjs",
6
+ "types": "../dist/animations/index.d.ts",
7
+ "sideEffects": false
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "//": "GENERATED by scripts/subpath-shims.mjs from the package.json \"exports\" map. Do not edit by hand; run `npm run build`.",
3
+ "react-native": "../dist/chat/index.js",
4
+ "module": "../dist/chat/index.js",
5
+ "main": "../dist/chat/index.cjs",
6
+ "types": "../dist/chat/index.d.ts",
7
+ "sideEffects": false
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "//": "GENERATED by scripts/subpath-shims.mjs from the package.json \"exports\" map. Do not edit by hand; run `npm run build`.",
3
+ "react-native": "../dist/client/index.js",
4
+ "module": "../dist/client/index.js",
5
+ "main": "../dist/client/index.cjs",
6
+ "types": "../dist/client/index.d.ts",
7
+ "sideEffects": false
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "//": "GENERATED by scripts/subpath-shims.mjs from the package.json \"exports\" map. Do not edit by hand; run `npm run build`.",
3
+ "react-native": "../dist/core/index.js",
4
+ "module": "../dist/core/index.js",
5
+ "main": "../dist/core/index.cjs",
6
+ "types": "../dist/core/index.d.ts",
7
+ "sideEffects": false
8
+ }
@@ -0,0 +1,29 @@
1
+ /** Opaque, host-defined capture request. Kept minimal at the spine; video/photo specs extend it. */
2
+ interface CaptureOptions {
3
+ /** 'photo' | 'video'; the coarse capture mode a capability requests. */
4
+ readonly mode: 'photo' | 'video';
5
+ /** Optional soft ceiling for a video capture, in seconds. */
6
+ readonly maxDurationSeconds?: number;
7
+ }
8
+ /** Opaque handle to captured media the FilePort can upload. Server-owned shape is defined by video/photo. */
9
+ interface MediaSource {
10
+ /** Local file URI produced by the capture. */
11
+ readonly uri: string;
12
+ /** MIME type, e.g. 'image/jpeg' | 'video/mp4'. */
13
+ readonly mimeType: string;
14
+ readonly widthPx?: number;
15
+ readonly heightPx?: number;
16
+ readonly durationSeconds?: number;
17
+ }
18
+ /**
19
+ * Camera capture. Absent (or an unavailable native peer) => `isAvailable:false`; the SDK never throws for a
20
+ * missing capability, it renders the OffState.
21
+ */
22
+ interface CameraPort {
23
+ readonly isAvailable: boolean;
24
+ capture(opts: CaptureOptions): Promise<MediaSource>;
25
+ }
26
+ /** The safe absent default: capture is unavailable and never throws until explicitly awaited. */
27
+ declare const unavailableCamera: CameraPort;
28
+
29
+ export { type CameraPort as C, type MediaSource as M, type CaptureOptions as a, unavailableCamera as u };
@@ -0,0 +1,29 @@
1
+ /** Opaque, host-defined capture request. Kept minimal at the spine; video/photo specs extend it. */
2
+ interface CaptureOptions {
3
+ /** 'photo' | 'video'; the coarse capture mode a capability requests. */
4
+ readonly mode: 'photo' | 'video';
5
+ /** Optional soft ceiling for a video capture, in seconds. */
6
+ readonly maxDurationSeconds?: number;
7
+ }
8
+ /** Opaque handle to captured media the FilePort can upload. Server-owned shape is defined by video/photo. */
9
+ interface MediaSource {
10
+ /** Local file URI produced by the capture. */
11
+ readonly uri: string;
12
+ /** MIME type, e.g. 'image/jpeg' | 'video/mp4'. */
13
+ readonly mimeType: string;
14
+ readonly widthPx?: number;
15
+ readonly heightPx?: number;
16
+ readonly durationSeconds?: number;
17
+ }
18
+ /**
19
+ * Camera capture. Absent (or an unavailable native peer) => `isAvailable:false`; the SDK never throws for a
20
+ * missing capability, it renders the OffState.
21
+ */
22
+ interface CameraPort {
23
+ readonly isAvailable: boolean;
24
+ capture(opts: CaptureOptions): Promise<MediaSource>;
25
+ }
26
+ /** The safe absent default: capture is unavailable and never throws until explicitly awaited. */
27
+ declare const unavailableCamera: CameraPort;
28
+
29
+ export { type CameraPort as C, type MediaSource as M, type CaptureOptions as a, unavailableCamera as u };
@@ -0,0 +1,146 @@
1
+ import { R as RequestFunnel } from './requestFunnel-dio5OmR9.js';
2
+ import { A as AuthContext } from './identity-DK9zORrG.js';
3
+ import { a as EverfurErrorCode, f as EverfurError, E as EverfurResult } from './EverfurResult-D92-uL82.js';
4
+ import { b as ResponseId, a as ConversationId, P as PetRef } from './ids-CJ1S6adf.js';
5
+ import { F as Frame } from './config-CiJ0PVBB.js';
6
+ import { T as TelemetryPort } from './TelemetryPort-BDNr00hu.js';
7
+
8
+ /**
9
+ * Recovery policy. A host may narrow retry ceilings (e.g. a metered plan) but can never widen the SDK's
10
+ * settle-don't-throw contract. `attempt` is 0-based (the first try is attempt 0).
11
+ */
12
+ interface ErrorPolicyPort {
13
+ /** Whether to retry `code` on this attempt. Default: `isAutoRetryable(code) && attempt < maxAttempts(code)`. */
14
+ shouldRetry(code: EverfurErrorCode, attempt: number): boolean;
15
+ /** Total attempts (the initial try plus retries) before the terminal error surfaces. Default 4. */
16
+ maxAttempts(code: EverfurErrorCode): number;
17
+ /** Observation hook for a handled, product-expected failure. Never rendered, never thrown. */
18
+ onExpectedFailure?(error: EverfurError): void;
19
+ }
20
+ /**
21
+ * The shipped default policy (applied when EverfurConfig omits `errorPolicy`). Pure and total.
22
+ * `authExpired` is deliberately NOT auto-retried here: it is a token refresh + single replay handled by the
23
+ * controller / authedSend, not a backoff retry (SPEC-00 §3.2 / §4.3).
24
+ */
25
+ declare const defaultErrorPolicy: ErrorPolicyPort;
26
+
27
+ /** Injectable timers so the watchdog + deadlines are testable with a fake clock (no direct setTimeout). */
28
+ interface Timers {
29
+ setTimeout(handler: () => void, ms: number): unknown;
30
+ clearTimeout(handle: unknown): void;
31
+ now(): number;
32
+ }
33
+
34
+ type ChatStatus = 'idle' | 'streaming' | 'complete' | 'error';
35
+ /** Opaque citation record (DoneFrame.citations shape). Rendered by the UI layer, never trusted for logic. */
36
+ type Citation = Readonly<Record<string, unknown>>;
37
+ interface ChatMessage {
38
+ /** Stable list key: a client id for a user turn, the responseId for a finalized assistant turn. */
39
+ readonly id: string;
40
+ readonly role: 'user' | 'assistant';
41
+ readonly text: string;
42
+ readonly citations?: readonly Citation[];
43
+ readonly responseId?: ResponseId;
44
+ /** Optimistic user bubble; removed if the turn fails BEFORE the first delta. */
45
+ readonly pending?: boolean;
46
+ }
47
+ interface ChatSnapshot {
48
+ readonly status: ChatStatus;
49
+ /** [] => Empty state; length > 0 => Populated. */
50
+ readonly messages: readonly ChatMessage[];
51
+ /** nulled on a 404 before recreate. */
52
+ readonly conversationId: ConversationId | null;
53
+ /** present iff status === 'error'; NEVER thrown. */
54
+ readonly error: EverfurError | null;
55
+ readonly isStreaming: boolean;
56
+ /** true during the initial conversation prime => Loading. */
57
+ readonly isBootstrapping: boolean;
58
+ /**
59
+ * Flattened, de-duplicated suggested-prompt strings from the prompts bootstrap (backend-owned copy; empty
60
+ * until the bootstrap resolves, empty when the backend returns none). The UI renders these as the tappable
61
+ * "Follow up questions" list; each string is a ready-to-send message.
62
+ */
63
+ readonly suggestedPrompts: readonly string[];
64
+ }
65
+ interface SendOptions {
66
+ readonly conversationId?: ConversationId;
67
+ readonly petRef?: PetRef;
68
+ readonly signal?: AbortSignal;
69
+ readonly idempotencyKey?: string;
70
+ }
71
+ interface ChatHandlers {
72
+ onFrame(frame: Frame): void;
73
+ onDone(responseId: ResponseId | null): void;
74
+ onError(error: EverfurError): void;
75
+ }
76
+ /** A suggested-prompt category returned by the prompts bootstrap endpoint. */
77
+ interface SuggestedPromptCategory {
78
+ readonly category: string;
79
+ readonly prompts: readonly string[];
80
+ }
81
+ interface ChatControllerDeps {
82
+ /** Device auth context (bearer-XOR-pk precedence + lazy token). Carries the TransportPort. */
83
+ readonly auth: AuthContext;
84
+ readonly telemetry?: TelemetryPort;
85
+ readonly errorPolicy?: ErrorPolicyPort;
86
+ readonly timers?: Timers;
87
+ /** Injected RNG for deterministic backoff in tests. */
88
+ readonly rng?: () => number;
89
+ /** Injected id factory (client message ids) for deterministic tests. */
90
+ readonly newId?: () => string;
91
+ /** Injected idempotency-key factory for the TURN identity (see `buildSendRequest`). */
92
+ readonly makeIdempotencyKey?: () => string;
93
+ /** The ONE request funnel. Built from `auth` when absent; injected in tests for a deterministic clock. */
94
+ readonly funnel?: RequestFunnel;
95
+ readonly budget?: {
96
+ readonly idleMs?: number;
97
+ readonly overallMs?: number;
98
+ };
99
+ readonly initialConversationId?: ConversationId | null;
100
+ }
101
+ interface ChatController {
102
+ /** useSyncExternalStore subscribe. Returns an unsubscribe. */
103
+ subscribe(listener: () => void): () => void;
104
+ /** A stable frozen snapshot; identity changes only on real change. */
105
+ getSnapshot(): ChatSnapshot;
106
+ /** Stream a reply. The iterable ALWAYS completes cleanly; a failure is a terminal Frame, never a throw. */
107
+ send(text: string, opts?: SendOptions): AsyncIterable<Frame>;
108
+ /** Imperative escape hatch; returns a teardown, also torn down on dispose(). */
109
+ send(text: string, opts: SendOptions, handlers: ChatHandlers): () => void;
110
+ /** Re-send the last turn without a duplicate user bubble, reusing its idempotency key. */
111
+ retryLast(): AsyncIterable<Frame>;
112
+ /** Abort in-flight; not an error. Returns to pre-send (idle) and drops a never-answered optimistic bubble. */
113
+ stop(): void;
114
+ /** Idempotent. Aborts in-flight, bumps the epoch, drops listeners + handler subscriptions. */
115
+ dispose(): void;
116
+ /** Re-scope on user/pet switch: aborts in-flight and bumps the epoch so a late write drops. */
117
+ rescope(): void;
118
+ /** Bootstrap helper (§8): create a conversation. Unary, authed, settles. */
119
+ createConversation(opts?: {
120
+ readonly petRef?: PetRef;
121
+ }): Promise<EverfurResult<ConversationId>>;
122
+ /** Bootstrap helper (§8): fetch the session's suggested prompts, optionally personalized to a pet. Settles; never throws. */
123
+ getPrompts(opts?: {
124
+ readonly petRef?: PetRef;
125
+ }): Promise<EverfurResult<readonly SuggestedPromptCategory[]>>;
126
+ /** Adopt (or clear) the active conversation id, e.g. after bootstrap primes one. */
127
+ adoptConversation(id: ConversationId | null): void;
128
+ /**
129
+ * Adopt the suggested-prompt categories fetched by the bootstrap (§8): flattened + de-duplicated onto the
130
+ * snapshot's `suggestedPrompts` so the prebuilt UI can render the tappable follow-up list. Idempotent; a
131
+ * no-op when the flattened set is empty and none are shown.
132
+ */
133
+ adoptPrompts(categories: readonly SuggestedPromptCategory[]): void;
134
+ }
135
+ declare function createChatController(deps: ChatControllerDeps): ChatController;
136
+ /**
137
+ * The server's own limits on a chat message, mirrored here so a violation is caught before any UI is
138
+ * rendered for it. `SendWidgetMessageRequest.message` on EFBackend main declares
139
+ * `min_length=1, max_length=4000`; the server remains authoritative and still enforces both.
140
+ *
141
+ * These are NOT in the generated contract projection, so they are pinned here with their source rather
142
+ * than derived. If the server relaxes either bound, this is the one place to update.
143
+ */
144
+ declare const MESSAGE_MAX_CHARS = 4000;
145
+
146
+ export { type ChatController as C, type ErrorPolicyPort as E, MESSAGE_MAX_CHARS as M, type SuggestedPromptCategory as S, type Timers as T, type ChatControllerDeps as a, type ChatHandlers as b, type ChatMessage as c, type ChatSnapshot as d, type ChatStatus as e, type Citation as f, type SendOptions as g, createChatController as h, defaultErrorPolicy as i };
@@ -0,0 +1,146 @@
1
+ import { R as RequestFunnel } from './requestFunnel-DuUH-kAe.cjs';
2
+ import { A as AuthContext } from './identity-Brl-lDd6.cjs';
3
+ import { a as EverfurErrorCode, f as EverfurError, E as EverfurResult } from './EverfurResult-D92-uL82.cjs';
4
+ import { b as ResponseId, a as ConversationId, P as PetRef } from './ids-CJ1S6adf.cjs';
5
+ import { F as Frame } from './config-BSjBdxrZ.cjs';
6
+ import { T as TelemetryPort } from './TelemetryPort-BDNr00hu.cjs';
7
+
8
+ /**
9
+ * Recovery policy. A host may narrow retry ceilings (e.g. a metered plan) but can never widen the SDK's
10
+ * settle-don't-throw contract. `attempt` is 0-based (the first try is attempt 0).
11
+ */
12
+ interface ErrorPolicyPort {
13
+ /** Whether to retry `code` on this attempt. Default: `isAutoRetryable(code) && attempt < maxAttempts(code)`. */
14
+ shouldRetry(code: EverfurErrorCode, attempt: number): boolean;
15
+ /** Total attempts (the initial try plus retries) before the terminal error surfaces. Default 4. */
16
+ maxAttempts(code: EverfurErrorCode): number;
17
+ /** Observation hook for a handled, product-expected failure. Never rendered, never thrown. */
18
+ onExpectedFailure?(error: EverfurError): void;
19
+ }
20
+ /**
21
+ * The shipped default policy (applied when EverfurConfig omits `errorPolicy`). Pure and total.
22
+ * `authExpired` is deliberately NOT auto-retried here: it is a token refresh + single replay handled by the
23
+ * controller / authedSend, not a backoff retry (SPEC-00 §3.2 / §4.3).
24
+ */
25
+ declare const defaultErrorPolicy: ErrorPolicyPort;
26
+
27
+ /** Injectable timers so the watchdog + deadlines are testable with a fake clock (no direct setTimeout). */
28
+ interface Timers {
29
+ setTimeout(handler: () => void, ms: number): unknown;
30
+ clearTimeout(handle: unknown): void;
31
+ now(): number;
32
+ }
33
+
34
+ type ChatStatus = 'idle' | 'streaming' | 'complete' | 'error';
35
+ /** Opaque citation record (DoneFrame.citations shape). Rendered by the UI layer, never trusted for logic. */
36
+ type Citation = Readonly<Record<string, unknown>>;
37
+ interface ChatMessage {
38
+ /** Stable list key: a client id for a user turn, the responseId for a finalized assistant turn. */
39
+ readonly id: string;
40
+ readonly role: 'user' | 'assistant';
41
+ readonly text: string;
42
+ readonly citations?: readonly Citation[];
43
+ readonly responseId?: ResponseId;
44
+ /** Optimistic user bubble; removed if the turn fails BEFORE the first delta. */
45
+ readonly pending?: boolean;
46
+ }
47
+ interface ChatSnapshot {
48
+ readonly status: ChatStatus;
49
+ /** [] => Empty state; length > 0 => Populated. */
50
+ readonly messages: readonly ChatMessage[];
51
+ /** nulled on a 404 before recreate. */
52
+ readonly conversationId: ConversationId | null;
53
+ /** present iff status === 'error'; NEVER thrown. */
54
+ readonly error: EverfurError | null;
55
+ readonly isStreaming: boolean;
56
+ /** true during the initial conversation prime => Loading. */
57
+ readonly isBootstrapping: boolean;
58
+ /**
59
+ * Flattened, de-duplicated suggested-prompt strings from the prompts bootstrap (backend-owned copy; empty
60
+ * until the bootstrap resolves, empty when the backend returns none). The UI renders these as the tappable
61
+ * "Follow up questions" list; each string is a ready-to-send message.
62
+ */
63
+ readonly suggestedPrompts: readonly string[];
64
+ }
65
+ interface SendOptions {
66
+ readonly conversationId?: ConversationId;
67
+ readonly petRef?: PetRef;
68
+ readonly signal?: AbortSignal;
69
+ readonly idempotencyKey?: string;
70
+ }
71
+ interface ChatHandlers {
72
+ onFrame(frame: Frame): void;
73
+ onDone(responseId: ResponseId | null): void;
74
+ onError(error: EverfurError): void;
75
+ }
76
+ /** A suggested-prompt category returned by the prompts bootstrap endpoint. */
77
+ interface SuggestedPromptCategory {
78
+ readonly category: string;
79
+ readonly prompts: readonly string[];
80
+ }
81
+ interface ChatControllerDeps {
82
+ /** Device auth context (bearer-XOR-pk precedence + lazy token). Carries the TransportPort. */
83
+ readonly auth: AuthContext;
84
+ readonly telemetry?: TelemetryPort;
85
+ readonly errorPolicy?: ErrorPolicyPort;
86
+ readonly timers?: Timers;
87
+ /** Injected RNG for deterministic backoff in tests. */
88
+ readonly rng?: () => number;
89
+ /** Injected id factory (client message ids) for deterministic tests. */
90
+ readonly newId?: () => string;
91
+ /** Injected idempotency-key factory for the TURN identity (see `buildSendRequest`). */
92
+ readonly makeIdempotencyKey?: () => string;
93
+ /** The ONE request funnel. Built from `auth` when absent; injected in tests for a deterministic clock. */
94
+ readonly funnel?: RequestFunnel;
95
+ readonly budget?: {
96
+ readonly idleMs?: number;
97
+ readonly overallMs?: number;
98
+ };
99
+ readonly initialConversationId?: ConversationId | null;
100
+ }
101
+ interface ChatController {
102
+ /** useSyncExternalStore subscribe. Returns an unsubscribe. */
103
+ subscribe(listener: () => void): () => void;
104
+ /** A stable frozen snapshot; identity changes only on real change. */
105
+ getSnapshot(): ChatSnapshot;
106
+ /** Stream a reply. The iterable ALWAYS completes cleanly; a failure is a terminal Frame, never a throw. */
107
+ send(text: string, opts?: SendOptions): AsyncIterable<Frame>;
108
+ /** Imperative escape hatch; returns a teardown, also torn down on dispose(). */
109
+ send(text: string, opts: SendOptions, handlers: ChatHandlers): () => void;
110
+ /** Re-send the last turn without a duplicate user bubble, reusing its idempotency key. */
111
+ retryLast(): AsyncIterable<Frame>;
112
+ /** Abort in-flight; not an error. Returns to pre-send (idle) and drops a never-answered optimistic bubble. */
113
+ stop(): void;
114
+ /** Idempotent. Aborts in-flight, bumps the epoch, drops listeners + handler subscriptions. */
115
+ dispose(): void;
116
+ /** Re-scope on user/pet switch: aborts in-flight and bumps the epoch so a late write drops. */
117
+ rescope(): void;
118
+ /** Bootstrap helper (§8): create a conversation. Unary, authed, settles. */
119
+ createConversation(opts?: {
120
+ readonly petRef?: PetRef;
121
+ }): Promise<EverfurResult<ConversationId>>;
122
+ /** Bootstrap helper (§8): fetch the session's suggested prompts, optionally personalized to a pet. Settles; never throws. */
123
+ getPrompts(opts?: {
124
+ readonly petRef?: PetRef;
125
+ }): Promise<EverfurResult<readonly SuggestedPromptCategory[]>>;
126
+ /** Adopt (or clear) the active conversation id, e.g. after bootstrap primes one. */
127
+ adoptConversation(id: ConversationId | null): void;
128
+ /**
129
+ * Adopt the suggested-prompt categories fetched by the bootstrap (§8): flattened + de-duplicated onto the
130
+ * snapshot's `suggestedPrompts` so the prebuilt UI can render the tappable follow-up list. Idempotent; a
131
+ * no-op when the flattened set is empty and none are shown.
132
+ */
133
+ adoptPrompts(categories: readonly SuggestedPromptCategory[]): void;
134
+ }
135
+ declare function createChatController(deps: ChatControllerDeps): ChatController;
136
+ /**
137
+ * The server's own limits on a chat message, mirrored here so a violation is caught before any UI is
138
+ * rendered for it. `SendWidgetMessageRequest.message` on EFBackend main declares
139
+ * `min_length=1, max_length=4000`; the server remains authoritative and still enforces both.
140
+ *
141
+ * These are NOT in the generated contract projection, so they are pinned here with their source rather
142
+ * than derived. If the server relaxes either bound, this is the one place to update.
143
+ */
144
+ declare const MESSAGE_MAX_CHARS = 4000;
145
+
146
+ export { type ChatController as C, type ErrorPolicyPort as E, MESSAGE_MAX_CHARS as M, type SuggestedPromptCategory as S, type Timers as T, type ChatControllerDeps as a, type ChatHandlers as b, type ChatMessage as c, type ChatSnapshot as d, type ChatStatus as e, type Citation as f, type SendOptions as g, createChatController as h, defaultErrorPolicy as i };