@susilkumar006/widgets-test 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.
Files changed (107) hide show
  1. package/README.md +445 -0
  2. package/dist/api/FacePeApiClient.d.cts +74 -0
  3. package/dist/api/FacePeApiClient.d.ts +74 -0
  4. package/dist/api/FacePeApiContext.d.cts +8 -0
  5. package/dist/api/FacePeApiContext.d.ts +8 -0
  6. package/dist/api/decoders.d.cts +48 -0
  7. package/dist/api/decoders.d.ts +48 -0
  8. package/dist/api/index.d.cts +8 -0
  9. package/dist/api/index.d.ts +8 -0
  10. package/dist/api/json.d.cts +5 -0
  11. package/dist/api/json.d.ts +5 -0
  12. package/dist/api/useFacePeResource.d.cts +21 -0
  13. package/dist/api/useFacePeResource.d.ts +21 -0
  14. package/dist/components/Avatar/FacePeAvatar.d.cts +23 -0
  15. package/dist/components/Avatar/FacePeAvatar.d.ts +23 -0
  16. package/dist/components/Avatar/engines.d.cts +30 -0
  17. package/dist/components/Avatar/engines.d.ts +30 -0
  18. package/dist/components/Avatar/index.d.cts +1 -0
  19. package/dist/components/Avatar/index.d.ts +1 -0
  20. package/dist/components/Form/FacePeForm.d.cts +19 -0
  21. package/dist/components/Form/FacePeForm.d.ts +19 -0
  22. package/dist/components/Form/index.d.cts +1 -0
  23. package/dist/components/Form/index.d.ts +1 -0
  24. package/dist/components/Picker/FacePePicker.d.cts +22 -0
  25. package/dist/components/Picker/FacePePicker.d.ts +22 -0
  26. package/dist/components/Picker/index.d.cts +1 -0
  27. package/dist/components/Picker/index.d.ts +1 -0
  28. package/dist/components/Placement/FacePeOverlay.d.cts +13 -0
  29. package/dist/components/Placement/FacePeOverlay.d.ts +13 -0
  30. package/dist/components/Placement/FacePePage.d.cts +10 -0
  31. package/dist/components/Placement/FacePePage.d.ts +10 -0
  32. package/dist/components/Placement/index.d.cts +2 -0
  33. package/dist/components/Placement/index.d.ts +2 -0
  34. package/dist/components/Timeline/FacePeTimeline.d.cts +23 -0
  35. package/dist/components/Timeline/FacePeTimeline.d.ts +23 -0
  36. package/dist/components/Timeline/index.d.cts +1 -0
  37. package/dist/components/Timeline/index.d.ts +1 -0
  38. package/dist/components/shared/FacePeErrorBoundary.d.cts +28 -0
  39. package/dist/components/shared/FacePeErrorBoundary.d.ts +28 -0
  40. package/dist/components/shared/surface.d.cts +6 -0
  41. package/dist/components/shared/surface.d.ts +6 -0
  42. package/dist/context/FacePeContext.d.cts +22 -0
  43. package/dist/context/FacePeContext.d.ts +22 -0
  44. package/dist/context/avatar.d.cts +6 -0
  45. package/dist/context/avatar.d.ts +6 -0
  46. package/dist/context/index.d.cts +5 -0
  47. package/dist/context/index.d.ts +5 -0
  48. package/dist/context/navigation.d.cts +10 -0
  49. package/dist/context/navigation.d.ts +10 -0
  50. package/dist/events/FacePeEventEmitter.d.cts +59 -0
  51. package/dist/events/FacePeEventEmitter.d.ts +59 -0
  52. package/dist/events/createCorrelationId.d.cts +9 -0
  53. package/dist/events/createCorrelationId.d.ts +9 -0
  54. package/dist/events/index.d.cts +8 -0
  55. package/dist/events/index.d.ts +8 -0
  56. package/dist/events/telemetry.d.cts +11 -0
  57. package/dist/events/telemetry.d.ts +11 -0
  58. package/dist/events/useFacePeEmitter.d.cts +15 -0
  59. package/dist/events/useFacePeEmitter.d.ts +15 -0
  60. package/dist/index.cjs +1925 -0
  61. package/dist/index.cjs.map +1 -0
  62. package/dist/index.d.cts +26 -0
  63. package/dist/index.d.ts +26 -0
  64. package/dist/index.js +1903 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/schemas/FacePeCustomerPayload.schema.json +31 -0
  67. package/dist/schemas/FacePeError.schema.json +63 -0
  68. package/dist/schemas/FacePeEventMeta.schema.json +53 -0
  69. package/dist/schemas/FacePeFormValues.schema.json +27 -0
  70. package/dist/schemas/FacePeNavigationRequest.schema.json +33 -0
  71. package/dist/schemas/FacePePickerOption.schema.json +34 -0
  72. package/dist/schemas/FacePePickerOptionsPayload.schema.json +49 -0
  73. package/dist/schemas/FacePeTelemetryEvent.schema.json +66 -0
  74. package/dist/schemas/FacePeTimelineItem.schema.json +91 -0
  75. package/dist/schemas/FacePeTimelinePayload.schema.json +109 -0
  76. package/dist/styles.css +887 -0
  77. package/dist/types/api.d.cts +60 -0
  78. package/dist/types/api.d.ts +60 -0
  79. package/dist/types/avatar.d.cts +102 -0
  80. package/dist/types/avatar.d.ts +102 -0
  81. package/dist/types/common.d.cts +42 -0
  82. package/dist/types/common.d.ts +42 -0
  83. package/dist/types/components.d.cts +53 -0
  84. package/dist/types/components.d.ts +53 -0
  85. package/dist/types/config.d.cts +27 -0
  86. package/dist/types/config.d.ts +27 -0
  87. package/dist/types/events.d.cts +85 -0
  88. package/dist/types/events.d.ts +85 -0
  89. package/dist/types/form.d.cts +83 -0
  90. package/dist/types/form.d.ts +83 -0
  91. package/dist/types/index.d.cts +18 -0
  92. package/dist/types/index.d.ts +18 -0
  93. package/dist/types/picker.d.cts +82 -0
  94. package/dist/types/picker.d.ts +82 -0
  95. package/dist/types/placement.d.cts +69 -0
  96. package/dist/types/placement.d.ts +69 -0
  97. package/dist/types/provider.d.cts +75 -0
  98. package/dist/types/provider.d.ts +75 -0
  99. package/dist/types/results.d.cts +28 -0
  100. package/dist/types/results.d.ts +28 -0
  101. package/dist/types/telemetry.d.cts +38 -0
  102. package/dist/types/telemetry.d.ts +38 -0
  103. package/dist/types/timeline.d.cts +103 -0
  104. package/dist/types/timeline.d.ts +103 -0
  105. package/dist/version.d.cts +2 -0
  106. package/dist/version.d.ts +2 -0
  107. package/package.json +71 -0
