farvex 0.1.0 → 0.2.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # farvex
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **BREAKING**: Replaced Offer with Invite. `OfferManager` → `InviteManager`, `Offer` → `Invite`, `useIncomingOffer` → `useIncomingInvite`, pulse topics `session.offer.*` → `session.invite.*`, endpoints `/sessions/offers/{offerId}/*` → `/sessions/{sessionId}/invites/{inviteId}/*`. No deprecation shims — all offer-era symbols are removed.
8
+ - **BREAKING**: `Session.telephonyLegs` is now derived from `participant.operations[]` where `kind === "sip_leg"`. The backend no longer returns a top-level `telephonyLegs[]`.
9
+ - **BREAKING**: Participant lifecycle states changed. Removed `ringing`, `joining`. Added `accepted`, `declined`, `withdrawn`. New `SessionEventMap` keys: `participant.accepted`, `participant.declined`, `participant.withdrawn`.
10
+ - **BREAKING**: `isSelf` is now keyed by `participantId` instead of `participantIdentity`. `SessionManagerOptions.selfIdentity` renamed to `selfParticipantId`. The join grant now carries `participantId`; `Session.join()` uses it.
11
+ - **BREAKING**: `useCallControls` controls now expose `enabled: boolean` on each slot and gate actions by the current participant's `capabilities[]`. `leave`/`end` return `{enabled, run}` instead of a bare function. Disabled when capability is missing; never hidden.
12
+ - Added `Dispatch` surface (ring-group) with `session.dispatches`, `useSessionDispatches`, and events: `dispatch.started`, `dispatch.resolved`, `dispatch.cancelled`, `dispatch.expired`.
13
+ - Added `SessionRouteContext` accessible as `session.route`.
14
+ - Added `useSessionCapabilities(sessionId?)` returning a capability `Set` for self.
15
+ - Added `session.capabilitiesOf(participantId?)`.
16
+ - Added `InviteManager.dropBySession` triggered on `session.remove` pulse messages so invites tied to ended sessions are reaped without waiting for per-invite removes.
17
+ - Tests now live beside source (`foo.test.ts` next to `foo.ts`).
18
+ - Regenerated API types against the rewritten session module (JSON-aggregate backend).
19
+ - Added `./livekit` and `./mock` subpath exports.
20
+ - Added `Session.mediaRoom` getter exposing the underlying `livekit-client` `Room`, so the React provider can share it with the LiveKit context.
21
+ - Fixed: `CallpadProvider` now reuses the SDK's internal `Room` via `<RoomContext.Provider>` instead of instantiating a second `Room` through `<LiveKitRoom>`. Eliminates duplicate-identity races; mute state reflects the `Room` that actually publishes audio.
22
+ - Fixed: `SessionEffectsExecutor` DI binding was missing from `sessionModule`; webhook jobs failed silently with `No bindings found for service: "SessionEffectsExecutor"`.
23
+ - Fixed: inbound SIP caller participant is now created with `initialState: "joined"` (was `"accepted"`). Normal hangups now emit `session.participant.left` (reason `left`) instead of `session.participant.failed` (reason `unreachable`).
24
+
3
25
  ## 0.1.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  Web SDK for Callpad sessions — a thin, React-friendly wrapper over LiveKit and the Callpad Pulse realtime layer for managing WebRTC and SIP sessions.
4
4
 
5
- > Scaffold state. No SDK surface is exposed yet beyond placeholder exports that prove the build pipeline. The first published version will be `0.1.0`; the real SDK surface lands in the next iteration.
6
-
7
5
  ## Install
8
6
 
9
7
  ```sh
@@ -22,30 +20,123 @@ pnpm add react react-dom
22
20
 
23
21
  ## Subpath exports
24
22
 
25
- | Entry | Use when |
26
- | -------------- | -------------------------------------------------------------- |
27
- | `farvex` | Curated top-level exports (ships placeholder during scaffold). |
28
- | `farvex/core` | Framework-agnostic client (browser-safe at module load). |
29
- | `farvex/react` | React hooks and providers. Carries `"use client"` for Next.js. |
30
-
31
- ```ts
32
- import { version } from "farvex";
33
- import { corePlaceholder } from "farvex/core";
34
- import { reactPlaceholder } from "farvex/react";
23
+ | Entry | Use when |
24
+ | ---------------- | -------------------------------------------------------------- |
25
+ | `farvex` | Curated top-level exports. |
26
+ | `farvex/core` | Framework-agnostic client (browser-safe at module load). |
27
+ | `farvex/react` | React hooks and providers. Carries `"use client"` for Next.js. |
28
+ | `farvex/livekit` | Re-exports livekit-client + @livekit/components-react types. |
29
+ | `farvex/mock` | In-memory mock client for tests and storybook. |
30
+
31
+ ## Quickstart (React)
32
+
33
+ ```tsx
34
+ import {
35
+ CallpadProvider,
36
+ useIncomingInvite,
37
+ useCallControls,
38
+ } from "farvex/react";
39
+
40
+ export default function App() {
41
+ return (
42
+ <CallpadProvider
43
+ config={{
44
+ apiBaseUrl: "https://api.callpad.example.com",
45
+ vendor: "acme",
46
+ auth: { getAccessToken: async () => myToken() },
47
+ }}
48
+ >
49
+ <CallScreen />
50
+ </CallpadProvider>
51
+ );
52
+ }
53
+
54
+ function CallScreen() {
55
+ const invite = useIncomingInvite();
56
+ const controls = useCallControls();
57
+
58
+ if (invite) {
59
+ return (
60
+ <div>
61
+ <p>Incoming invite from {invite.inviterParticipantId ?? "unknown"}</p>
62
+ <button onClick={() => invite.accept()}>Accept</button>
63
+ <button onClick={() => invite.reject()}>Decline</button>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <div>
70
+ <button disabled={!controls.hold.enabled} onClick={controls.hold.toggle}>
71
+ {controls.hold.active ? "Resume" : "Hold"}
72
+ </button>
73
+ <button
74
+ disabled={!controls.recording.enabled}
75
+ onClick={controls.recording.toggle}
76
+ >
77
+ {controls.recording.active ? "Stop recording" : "Record"}
78
+ </button>
79
+ <button disabled={!controls.end.enabled} onClick={controls.end.run}>
80
+ End
81
+ </button>
82
+ </div>
83
+ );
84
+ }
35
85
  ```
