farvex 0.1.0 → 1.0.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,38 @@
1
1
  # farvex
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Prepare the SDK for a stable release with expanded customer and vendor integration documentation.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - **BREAKING**: Session APIs are now vendor-scoped. The SDK uses `/api/v1/vendors/{slug}/sessions...` and no longer calls `/api/v1/sessions`.
14
+ - **BREAKING**: `CallpadConfig` now requires `audience: "agent" | "customer"`. Agent clients fetch vendor Pulse grants; customer clients fetch global user Pulse grants.
15
+ - **BREAKING**: Removed root `client.createSession`. Use `client.sessions.create(...)` for agent-originated sessions and `client.sessions.request(...)` for customer-originated sessions.
16
+ - Added customer session request flow with default auto-join and `useSessionRequest()`.
17
+ - Added `session.updated` client event and React hook invalidation for session upserts.
18
+ - **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.
19
+ - **BREAKING**: `Session.telephonyLegs` is now derived from `participant.operations[]` where `kind === "sip_leg"`. The backend no longer returns a top-level `telephonyLegs[]`.
20
+ - **BREAKING**: Participant lifecycle states changed. Removed `ringing`, `joining`. Added `accepted`, `declined`, `withdrawn`. New `SessionEventMap` keys: `participant.accepted`, `participant.declined`, `participant.withdrawn`.
21
+ - **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.
22
+ - **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.
23
+ - Added `Dispatch` surface for team fan-out with `session.dispatches` and events: `dispatch.started`, `dispatch.resolved`, `dispatch.cancelled`.
24
+ - Added `SessionRouteContext` accessible as `session.route`.
25
+ - Added `useSessionCapabilities(sessionId?)` returning a capability `Set` for self.
26
+ - Added `session.capabilitiesOf(participantId?)`.
27
+ - Added `InviteManager.dropBySession` triggered on `session.remove` pulse messages so invites tied to ended sessions are reaped without waiting for per-invite removes.
28
+ - Tests now live beside source (`foo.test.ts` next to `foo.ts`).
29
+ - Regenerated API types against the rewritten session module (JSON-aggregate backend).
30
+ - Added `./livekit` and `./mock` subpath exports.
31
+ - Added `Session.mediaRoom` getter exposing the underlying `livekit-client` `Room`, so the React provider can share it with the LiveKit context.
32
+ - 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.
33
+ - Fixed: `SessionEffectsExecutor` DI binding was missing from `sessionModule`; webhook jobs failed silently with `No bindings found for service: "SessionEffectsExecutor"`.
34
+ - 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`).
35
+
3
36
  ## 0.1.0
4
37
 
5
38
  ### Minor Changes
package/README.md CHANGED
@@ -1,144 +1,347 @@
1
1
  # farvex
2
2
 
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.
3
+ Web SDK for Callpad voice sessions. It wraps the generated session HTTP client,
4
+ Centrifugo realtime updates, and LiveKit re-exports. Calls history, notes,
5
+ wrap-up, billing, metrics, and recording playback should use the normal REST
6
+ API clients.
4
7
 
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.
8
+ ## Exports
9
+
10
+ | Entry | Purpose |
11
+ | ---------------- | ------------------------------------------- |
12
+ | `farvex` | Session client factories and SDK/API types. |
13
+ | `farvex/react` | React provider and focused session hooks. |
14
+ | `farvex/livekit` | LiveKit React/client re-exports. |
6
15
 
7
16
  ## Install
8
17
 
9
18
  ```sh
10
- npm i farvex
11
- # or
12
- pnpm add farvex
13
- # or
14
- bun add farvex
19
+ npm install farvex livekit-client @livekit/components-react
15
20
  ```
16
21
 
17
- React and react-dom are optional peer dependencies (only needed when you import from `farvex/react`):
22
+ `livekit-client`, `@livekit/components-react`, `react`, and `react-dom` are
23
+ peer dependencies. Install the LiveKit packages only in apps that render call
24
+ media.
18
25
 
19
- ```sh
20
- pnpm add react react-dom
21
- ```
26
+ ## Setup
22
27
 
23
- ## Subpath exports
28
+ The SDK needs these values from the app:
24
29
 
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
+ | Value | Description |
31
+ | -------- | ----------------------------------------------- |
32
+ | `apiUrl` | Base URL for the Callpad API. |
33
+ | `userId` | Callpad user ID for the signed-in app user. |
34
+ | `auth` | Function that returns a bearer token on demand. |
35
+ | `vendor` | Vendor slug. Required by host clients. |
36
+
37
+ `getAccessToken` is called before API requests and when realtime auth needs a
38
+ fresh token. Keep token minting in your app backend or existing auth layer; the
39
+ browser client should only receive a user-scoped token. The examples below
40
+ assume `getAccessToken` already exists in the app.
30
41
 
31
42
  ```ts