package/README.md ADDED
@@ -0,0 +1,445 @@
1
+ # @facepe/widgets
2
+
3
+ The FacePe SDK: a reusable React + TypeScript library that a host application
4
+ installs and compiles into its own bundle. It is **not** a standalone app — it
5
+ has no page, no dev server and nothing to run on its own.
6
+
7
+ FacePe components will be exported from this package. At this stage it contains
8
+ only the package foundation and a minimal public entry point
9
+ (`SDK_VERSION`).
10
+
11
+ ## React comes from your application
12
+
13
+ `react` and `react-dom` are **peer dependencies** (React 18 or 19). The SDK does
14
+ not ship its own copy: the build leaves every `react` / `react-dom` import
15
+ external, so the host's React is the only one in the final bundle. A second copy
16
+ would break hooks.
17
+
18
+ ## Install dependencies
19
+
20
+ ```bash
21
+ cd facepe-sdk
22
+ npm install
23
+ ```
24
+
25
+ This also installs React as a dev dependency, used only for type checking and
26
+ building the SDK itself.
27
+
28
+ ## Build
29
+
30
+ ```bash
31
+ npm run build
32
+ ```
33
+
34
+ This runs these steps, and stops at the first failure:
35
+
36
+ 1. `lint-css` checks the stylesheets against the styling contract.
37
+ 2. `vite build` compiles `src/` in library mode to JavaScript.
38
+ 3. `tsc` generates the TypeScript declaration files (and `emit-cjs-types` their CommonJS copies).
39
+ 4. `emit-schemas` generates the JSON Schemas of the payload types.
40
+
41
+ Type check without building:
42
+
43
+ ```bash
44
+ npm run typecheck
45
+ ```
46
+
47
+ ## Build output
48
+
49
+ Everything is written to `dist/`, which is the only folder that is published:
50
+
51
+ | File | Purpose |
52
+ | --- | --- |
53
+ | `dist/index.js` | ES module build (`import`) |
54
+ | `dist/index.cjs` | CommonJS build (`require`) |
55
+ | `dist/index.d.ts` / `dist/index.d.cts` | TypeScript declarations (ESM / CommonJS) |
56
+ | `dist/styles.css` | Component styles, published as `@facepe/widgets/styles.css` |
57
+ | `dist/schemas/*.schema.json` | JSON Schemas of the payloads, published as `@facepe/widgets/schemas/…` |
58
+ | `*.map` | Source maps (with sources embedded) |
59
+
60
+ ## Using it from a React app
61
+
62
+ ```tsx
63
+ import { FacePeProvider, FacePeForm } from '@facepe/widgets';
64
+ import '@facepe/widgets/styles.css'; // once, anywhere in the app
65
+
66
+ <FacePeProvider config={{ mode: 'edit', locale: 'en-IN' }}>
67
+ <FacePeForm onSubmit={(event) => save(event.payload)} />
68
+ </FacePeProvider>
69
+ ```
70
+
71
+ ### Who can use FacePe: the access flow
72
+
73
+ Access is granted top-down, and every level is checked on every request:
74
+
75
+ ```text
76
+ Super admin creates the company, enables "FacePe SDK" and/or "FacePe API" for it,
77
+ and grants it avatars
78
+ Company admin creates users, assigns each user avatar_ids, and gives a user
79
+ SDK/API access: Dashboard → Users → SDK / API → access key + secret key
80
+ User's app its SERVER exchanges the keys for a 5-minute token; the SDK (or the
81
+ app's own API calls) use that token
82
+ Every call company allowed? → user allowed? → this avatar_id assigned? → allowed
83
+ ```
84
+
85
+ Revoking or regenerating a user's keys, disabling the user, or switching the
86
+ company's access off takes effect on the next request, not when tokens expire.
87
+
88
+ ### The avatar: `<FacePeAvatar />`
89
+
90
+ The same avatar experience as on the FacePe website: the same avatar, trained
91
+ prompt, voice and features, embedded in your page. The minimum setup:
92
+
93
+ ```tsx
94
+ import '@facepe/widgets/styles.css';
95
+ import { FacePeProvider, FacePeAvatar } from '@facepe/widgets';
96
+
97
+ <FacePeProvider
98
+ config={{}}
99
+ apiBaseUrl="https://api.facepe.com/api/v1/sdk"
100
+ getToken={({ forceRefresh }) => // YOUR server, below
101
+ fetch(forceRefresh ? '/facepe-token?refresh=1' : '/facepe-token')
102
+ .then((r) => (r.ok ? r.text() : Promise.reject(new Error(`token ${r.status}`))))}
103
+ >
104
+ <FacePeAvatar />
105
+ </FacePeProvider>
106
+ ```
107
+
108
+ No avatar_id is needed: with only the user's keys (on your server), the
109
+ component loads the avatars assigned to that user and shows them to choose
110
+ from (the first is selected; with one avatar there is no choice to make). To
111
+ show one particular avatar instead, pass its `avatarId` to the provider or
112
+ the component.
113
+
114
+ `<FacePeAvatar>` shows the avatar's portrait and a **Start conversation**
115
+ button (browsers allow sound and the microphone only after a click). It then
116
+ streams the avatar's video and voice, listens to the user's microphone, and
117
+ shows captions; **Mute** and **End** control the session. Props: `avatarId`
118
+ (optional; overrides the provider's), `language` (`'en'`, `'hi'`, …), `autoStart`,
119
+ `captions`, plus the usual `emphasis`, `size`, `density`. Events:
120
+ `onSessionStart`, `onSessionEnd`, `onTranscript`, `onOrderUpdate`, `onError`.
121
+ Handle: `start()`, `end()`, `setMuted(muted)`, `focus()`. Slot: `controls`
122
+ replaces the built-in buttons.
123
+
124
+ The connection libraries are installed with the package and loaded in their
125
+ own chunks only when an avatar is shown, so they cost nothing on other pages.
126
+ An avatar is described only by its name, gender, portrait, description and
127
+ languages: where it comes from is never part of the API or the SDK.
128
+
129
+ ### Connecting: keys on your server, a token in the browser
130
+
131
+ The secret key must never be in browser code. Your server exchanges the keys
132
+ and hands the browser only the short-lived token:
133
+
134
+ ```ts
135
+ // Your server (e.g. Express), with the keys from the FacePe dashboard in env vars.
136
+ // Reuse the token for its 5 minutes; fetch a new one only near expiry or on ?refresh=1.
137
+ let cached = { token: null, expiresAt: 0 };
138
+ app.get('/facepe-token', async (req, res) => {
139
+ if (req.query.refresh !== '1' && cached.token && cached.expiresAt - Date.now() > 30_000) {
140
+ return res.type('text').send(cached.token);
141
+ }
142
+ const r = await fetch('https://api.facepe.com/api/v1/sdk/token', {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({
146
+ accessKey: process.env.FACEPE_ACCESS_KEY,
147
+ secretKey: process.env.FACEPE_SECRET_KEY,
148
+ channel: 'sdk', // 'api' for direct API use
149
+ }),
150
+ });
151
+ const body = await r.json();
152
+ if (!r.ok) return res.status(r.status).json(body.error); // e.g. access switched off
153
+ cached = { token: body.data.token, expiresAt: Date.now() + body.data.expiresIn * 1000 };
154
+ res.type('text').send(cached.token);
155
+ });
156
+ ```
157
+
158
+ - `apiBaseUrl` is the FacePe gateway, `…/api/v1/sdk`. The SDK calls nothing else.
159
+ - **Cache the token** for its lifetime (`expiresIn`, 5 minutes) in `getToken`
160
+ and fetch a new one only when it is about to expire or `forceRefresh` is
161
+ true; the SDK asks for a token on every request, and a fresh exchange each
162
+ time adds a round trip to every call.
163
+ - `getToken` receives `{ audience: 'app-b', forceRefresh }` and returns a string
164
+ or a Promise. After a 401 it is called again with `forceRefresh: true` and
165
+ the request is retried once. If the retry is refused too, `onSessionExpired` fires.
166
+ - Every request carries `X-Request-Id` (correlation) and `X-FacePe-SDK-Version`.
167
+
168
+ ### Using the API directly (no SDK)
169
+
170
+ Exchange the keys with `"channel": "api"` (the company needs API access), then
171
+ call the gateway with `Authorization: Bearer <token>`:
172
+
173
+ | Request | Returns |
174
+ | --- | --- |
175
+ | `POST /api/v1/sdk/token` `{ accessKey, secretKey, channel }` | `{ token, expiresIn, channel }` |
176
+ | `GET /api/v1/sdk/session` | the company, the user, the user's `avatarIds` |
177
+ | `GET /api/v1/sdk/avatars` | the avatars assigned to the user |
178
+ | `GET /api/v1/sdk/avatars/:avatarId` | one avatar (403 if not assigned) |
179
+ | `POST /api/v1/sdk/avatars/:avatarId/sessions` `{ language? }` | how to connect: `connection` (`{ type: 'room', url, token }` or `{ type: 'stream', token, greeting }`), and a `usageId` |
180
+ | `POST /api/v1/sdk/avatar-sessions/:usageId/end` | closes the session (usage minutes) |
181
+
182
+ Refusals: `401` for missing, invalid, expired or revoked credentials; `403`
183
+ when the company's access is off, the user is disabled, or the avatar is not
184
+ assigned to the user; `429` when one access key exceeds its rate limit
185
+ (`SDK_RATE_LIMIT_PER_MIN`, default 120 per minute).
186
+
187
+ ### Data: pass an id, or pass the data
188
+
189
+ Each component either shows data the host passes in, or, given only an
190
+ `entityId`, fetches it through the gateway (architecture §7.5):
191
+
192
+ | Component | Host passes | Or `entityId` = | Fetched from |
193
+ | --- | --- | --- | --- |
194
+ | `FacePeTimeline` | `items` | an order id | `GET /orders/:id/timeline`, the order's status history |
195
+ | `FacePePicker` | `options` | a location id | `GET /locations/:id/categories`, its menu categories |
196
+ | `FacePeForm` | `initialValues` | an order id | `GET /orders/:id/customer`; Submit saves with `PUT` |
197
+
198
+ Data passed by the host always wins; nothing is fetched then. While loading,
199
+ a component shows a loading state; a failed load raises `onError` with the
200
+ gateway's error code. Payloads are validated on arrival, and unknown fields
201
+ are ignored.
202
+
203
+ `entityId` can also be set once on the provider's `config`. It then applies
204
+ to every component, so set it per component when they show different
205
+ entities (an order and a location).
206
+
207
+ JSON Schemas of every payload ship in the package:
208
+ `@facepe/widgets/schemas/FacePeTimelinePayload.schema.json`, and likewise
209
+ `FacePePickerOptionsPayload`, `FacePeCustomerPayload`, `FacePeTimelineItem`,
210
+ `FacePePickerOption`, `FacePeFormValues`, `FacePeEventMeta`, `FacePeError`,
211
+ `FacePeNavigationRequest` and `FacePeTelemetryEvent`.
212
+
213
+ ### Telemetry
214
+
215
+ Pass your own sink; the SDK makes no analytics calls of its own:
216
+
217
+ ```tsx
218
+ <FacePeProvider config={config} telemetry={{ track: (e) => analytics.log(e) }}>
219
+ ```
220
+
221
+ `track` receives one record per component event (`name` = the event type)
222
+ and one per API call (`name` = `'api.request'`, with `method`, `path`,
223
+ `status`, `outcome` and `durationMs`). Every record carries the `sdkVersion`
224
+ and the `correlationId` (the same as the event's, or the request's
225
+ `X-Request-Id`). Event payloads are never included, since they can hold what
226
+ the user typed; `error` events carry only the error code. A sink that throws
227
+ is reported and ignored.
228
+
229
+ ### Placement: inline, page, overlay
230
+
231
+ The host decides where an experience appears; the component inside is the same.
232
+
233
+ ```tsx
234
+ <FacePeForm /> {/* inline: inside an existing page */}
235
+ <FacePePage><FacePeTimeline items={items} /></FacePePage> {/* full view */}
236
+ <FacePeOverlay open={open} label="Edit" onClose={() => setOpen(false)}>
237
+ <FacePeForm onCancel={() => setOpen(false)} />
238
+ </FacePeOverlay> {/* modal dialog */}
239
+ <FacePeOverlay variant="drawer" open={open} label="Details" onClose={close}>
240
+ …
241
+ </FacePeOverlay> {/* side drawer */}
242
+ ```
243
+
244
+ Titles, breadcrumbs, routing and action bars stay with the host.
245
+
246
+ ### Navigation
247
+
248
+ Components never change the URL. They raise `navigate` events with a
249
+ `{ to, params?, replace? }` request; wire them to your router once on the
250
+ provider (or per component):
251
+
252
+ ```tsx
253
+ <FacePeProvider config={config} onNavigate={(e) => router.navigate(e.payload.to)}>
254
+ ```
255
+
256
+ ### Events
257
+
258
+ Every `on…` callback receives `{ type, payload, meta }`. `meta` carries
259
+ `schemaVersion` (1), `correlationId`, `timestamp`, `sdkVersion`, `source`
260
+ (the component) and, when the component has one, `entityId`. Events never
261
+ contain credentials.
262
+
263
+ ### Errors
264
+
265
+ Each component has its own error boundary: a failure inside it does not take
266
+ your page down. It raises `onError` with code `RENDER_FAILED` and shows the
267
+ `fallback` slot (or a short message). Remount it (e.g. a new `key`) to retry.
268
+
269
+ ### Imperative handles
270
+
271
+ Reach them with a `ref`. Every method returns a `Promise<Result>` and never throws.
272
+
273
+ | Component | Methods |
274
+ | --- | --- |
275
+ | `FacePeForm` | `focus`, `validate`, `reset` |
276
+ | `FacePePicker` | `focus`, `reset` |
277
+ | `FacePeTimeline` | `focus`, `reset`, `scrollToSection(id)` |
278
+
279
+ Submitting, selecting and cancelling are user actions; they reach you as
280
+ events (`onSubmit`, `onSelect`, `onCancel`), not as handle methods.
281
+
282
+ ### Customization
283
+
284
+ The package hard-codes no colours and no fonts. With nothing set, a component
285
+ inherits the host page's text colour, font and size, has a transparent
286
+ surface, and derives borders and muted text from the text colour. You choose
287
+ the look, from the least to the most effort:
288
+
289
+ 1. **Design tokens (L1)** on the provider: `theme={{ colorSurface,
290
+ colorSurfaceMuted, colorText, colorMuted, colorBorder, colorAccent,
291
+ colorOnAccent, colorDanger, colorSuccess, fontFamily, fontSize, spacing,
292
+ radius, shadow }}`. For dark mode pass `darkTheme` too, and set
293
+ `colorScheme="light" | "dark"` from your own app's setting: dark tokens
294
+ override the main ones per token. The SDK never reads the system preference
295
+ itself.
296
+ 2. **Variants (L2)** on every component: `emphasis="subtle" | "default" |
297
+ "strong"`, `size="small" | "medium" | "large"`, `density`; per component,
298
+ `variant` — `FacePeForm`: `"stacked" | "inline"`, `FacePePicker`:
299
+ `"list" | "grid"`.
300
+ 3. **Slots (L3)**: `FacePeForm` — `header`, `actions` (render function given
301
+ `{ submit, cancel, submitting, readOnly }`), `footer`; `FacePePicker` —
302
+ `header`, `option` (render function given `{ option, selected, disabled }`),
303
+ `emptyState`; `FacePeTimeline` — `item`, `emptyState`; all — `fallback`.
304
+ 4. **CSS variables (L4)**, documented; set them on any ancestor:
305
+
306
+ | Variable | Default when unset |
307
+ | --- | --- |
308
+ | `--fp-color-surface` | transparent (the host's background) |
309
+ | `--fp-color-surface-muted` | a light tint of the text colour |
310
+ | `--fp-color-text` | inherited from the host |
311
+ | `--fp-color-muted` | the text colour, partly transparent |
312
+ | `--fp-color-border` | the text colour, mostly transparent |
313
+ | `--fp-color-accent` | the text colour (the primary action is outlined) |
314
+ | `--fp-color-on-accent` | inherited text colour |
315
+ | `--fp-color-danger` | the text colour |
316
+ | `--fp-color-success` | the text colour |
317
+ | `--fp-font-family` | inherited from the host |
318
+ | `--fp-font-size` | inherited from the host |
319
+ | `--fp-spacing` | `0.25rem` (gaps and padding are multiples) |
320
+ | `--fp-radius` | `0.375rem` (frames use 1.5×) |
321
+ | `--fp-shadow` | a soft shadow tinted from the text colour (`emphasis="strong"`) |
322
+ | `--fp-content-max-width` | `32rem` (form, picker; `none` in page/overlay) |
323
+
324
+ Components adapt to the width of their container (container queries), not
325
+ the viewport, so they fit a sidebar or a full page alike.
326
+
327
+ `npm run lint:css` (also run by `npm run build`) fails the build if a
328
+ stylesheet hard-codes a colour or font, sets a `--fp-*` token, or uses an
329
+ unscoped selector.
330
+
331
+ ### Using the SDK in a Tailwind app
332
+
333
+ It works as is: the SDK's classes are scoped, so Tailwind and the SDK cannot
334
+ clash, and its buttons keep their look under Tailwind's Preflight reset.
335
+ Style it with Tailwind through the documented variables (arbitrary
336
+ properties) and through slots — not by targeting the SDK's internal class
337
+ names, which change with every build.
338
+
339
+ ```jsx
340
+ <div className="[--fp-color-accent:theme(colors.violet.600)] [--fp-color-on-accent:white] [--fp-radius:theme(borderRadius.xl)]">
341
+ <FacePeForm slots={{ header: <h3 className="text-lg font-semibold">Contact us</h3> }} />
342
+ </div>
343
+ ```
344
+
345
+ With Tailwind v4, use its CSS variables instead of `theme()`:
346
+ `[--fp-color-accent:var(--color-violet-600)]`.
347
+
348
+ Component styles are scoped (CSS Modules) and ship as one stylesheet,
349
+ `@facepe/widgets/styles.css`. Import it once; without it the components render
350
+ unstyled.
351
+
352
+ Until the package is published to a registry, a local app can install it
353
+ straight from this folder:
354
+
355
+ ```bash
356
+ npm install ../facepe-sdk
357
+ ```
358
+
359
+ Everything public is imported from the package root. Deeper paths such as
360
+ `@facepe/widgets/dist/...` are blocked by the package `exports` map, so
361
+ internal files can change without breaking your app.
362
+
363
+ ## Compatibility
364
+
365
+ | | Supported | Verified with |
366
+ | --- | --- | --- |
367
+ | React (peer dependency) | 18 and 19 | 18.3 (test suites) |
368
+ | TypeScript consumers | 5.x; `bundler`, `node16` ESM and `node16` CommonJS resolution | 5.9, all three resolution modes |
369
+ | Bundlers | any that reads `exports` and CSS imports | Vite 7 (SDK build), webpack 5 (`tests/webpack.test.mjs`) |
370
+ | Browsers | Chrome / Edge 111+, Safari 16.2+, Firefox 113+ (needs container queries, `<dialog>`, `color-mix()`) | Chrome (automated checks) |
371
+
372
+ Report an incompatibility rather than working around it in your app.
373
+
374
+ ## Testing
375
+
376
+ ```bash
377
+ npm test # build, then every suite in tests/
378
+ npm run test:only picker # suites whose file name contains "picker" (needs a build)
379
+ npm run api:check # public API unchanged vs etc/widgets.api.md
380
+ npm run size # bundle budget (150 KB gzip) and no bundled React
381
+ ```
382
+
383
+ The suites test the built package the way a host consumes it: behaviour of
384
+ each component in jsdom, the TypeScript contract in three module setups,
385
+ telemetry and gateway data against a fake gateway, and a webpack 5 host build.
386
+
387
+ When you change the public API on purpose, run `npm run api:update` and
388
+ commit `etc/widgets.api.md` with the change; reviewers see exactly what changed
389
+ in the contract, and the version bump (major for a breaking change) follows
390
+ from it. CI (`.github/workflows/sdk.yml`) runs all of the above on every pull
391
+ request, plus the gateway tests.
392
+
393
+ ## Releasing
394
+
395
+ Only CI publishes (`npm publish` refuses anywhere else):
396
+
397
+ 1. Bump `version` in `package.json` and move the `[Unreleased]` notes in
398
+ `CHANGELOG.md` under a `## [x.y.z]` heading.
399
+ 2. Merge to `main`, then push the tag: `git tag widgets-vx.y.z && git push origin widgets-vx.y.z`.
400
+ 3. The "SDK release" workflow re-runs every check, signs a build-provenance
401
+ attestation, writes an SBOM, publishes the tarball, and creates a GitHub
402
+ release with the tarball, SBOM and notes.
403
+
404
+ Support: the current and previous minor lines get security and critical
405
+ fixes (N-1). Deprecated exports, slots and tokens keep working, with a
406
+ warning in the changelog, for at least one minor release before removal.
407
+
408
+ ## Upgrading host apps
409
+
410
+ Host apps get an upgrade pull request for each release from Renovate by
411
+ extending the preset in this folder:
412
+
413
+ ```json
414
+ { "extends": ["github>Dharun4242/Facepe//facepe-sdk/renovate-host-preset"] }
415
+ ```
416
+
417
+ Pin the version (the preset does), keep the lockfile committed, and let the
418
+ host's CI and visual regression run on the PR. To roll back, revert to the
419
+ previous pinned version.
420
+
421
+ ## Adding to the public API
422
+
423
+ `src/index.ts` is the only public entry point and holds exports only. Add each
424
+ new public component, hook or type there by name (`export { X } from ...`,
425
+ `export type { Y } from ...`), never with `export *`. Anything not listed there
426
+ is internal.
427
+
428
+ ## Project layout
429
+
430
+ ```
431
+ facepe-sdk/
432
+ ├── src/
433
+ │ ├── index.ts public entry point: the only file consumers import from
434
+ │ ├── components/ FacePe components (Form/, Picker/, Timeline/, Placement/)
435
+ │ ├── context/ FacePeProvider and useFacePe
436
+ │ ├── types/ public TypeScript contracts
437
+ │ ├── events/ event emitter shared by components (not public)
438
+ │ ├── api/ API client, one per provider (not public)
439
+ │ ├── version.ts SDK_VERSION
440
+ │ └── globals.d.ts build-time constants and CSS Module typings
441
+ ├── scripts/ build helpers (CommonJS declarations)
442
+ ├── package.json package metadata, exports, peer dependencies
443
+ ├── tsconfig.json strict TypeScript, declaration output
444
+ └── vite.config.ts library-mode build, React kept external
445
+ ```
@@ -0,0 +1,74 @@
1
+ import type { FacePeTelemetryReporter } from '../events/telemetry.cjs';
2
+ import type { AsyncResult, FacePeSessionExpiredPayload, FacePeTokenProvider, JsonValue, Result } from '../types/index.cjs';
3
+ /** Request header carrying the correlation id (the gateway's agreed name). */
4
+ export declare const CORRELATION_HEADER = "X-Request-Id";
5
+ /** Request header carrying the SDK version, for the gateway's per-version metrics. */
6
+ export declare const SDK_VERSION_HEADER = "X-FacePe-SDK-Version";
7
+ /** The audience every token is requested for (architecture §7.4: the FacePe service). */
8
+ export declare const TOKEN_AUDIENCE = "app-b";
9
+ /** Per-attempt timeout when neither the provider nor the call sets one. */
10
+ export declare const DEFAULT_TIMEOUT_MS = 15000;
11
+ /** Provider settings, read afresh on every request. */
12
+ export type FacePeApiSettings = {
13
+ readonly baseUrl?: string;
14
+ readonly getToken?: FacePeTokenProvider;
15
+ readonly timeoutMs?: number;
16
+ };
17
+ export type FacePeApiClientOptions = {
18
+ readonly settings: () => FacePeApiSettings;
19
+ /** Called once a request is refused even after a fresh token. */
20
+ readonly onSessionExpired?: (payload: FacePeSessionExpiredPayload) => void;
21
+ /** Receives one `api.request` telemetry record per call. */
22
+ readonly onTelemetry?: FacePeTelemetryReporter;
23
+ };
24
+ /** Turns response data into the type a caller expects, or a Failure if it does not fit. */
25
+ export type FacePeDecoder<T> = (data: JsonValue) => Result<T>;
26
+ export type FacePeQuery = Readonly<Record<string, string | number | boolean | null | undefined>>;
27
+ export type FacePeRequestOptions = {
28
+ /** Query-string parameters; `null`/`undefined` ones are left out. */
29
+ readonly query?: FacePeQuery;
30
+ /** Overrides the provider's per-attempt timeout for this call. */
31
+ readonly timeoutMs?: number;
32
+ /** Lets the caller cancel (e.g. on unmount); resolves to Failure ABORTED. */
33
+ readonly signal?: AbortSignal;
34
+ /** Let the request outlive the page (e.g. ending a session as the tab closes). */
35
+ readonly keepalive?: boolean;
36
+ };
37
+ type Decoded<T> = FacePeRequestOptions & {
38
+ readonly decode: FacePeDecoder<T>;
39
+ };
40
+ export declare class FacePeApiClient {
41
+ private readonly options;
42
+ /** The last token getToken returned (memory only), for keepalive requests. */
43
+ private lastToken;
44
+ constructor(options: FacePeApiClientOptions);
45
+ /**
46
+ * A full address for an asset the API returned as a path (e.g. an avatar's
47
+ * picture, "/avatars/aria.jpg"): resolved against the API's address, not
48
+ * the host page's. Absolute URLs are returned unchanged.
49
+ */
50
+ assetUrl(url: string | null | undefined): string | null;
51
+ get(path: string, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
52
+ get<T>(path: string, options: Decoded<T>): AsyncResult<T>;
53
+ post(path: string, body?: JsonValue, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
54
+ post<T>(path: string, body: JsonValue | undefined, options: Decoded<T>): AsyncResult<T>;
55
+ put(path: string, body?: JsonValue, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
56
+ put<T>(path: string, body: JsonValue | undefined, options: Decoded<T>): AsyncResult<T>;
57
+ patch(path: string, body?: JsonValue, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
58
+ patch<T>(path: string, body: JsonValue | undefined, options: Decoded<T>): AsyncResult<T>;
59
+ delete(path: string, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
60
+ delete<T>(path: string, options: Decoded<T>): AsyncResult<T>;
61
+ private send;
62
+ private perform;
63
+ /** The token to send, or `null` when the provider has no token provider. */
64
+ private token;
65
+ /** One HTTP attempt, response body included, under one timeout. */
66
+ private attempt;
67
+ /**
68
+ * Parses the body. The FacePe backend answers `{ success: true, data }` or
69
+ * `{ success: false, error: { code, message } }`; the envelope is unwrapped
70
+ * and its error code kept. Any other JSON body is taken as the data itself.
71
+ */
72
+ private read;
73
+ }
74
+ export {};
@@ -0,0 +1,74 @@
1
+ import type { FacePeTelemetryReporter } from '../events/telemetry.js';
2
+ import type { AsyncResult, FacePeSessionExpiredPayload, FacePeTokenProvider, JsonValue, Result } from '../types/index.js';
3
+ /** Request header carrying the correlation id (the gateway's agreed name). */
4
+ export declare const CORRELATION_HEADER = "X-Request-Id";
5
+ /** Request header carrying the SDK version, for the gateway's per-version metrics. */
6
+ export declare const SDK_VERSION_HEADER = "X-FacePe-SDK-Version";
7
+ /** The audience every token is requested for (architecture §7.4: the FacePe service). */
8
+ export declare const TOKEN_AUDIENCE = "app-b";
9
+ /** Per-attempt timeout when neither the provider nor the call sets one. */
10
+ export declare const DEFAULT_TIMEOUT_MS = 15000;
11
+ /** Provider settings, read afresh on every request. */
12
+ export type FacePeApiSettings = {
13
+ readonly baseUrl?: string;
14
+ readonly getToken?: FacePeTokenProvider;
15
+ readonly timeoutMs?: number;
16
+ };
17
+ export type FacePeApiClientOptions = {
18
+ readonly settings: () => FacePeApiSettings;
19
+ /** Called once a request is refused even after a fresh token. */
20
+ readonly onSessionExpired?: (payload: FacePeSessionExpiredPayload) => void;
21
+ /** Receives one `api.request` telemetry record per call. */
22
+ readonly onTelemetry?: FacePeTelemetryReporter;
23
+ };
24
+ /** Turns response data into the type a caller expects, or a Failure if it does not fit. */
25
+ export type FacePeDecoder<T> = (data: JsonValue) => Result<T>;
26
+ export type FacePeQuery = Readonly<Record<string, string | number | boolean | null | undefined>>;
27
+ export type FacePeRequestOptions = {
28
+ /** Query-string parameters; `null`/`undefined` ones are left out. */
29
+ readonly query?: FacePeQuery;
30
+ /** Overrides the provider's per-attempt timeout for this call. */
31
+ readonly timeoutMs?: number;
32
+ /** Lets the caller cancel (e.g. on unmount); resolves to Failure ABORTED. */
33
+ readonly signal?: AbortSignal;
34
+ /** Let the request outlive the page (e.g. ending a session as the tab closes). */
35
+ readonly keepalive?: boolean;
36
+ };
37
+ type Decoded<T> = FacePeRequestOptions & {
38
+ readonly decode: FacePeDecoder<T>;
39
+ };
40
+ export declare class FacePeApiClient {
41
+ private readonly options;
42
+ /** The last token getToken returned (memory only), for keepalive requests. */
43
+ private lastToken;
44
+ constructor(options: FacePeApiClientOptions);
45
+ /**
46
+ * A full address for an asset the API returned as a path (e.g. an avatar's
47
+ * picture, "/avatars/aria.jpg"): resolved against the API's address, not
48
+ * the host page's. Absolute URLs are returned unchanged.
49
+ */
50
+ assetUrl(url: string | null | undefined): string | null;
51
+ get(path: string, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
52
+ get<T>(path: string, options: Decoded<T>): AsyncResult<T>;
53
+ post(path: string, body?: JsonValue, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
54
+ post<T>(path: string, body: JsonValue | undefined, options: Decoded<T>): AsyncResult<T>;
55
+ put(path: string, body?: JsonValue, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
56
+ put<T>(path: string, body: JsonValue | undefined, options: Decoded<T>): AsyncResult<T>;
57
+ patch(path: string, body?: JsonValue, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
58
+ patch<T>(path: string, body: JsonValue | undefined, options: Decoded<T>): AsyncResult<T>;
59
+ delete(path: string, options?: FacePeRequestOptions): AsyncResult<JsonValue>;
60
+ delete<T>(path: string, options: Decoded<T>): AsyncResult<T>;
61
+ private send;
62
+ private perform;
63
+ /** The token to send, or `null` when the provider has no token provider. */
64
+ private token;
65
+ /** One HTTP attempt, response body included, under one timeout. */
66
+ private attempt;
67
+ /**
68
+ * Parses the body. The FacePe backend answers `{ success: true, data }` or
69
+ * `{ success: false, error: { code, message } }`; the envelope is unwrapped
70
+ * and its error code kept. Any other JSON body is taken as the data itself.
71
+ */
72
+ private read;
73
+ }
74
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { FacePeApiClient } from './FacePeApiClient.cjs';
2
+ /**
3
+ * Internal: the API client of the nearest `<FacePeProvider>`. Kept apart from
4
+ * the public context so `useFacePe()` and `FacePeContextValue` are unchanged.
5
+ */
6
+ export declare const FacePeApiContext: import("react").Context<FacePeApiClient | null>;
7
+ /** The API client of the nearest `<FacePeProvider>`, for SDK components. */
8
+ export declare function useFacePeApi(): FacePeApiClient;
@@ -0,0 +1,8 @@
1
+ import type { FacePeApiClient } from './FacePeApiClient.js';
2
+ /**
3
+ * Internal: the API client of the nearest `<FacePeProvider>`. Kept apart from
4
+ * the public context so `useFacePe()` and `FacePeContextValue` are unchanged.
5
+ */
6
+ export declare const FacePeApiContext: import("react").Context<FacePeApiClient | null>;
7
+ /** The API client of the nearest `<FacePeProvider>`, for SDK components. */
8
+ export declare function useFacePeApi(): FacePeApiClient;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Internal: runtime validation of gateway payloads — the trust boundary, the
3
+ * one place types are checked at runtime (architecture §7.5). Each decoder
4
+ * accepts the documented shape, ignores unknown fields (forward
5
+ * compatibility) and turns anything else into Failure INVAL_RESPONSE-style
6
+ * `INVALID_PAYLOAD`, never an exception.
7
+ */
8
+ import type { FacePeAvatarInfo, FacePeFormValues, FacePePickerOption, FacePeTimelineItem } from '../types/index.cjs';
9
+ import type { FacePeDecoder } from './FacePeApiClient.cjs';
10
+ /** GET /orders/:id/timeline → `{ items: FacePeTimelineItem[] }`. */
11
+ export declare const decodeTimeline: FacePeDecoder<readonly FacePeTimelineItem[]>;
12
+ /** GET /locations/:id/categories → `{ options: FacePePickerOption[] }`. */
13
+ export declare const decodePickerOptions: FacePeDecoder<readonly FacePePickerOption[]>;
14
+ /** GET/PUT /orders/:id/customer → `{ name, email, notes }`. */
15
+ export declare const decodeFormValues: FacePeDecoder<FacePeFormValues>;
16
+ /** GET /avatars/:avatarId → `{ avatar }`. */
17
+ export declare const decodeAvatar: FacePeDecoder<FacePeAvatarInfo>;
18
+ /** GET /avatars → `{ avatars }`: the avatars the signed-in user may use. Entries that do not decode are skipped. */
19
+ export declare const decodeAvatarList: FacePeDecoder<readonly FacePeAvatarInfo[]>;
20
+ /**
21
+ * What POST /avatars/:avatarId/sessions returns: how to connect. The gateway
22
+ * never says where an avatar comes from, only the kind of connection:
23
+ * `room` (join a media room) or `stream` (a hosted stream).
24
+ */
25
+ export type FacePeAvatarConnection = {
26
+ readonly type: 'stream';
27
+ readonly usageId: string | null;
28
+ readonly avatar: FacePeAvatarInfo;
29
+ readonly token: string;
30
+ readonly greeting: string | null;
31
+ } | {
32
+ readonly type: 'room';
33
+ readonly usageId: string | null;
34
+ readonly avatar: FacePeAvatarInfo;
35
+ readonly url: string;
36
+ readonly token: string;
37
+ };
38
+ export declare const decodeAvatarConnection: FacePeDecoder<FacePeAvatarConnection>;
39
+ /** Gateway paths, one per component (entity ids are path-encoded). */
40
+ export declare const gatewayPath: {
41
+ readonly avatars: "/avatars";
42
+ readonly avatar: (avatarId: string) => string;
43
+ readonly avatarSessions: (avatarId: string) => string;
44
+ readonly endAvatarSession: (usageId: string) => string;
45
+ readonly timeline: (orderId: string) => string;
46
+ readonly pickerOptions: (locationId: string) => string;
47
+ readonly customer: (orderId: string) => string;
48
+ };