36
86
 
37
- Both ESM and CJS builds ship. Types resolve correctly for bundlers, Node 16+ ESM, and Node 16+ CJS.
38
-
39
- ## Next.js
40
-
41
- - Import from `farvex/react` directly inside client components the build preserves the `"use client"` directive, so no transpile hack is needed.
42
- - Import from `farvex/core` from server components (no DOM access at module load).
43
- - No `next.config.js` `transpilePackages` changes required.
44
-
45
- ## Vite
46
-
47
- - Works out of the box. Vite consumes the ESM output.
48
- - `react` and `react-dom` are `external` at build time; your app supplies them.
87
+ Controls are capability-gated: the backend sends a per-participant `capabilities[]` list, and hooks return `enabled: false` when the required capability is missing. Render them disabled rather than hidden.
88
+
89
+ ## Core concepts
90
+
91
+ - **Session** one call. Has `participants`, `dispatches`, `recordings`, `route`. State is derived: `ringing | active | on_hold | ended`. Telephony legs (SIP) live inside each participant's `operations[]`.
92
+ - **Invite** a pending app invite sent to a specific recipient (agent or user). The recipient accepts via `POST /api/v1/sessions/{sessionId}/invites/{inviteId}/accept`.
93
+ - **Dispatch** — a ring-group: one or more agents rung in parallel or sequentially. When one answers, the dispatch `resolves` and the siblings transition to `withdrawn`.
94
+ - **Capability** — per-participant permission: `hold_session`, `end_session`, `start_recording`, etc. Use `session.capabilitiesOf()` to gate UI.
95
+
96
+ ## React hooks
97
+
98
+ | Hook | Purpose |
99
+ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
100
+ | `useCallpadClient()` | Access underlying client + connection status. |
101
+ | `useSessions()` / `useSession(id?)` | All sessions / active session or by id. |
102
+ | `useIncomingInvite()` / `useIncomingInvites()` | First pending invite / all pending invites for the current user. |
103
+ | `useSessionParticipants(id?)` / `useSessionParticipant(pid, id?)` / `useSelfParticipant(id?)` | Participants in a session. |
104
+ | `useSessionCapabilities(id?)` | Capability `Set` for self in the active session. |
105
+ | `useSessionDispatches(id?)` | Active ring-group dispatches in a session. |
106
+ | `useCallControls(id?)` | Capability-gated mic / camera / screen / hold / recording / leave / end. |
107
+ | `useCallDuration(id?)` | `{formatted, elapsedSeconds}` since session activated. |
108
+ | `useIsHeld(id?)` | Whether the session is on hold. |
109
+ | `useActiveRecording(id?)` | Current in-progress recording (if any). |
110
+ | `useSessionEvent(event, handler, id?)` | Subscribe to a session event with auto cleanup. |
111
+ | `useClientEvent(event, handler)` | Subscribe to a client event with auto cleanup. |
112
+ | `usePresence(refs)` / `useMyPresence()` | Presence query + my presence control. |
113
+
114
+ ## Realtime events
115
+
116
+ Pulse pushes these topics (SDK forwards them on `PulseTransport`):
117
+
118
+ | Topic | Payload |
119
+ | ----------------------- | ------------------------------------------------------------------------ |
120
+ | `session.upsert` | Full `SessionView`. |
121
+ | `session.remove` | `{sessionId, version}`. Also drops any pending invites for that session. |
122
+ | `session.invite.upsert` | `SessionInviteView` — add/update a pending invite for the current user. |
123
+ | `session.invite.remove` | `{inviteId, sessionId, participantId, state}`. |
124
+
125
+ ## HTTP endpoints used
126
+
127
+ All under `/api/v1`:
128
+
129
+ - `GET /sessions` — prime the session list.
130
+ - `POST /sessions` — create a session.
131
+ - `GET /sessions/{sessionId}` — fetch by id.
132
+ - `PATCH /sessions/{sessionId}` — update name.
133
+ - `POST /sessions/{sessionId}/join` — get a LiveKit token grant.
134
+ - `POST /sessions/{sessionId}/hold` / `.../unhold` / `.../cancel` / `.../leave` / `.../end`
135
+ - `POST /sessions/{sessionId}/participants` — invite a target (agent/user/contact/phone).
136
+ - `DELETE /sessions/{sessionId}/participants/{participantId}` — remove a participant.
137
+ - `POST /sessions/{sessionId}/recordings` / `.../recordings/{recordingId}/stop` — recording control.
138
+ - `GET /sessions/invites` — list pending invites for the current user.
139
+ - `POST /sessions/{sessionId}/invites/{inviteId}/accept` / `.../reject` — act on an invite.
49
140
 