32
- import { version } from "farvex";
33
- import { corePlaceholder } from "farvex/core";
34
- import { reactPlaceholder } from "farvex/react";
43
+ import { createSessionClient } from "farvex";
44
+
45
+ const callpad = createSessionClient({
46
+ apiUrl: "https://api.callpad.example.com",
47
+ userId: currentUser.id,
48
+ auth: {
49
+ getAccessToken,
50
+ onUnauthorized: () => {
51
+ auth.signOut();
52
+ },
53
+ },
54
+ });
35
55
  ```
36
56
 
37
- Both ESM and CJS builds ship. Types resolve correctly for bundlers, Node 16+ ESM, and Node 16+ CJS.
57
+ Call `connect()` once when the voice UI mounts. This opens the realtime
58
+ connection and keeps `sessions`, `invites`, and `status` updated. Call
59
+ `disconnect()` when the user leaves the voice UI, or `dispose()` when the client
60
+ will not be reused.
38
61
 
39
- ## Next.js
62
+ ```ts
63
+ await callpad.connect();
64
+ await callpad.sessions.sync();
65
+ ```
40
66
 
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.
67
+ SDK methods throw `CallpadError` for API problem responses. They do not return a
68
+ custom result wrapper.
44
69
 
45
- ## Vite
70
+ ## Customer App
46
71
 
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.
72
+ Customer apps use the common session client:
49
73
 
50
- ## Local development
74
+ ```ts
75
+ import { createSessionClient } from "farvex";
51
76
 
52
- From `websdk/`:
77
+ const callpad = createSessionClient({
78
+ apiUrl: "https://api.callpad.example.com",
79
+ userId: customer.id,
80
+ auth: { getAccessToken },
81
+ });
53
82
 
54
- ```sh
55
- bun install
56
- bun run dev # tsup watch mode; rebuilds dist/ on change
57
- bun run test # vitest smoke tests
58
- bun run check # lint + format + typecheck
59
- bun run build # one-off production build
83
+ await callpad.connect();
84
+
85
+ const started = await callpad.sessions.start({
86
+ vendor: "acme",
87
+ target: { type: "vendor_user", userId: 42 },
88
+ });
89
+
90
+ if (!started.join) {
91
+ throw new Error("This session did not return a media grant");
92
+ }
60
93
  ```
61
94
 
62
- ### Linking into a consumer app (goagentfe, customer app, etc.)
95
+ `started.session` is the current `VoiceSession`. `started.join` is a LiveKit
96
+ grant for the caller when the current user should join the media room.
97
+
98
+ ### Customer Calls A Vendor User
99
+
100
+ This example starts an app-to-app call from a customer to a specific vendor user
101
+ and renders the audio room after the session is created.
102
+
103
+ ```tsx
104
+ "use client";
105
+
106
+ import { useMemo, useState } from "react";
107
+ import { createSessionClient, type SessionClient, type VoiceJoinGrant } from "farvex";
108
+ import { CallpadProvider, useCallpad } from "farvex/react";
109
+ import { LiveKitRoom, RoomAudioRenderer } from "farvex/livekit";
110
+
111
+ const vendor = "acme";
112
+ const vendorUserId = 42;
113
+
114
+ export const CustomerVoice = ({ customerId }: { customerId: number }) => {
115
+ const client = useMemo<SessionClient>(
116
+ () =>
117
+ createSessionClient({
118
+ apiUrl: process.env.NEXT_PUBLIC_CALLPAD_API_URL!,
119
+ userId: customerId,
120
+ auth: { getAccessToken },
121
+ }),
122
+ [customerId],
123
+ );
124
+
125
+ return (
126
+ <CallpadProvider client={client} connect disposeOnUnmount>
127
+ <CustomerCallButton />
128
+ </CallpadProvider>
129
+ );
130
+ };
131
+
132
+ const CustomerCallButton = () => {
133
+ const callpad = useCallpad();
134
+ const [join, setJoin] = useState<VoiceJoinGrant | null>(null);
135
+
136
+ const startCall = async () => {
137
+ const started = await callpad.sessions.start({
138
+ vendor,
139
+ target: { type: "vendor_user", userId: vendorUserId },
140
+ });
141
+ setJoin(started.join);
142
+ };
143
+
144
+ return (
145
+ <>
146
+ <button type="button" onClick={startCall}>
147
+ Call support
148
+ </button>
149
+ {join ? <SessionMedia join={join} /> : null}
150
+ </>
151
+ );
152
+ };
153
+
154
+ const SessionMedia = ({ join }: { join: VoiceJoinGrant }) => (
155
+ <LiveKitRoom serverUrl={join.url} token={join.token} audio video={false}>
156
+ <RoomAudioRenderer />
157
+ </LiveKitRoom>
158
+ );
159
+ ```
63
160
 