50
141
  ## Local development
51
142
 
@@ -54,11 +145,18 @@ From `websdk/`:
54
145
  ```sh
55
146
  bun install
56
147
  bun run dev # tsup watch mode; rebuilds dist/ on change
57
- bun run test # vitest smoke tests
148
+ bun run test # vitest; includes unit tests next to source files
58
149
  bun run check # lint + format + typecheck
59
150
  bun run build # one-off production build
60
151
  ```
61
152
 
153
+ API types are generated from the backend's OpenAPI spec:
154
+
155
+ ```sh
156
+ # start the backend first: `bun run docker:up && docker compose up callpad.http`
157
+ bun run generate:api # writes src/api/generated/
158
+ ```
159
+
62
160
  ### Linking into a consumer app (goagentfe, customer app, etc.)
63
161
 
64
162
  We recommend [`yalc`](https://github.com/wclr/yalc) over `pnpm link` / `bun link` because it survives Vite and Next.js module caches.
@@ -85,7 +183,7 @@ Each change in `websdk/`:
85
183
  npx yalc push # push built dist/ to subscribers
86
184
  ```
87
185
 
88
- Before committing in the consumer app, undo the local link so `package.json` doesn't point at `file:.yalc/farvex`:
186
+ Before committing in the consumer app, undo the local link:
89
187
 
90
188
  ```sh
91
189
  npx yalc remove --all
@@ -94,36 +192,22 @@ pnpm install
94
192
 
95
193
  ## Release flow
96
194
 
97
- See `sdk-scaffold.md` at the repo root for the full plan. Short version (manual, no CI yet):
98
-
99
195
  ```sh
100
- # while working on a change (from repo root or websdk/):
101
- bun run changeset
102
-
103
- # when ready to publish (from websdk/), on clean main:
196
+ bun run changeset # while working on a change
104
197
  bun run version # applies pending changesets, writes CHANGELOG.md
105
198
  git commit -am "chore: release farvex@x.y.z"
106
199
  bun run release # builds, runs changeset publish
107
200
  git push --follow-tags
108
201
  ```
109
202
 
110
- Before the first real publish:
111
-
112
- ```sh
113
- npm login # as the publisher account
114
- npm view farvex # must return 404
115
- bun run build
116
- bunx publint
117
- bun pm pack # inspect the tarball contents
118
- ```
119
-
120
203
  Pre-1.0: every feature ships as a `minor` bump; breaking changes allowed without a major.
121
204
 
122
205
  ## Package conventions
123
206
 
124
207
  - Side-effect free — `"sideEffects": false` in `package.json`. Tree-shakes cleanly.
125
208
  - No top-level access to `window`, `document`, `navigator`, `RTCPeerConnection`, or `localStorage`. Any such access happens inside methods or effects.
126
- - `typescript/no-explicit-any` is enforced by oxlint; we avoid `unknown` in exported types by convention.
209
+ - No `any` or `unknown` in exported types.
210
+ - Tests live next to source (`foo.test.ts` beside `foo.ts`).
127
211
  - `engines.node >= 18`.
128
212
 
129
213
  ## Layout
@@ -131,14 +215,16 @@ Pre-1.0: every feature ships as a `minor` bump; breaking changes allowed without
131
215
  ```
132
216
  websdk/
133
217
  src/
134
- index.ts public entry (curated re-exports later)
135
- core/index.ts framework-agnostic client
136
- react/index.ts React hooks + providers
137
- test/ vitest + jsdom smoke tests
218
+ index.ts public entry
219
+ core/ framework-agnostic client
220
+ session/ Session class + commands
221
+ managers/ SessionManager, InviteManager, PresenceManager
222
+ transport/ PulseTransport, MediaRoomAdapter
223
+ react/ React hooks + providers
224
+ mock/ in-memory mock client + scenarios
225
+ test/ setup + cross-cutting smoke tests
138
226
  dist/ build output (gitignored)
139
227
  tsup.config.ts build config (ESM + CJS + dts, use-client preserved)
140
228
  tsconfig.json strict TS, bundler resolution
141
- vitest.config.ts jsdom test env
142
- .oxlintrc.json lint rules (matches apps/main)
143
- .oxfmtrc.json format rules (matches apps/main)
229
+ vitest.config.ts jsdom test env (includes src/**/*.test.ts)
144
230
  ```