64
- We recommend [`yalc`](https://github.com/wclr/yalc) over `pnpm link` / `bun link` because it survives Vite and Next.js module caches.
161
+ ## Vendor App
65
162
 
66
- One-time setup:
163
+ Vendor-side apps use the host client. Host clients can start calls to customers,
164
+ contacts, and phone numbers, and can use elevated controls such as recording and
165
+ ending a session.
67
166
 
68
- ```sh
69
- # inside websdk/
70
- bun run dev # keep running; watches src/ and rebuilds dist/
71
- npx yalc publish --push # in another shell, first time
72
- ```
167
+ ```ts
168
+ import { createHostClient } from "farvex";
73
169
 
74
- In the consumer app (e.g. `goagentfe`):
170
+ const callpad = createHostClient({
171
+ apiUrl: "https://api.callpad.example.com",
172
+ vendor: "acme",
173
+ userId: user.id,
174
+ auth: { getAccessToken },
175
+ });
75
176
 
76
- ```sh
77
- npx yalc add farvex
78
- pnpm install
79
- ```
177
+ const started = await callpad.sessions.start({
178
+ target: { type: "customer", userId: customer.id },
179
+ });
80
180
 
81
- Each change in `websdk/`:
181
+ await callpad.host.startRecording(started.session.id);
182
+ ```
82
183
 
83
- ```sh
84
- # inside websdk/
85
- npx yalc push # push built dist/ to subscribers
184
+ ### Receiving Customer Calls
185
+
186
+ When a customer calls a vendor user, the vendor app receives an incoming invite
187
+ over realtime. Accepting the invite returns a join grant for the vendor user.
188
+
189
+ ```tsx
190
+ "use client";
191
+
192
+ import { useState } from "react";
193
+ import { type VoiceJoinGrant } from "farvex";
194
+ import { useCallpad, useIncomingInvite } from "farvex/react";
195
+ import { LiveKitRoom, RoomAudioRenderer } from "farvex/livekit";
196
+
197
+ export const IncomingCall = () => {
198
+ const callpad = useCallpad();
199
+ const invite = useIncomingInvite();
200
+ const [join, setJoin] = useState<VoiceJoinGrant | null>(null);
201
+
202
+ if (!invite) {
203
+ return null;
204
+ }
205
+
206
+ const accept = async () => {
207
+ const started = await callpad.sessions.accept({
208
+ sessionId: invite.session.id,
209
+ participantId: invite.participant.id,
210
+ });
211
+ setJoin(started.join);
212
+ };
213
+
214
+ const reject = () =>
215
+ callpad.sessions.reject({
216
+ sessionId: invite.session.id,
217
+ participantId: invite.participant.id,
218
+ });
219
+
220
+ return (
221
+ <>
222
+ <button type="button" onClick={accept}>
223
+ Answer
224
+ </button>
225
+ <button type="button" onClick={reject}>
226
+ Decline
227
+ </button>
228
+ {join ? (
229
+ <LiveKitRoom serverUrl={join.url} token={join.token} audio video={false}>
230
+ <RoomAudioRenderer />
231
+ </LiveKitRoom>
232
+ ) : null}
233
+ </>
234
+ );
235
+ };
86
236
  ```
87
237
 
88
- Before committing in the consumer app, undo the local link so `package.json` doesn't point at `file:.yalc/farvex`:
238
+ ### Host Controls
89
239
 
90
- ```sh
91
- npx yalc remove --all
92
- pnpm install
240
+ ```ts
241
+ const sessionId = started.session.id;
242
+
243
+ await callpad.host.addParticipant({
244
+ sessionId,
245
+ target: { type: "phone", phone: "+15551234567" },
246
+ });
247
+
248
+ await callpad.host.startRecording(sessionId);
249
+ await callpad.host.stopRecording(sessionId);
250
+ await callpad.host.end(sessionId);
93
251
  ```
94
252
 
95
- ## Release flow
253
+ Use `sessions.can(session, action)` or the React `useCan(id, action)` hook to
254
+ gate controls before calling actions such as `accept`, `leave`, `end`, or
255
+ `startRecording`.
256
+
257
+ ## React
258
+
259
+ Import `farvex/react` only from client components.
260
+
261
+ ```tsx
262
+ "use client";
263
+
264
+ import { useMemo, type ReactNode } from "react";
265
+ import { createHostClient, type HostClient } from "farvex";
266
+ import {
267
+ CallpadProvider,
268
+ useCallpad,
269
+ useIncomingInvite,
270
+ useSession,
271
+ useStatus,
272
+ } from "farvex/react";
273
+
274
+ export const VoiceProvider = ({ children }: { children: ReactNode }) => {
275
+ const client = useMemo<HostClient>(
276
+ () =>
277
+ createHostClient({
278
+ apiUrl: process.env.NEXT_PUBLIC_CALLPAD_API_URL!,
279
+ vendor: "acme",
280
+ userId: 42,
281
+ auth: { getAccessToken },
282
+ }),
283
+ [],
284
+ );
285
+
286
+ return (
287
+ <CallpadProvider client={client} connect disposeOnUnmount>
288
+ {children}
289
+ </CallpadProvider>
290
+ );
291
+ };
292
+ ```
96
293
 
97
- See `sdk-scaffold.md` at the repo root for the full plan. Short version (manual, no CI yet):
294
+ Hooks:
98
295
 
99
- ```sh
100
- # while working on a change (from repo root or websdk/):
101
- bun run changeset
296
+ | Hook | Purpose |
297
+ | ------------------------- | ------------------------------------------------- |
298
+ | `useCallpad()` | Current SDK client. |
299
+ | `useStatus()` | Realtime connection status. |
300
+ | `useSessions()` | Visible live sessions. |
301
+ | `useSession(id?)` | Specific session, or active session when omitted. |
302
+ | `useIncomingInvites()` | Pending invites for the current user. |
303
+ | `useIncomingInvite()` | First pending invite. |
304
+ | `useCan(id, action)` | UI helper for session actions. |
305
+ | `useSessionDuration(id?)` | Live elapsed time. |
306
+
307
+ Session participants and metadata are exposed on `VoiceSession.participants`.
308
+ Use LiveKit hooks for media participants, tracks, mute state, speaking state,
309
+ device selection, and room lifecycle.
102
310
 
103
- # when ready to publish (from websdk/), on clean main:
104
- bun run version # applies pending changesets, writes CHANGELOG.md
105
- git commit -am "chore: release farvex@x.y.z"
106
- bun run release # builds, runs changeset publish
107
- git push --follow-tags
311
+ ## LiveKit
312
+
313
+ `farvex/livekit` re-exports first-party LiveKit primitives:
314
+
315
+ ```tsx
316
+ import { LiveKitRoom, RoomAudioRenderer } from "farvex/livekit";
317
+ import type { VoiceJoinGrant } from "farvex";
318
+
319
+ export const SessionMedia = ({ join }: { join: VoiceJoinGrant }) => (
320
+ <LiveKitRoom serverUrl={join.url} token={join.token} audio video={false}>
321
+ <RoomAudioRenderer />
322
+ </LiveKitRoom>
323
+ );
108
324
  ```
109
325
 
110
- Before the first real publish:
326
+ ## Development
327
+
328
+ From `websdk/`:
111
329
 
112
330
  ```sh
113
- npm login # as the publisher account
114
- npm view farvex # must return 404
331
+ bun run generate
332
+ bun run check
115
333
  bun run build
116
- bunx publint
117
- bun pm pack # inspect the tarball contents
118
334
  ```
119
335
 
120
- Pre-1.0: every feature ships as a `minor` bump; breaking changes allowed without a major.
336
+ HTTP calls must go through the generated Hey API client in `src/api/generated/`.
337
+ Do not hand-edit generated files.
121
338
 
122
- ## Package conventions
339
+ ## Release
123
340
 
124
- - Side-effect free — `"sideEffects": false` in `package.json`. Tree-shakes cleanly.
125
- - 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.
127
- - `engines.node >= 18`.
341
+ Changesets lives in `websdk/.changeset`. From `websdk/`:
128
342
 
129
- ## Layout
130
-
131
- ```
132
- websdk/
133
- 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
138
- dist/ build output (gitignored)
139
- tsup.config.ts build config (ESM + CJS + dts, use-client preserved)
140
- 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)
343
+ ```sh
344
+ bun run changeset
345
+ bun run version
346
+ bun run release
144
347
  ```