@hainiva/agent-sdk 0.0.0-stage → 0.3.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 (119) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +21 -0
  3. package/README.md +278 -2
  4. package/SECURITY.md +33 -0
  5. package/dist/cjs/agent.cjs +320 -0
  6. package/dist/cjs/agent.d.cts +239 -0
  7. package/dist/cjs/calls.cjs +277 -0
  8. package/dist/cjs/calls.d.cts +232 -0
  9. package/dist/cjs/contract.cjs +14 -0
  10. package/dist/cjs/contract.d.cts +225 -0
  11. package/dist/cjs/embed.cjs +253 -0
  12. package/dist/cjs/embed.d.cts +187 -0
  13. package/dist/cjs/emitter.cjs +50 -0
  14. package/dist/cjs/emitter.d.cts +22 -0
  15. package/dist/cjs/errors.cjs +28 -0
  16. package/dist/cjs/errors.d.cts +15 -0
  17. package/dist/cjs/events.cjs +97 -0
  18. package/dist/cjs/events.d.cts +97 -0
  19. package/dist/cjs/index.cjs +72 -0
  20. package/dist/cjs/index.d.cts +28 -0
  21. package/dist/cjs/livekit.cjs +56 -0
  22. package/dist/cjs/livekit.d.cts +35 -0
  23. package/dist/cjs/log.cjs +15 -0
  24. package/dist/cjs/log.d.cts +5 -0
  25. package/dist/cjs/react-native.cjs +32 -0
  26. package/dist/cjs/react-native.d.cts +39 -0
  27. package/dist/cjs/reactNativeDesk.cjs +107 -0
  28. package/dist/cjs/reactNativeDesk.d.cts +53 -0
  29. package/dist/cjs/redact.cjs +71 -0
  30. package/dist/cjs/redact.d.cts +21 -0
  31. package/dist/cjs/rest.cjs +286 -0
  32. package/dist/cjs/rest.d.cts +172 -0
  33. package/dist/cjs/session.cjs +159 -0
  34. package/dist/cjs/session.d.cts +101 -0
  35. package/dist/cjs/singleFlight.cjs +49 -0
  36. package/dist/cjs/singleFlight.d.cts +32 -0
  37. package/dist/cjs/url.cjs +39 -0
  38. package/dist/cjs/url.d.cts +13 -0
  39. package/dist/cjs/web.cjs +28 -0
  40. package/dist/cjs/web.d.cts +23 -0
  41. package/dist/cjs/ws.cjs +246 -0
  42. package/dist/cjs/ws.d.cts +134 -0
  43. package/dist/esm/agent.d.ts +240 -0
  44. package/dist/esm/agent.d.ts.map +1 -0
  45. package/dist/esm/agent.js +317 -0
  46. package/dist/esm/agent.js.map +1 -0
  47. package/dist/esm/calls.d.ts +233 -0
  48. package/dist/esm/calls.d.ts.map +1 -0
  49. package/dist/esm/calls.js +271 -0
  50. package/dist/esm/calls.js.map +1 -0
  51. package/dist/esm/contract.d.ts +226 -0
  52. package/dist/esm/contract.d.ts.map +1 -0
  53. package/dist/esm/contract.js +12 -0
  54. package/dist/esm/contract.js.map +1 -0
  55. package/dist/esm/embed.d.ts +188 -0
  56. package/dist/esm/embed.d.ts.map +1 -0
  57. package/dist/esm/embed.js +247 -0
  58. package/dist/esm/embed.js.map +1 -0
  59. package/dist/esm/emitter.d.ts +23 -0
  60. package/dist/esm/emitter.d.ts.map +1 -0
  61. package/dist/esm/emitter.js +47 -0
  62. package/dist/esm/emitter.js.map +1 -0
  63. package/dist/esm/errors.d.ts +16 -0
  64. package/dist/esm/errors.d.ts.map +1 -0
  65. package/dist/esm/errors.js +25 -0
  66. package/dist/esm/errors.js.map +1 -0
  67. package/dist/esm/events.d.ts +98 -0
  68. package/dist/esm/events.d.ts.map +1 -0
  69. package/dist/esm/events.js +90 -0
  70. package/dist/esm/events.js.map +1 -0
  71. package/dist/esm/index.d.ts +29 -0
  72. package/dist/esm/index.d.ts.map +1 -0
  73. package/dist/esm/index.js +29 -0
  74. package/dist/esm/index.js.map +1 -0
  75. package/dist/esm/livekit.d.ts +36 -0
  76. package/dist/esm/livekit.d.ts.map +1 -0
  77. package/dist/esm/livekit.js +54 -0
  78. package/dist/esm/livekit.js.map +1 -0
  79. package/dist/esm/log.d.ts +6 -0
  80. package/dist/esm/log.d.ts.map +1 -0
  81. package/dist/esm/log.js +13 -0
  82. package/dist/esm/log.js.map +1 -0
  83. package/dist/esm/react-native.d.ts +40 -0
  84. package/dist/esm/react-native.d.ts.map +1 -0
  85. package/dist/esm/react-native.js +26 -0
  86. package/dist/esm/react-native.js.map +1 -0
  87. package/dist/esm/reactNativeDesk.d.ts +54 -0
  88. package/dist/esm/reactNativeDesk.d.ts.map +1 -0
  89. package/dist/esm/reactNativeDesk.js +103 -0
  90. package/dist/esm/reactNativeDesk.js.map +1 -0
  91. package/dist/esm/redact.d.ts +22 -0
  92. package/dist/esm/redact.d.ts.map +1 -0
  93. package/dist/esm/redact.js +66 -0
  94. package/dist/esm/redact.js.map +1 -0
  95. package/dist/esm/rest.d.ts +173 -0
  96. package/dist/esm/rest.d.ts.map +1 -0
  97. package/dist/esm/rest.js +281 -0
  98. package/dist/esm/rest.js.map +1 -0
  99. package/dist/esm/session.d.ts +102 -0
  100. package/dist/esm/session.d.ts.map +1 -0
  101. package/dist/esm/session.js +155 -0
  102. package/dist/esm/session.js.map +1 -0
  103. package/dist/esm/singleFlight.d.ts +33 -0
  104. package/dist/esm/singleFlight.d.ts.map +1 -0
  105. package/dist/esm/singleFlight.js +46 -0
  106. package/dist/esm/singleFlight.js.map +1 -0
  107. package/dist/esm/url.d.ts +14 -0
  108. package/dist/esm/url.d.ts.map +1 -0
  109. package/dist/esm/url.js +37 -0
  110. package/dist/esm/url.js.map +1 -0
  111. package/dist/esm/web.d.ts +24 -0
  112. package/dist/esm/web.d.ts.map +1 -0
  113. package/dist/esm/web.js +23 -0
  114. package/dist/esm/web.js.map +1 -0
  115. package/dist/esm/ws.d.ts +135 -0
  116. package/dist/esm/ws.d.ts.map +1 -0
  117. package/dist/esm/ws.js +242 -0
  118. package/dist/esm/ws.js.map +1 -0
  119. package/package.json +117 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@hainiva/agent-sdk` are listed here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses
5
+ [Semantic Versioning](https://semver.org/). While the version is `0.x`, a minor
6
+ release (`0.2.0`) may change the API; a patch release (`0.1.1`) never does.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] - 2026-11-14
11
+
12
+ - `@hainiva/agent-sdk/embed`: `mountDesk(container, { agent, appId, baseUrl,
13
+ theme?, locale?, onEnded?, onDial?, onDialer?, onOpenUrl?, onMediaError? })`
14
+ shows the HAINIVA desk in a sandboxed iframe signed in with your agent
15
+ session and returns `{ destroy(), setTheme(), setLocale(), iframe }`. The
16
+ handshake checks `event.source` and the desk origin, posts to the desk origin
17
+ only, supplies and refreshes the desk token (single flight), and ends on a
18
+ refused session or when the desk does not load (`load_failed`). Exports the
19
+ protocol (`DeskToHost`, `HostToDesk`, `parseDeskMessage`), `DESK_SANDBOX`,
20
+ `DESK_ALLOW` and `deskUrl()`.
21
+ - `@hainiva/agent-sdk/react-native`: `<HainivaDesk agent appId baseUrl onDial …/>`
22
+ in a `react-native-webview` WebView (optional peer, or pass `WebView`):
23
+ bridge injected before the desk's scripts on the desk origin only,
24
+ `originWhitelist` limited to the desk, no `window.open` passthrough; calls
25
+ are handed to your app (`onDial`, `onDialer`).
26
+ - `agent.desk.session()` / `AgentRestClient.deskSession()`:
27
+ `POST /agent/v1/desk-session` → `AgentDeskSessionDto {token, expiresAt}`.
28
+ - Optional peers `react` and `react-native-webview` (only for `<HainivaDesk/>`).
29
+
30
+ ## [0.2.0] - 2026-11-14
31
+
32
+ - `HainivaAgent`: one object for a staff member's app — `connect()`, `me`,
33
+ `setStatus`, `rings.pending`, `calls.accept/decline/end/hold/mute/transfer`,
34
+ `joinMedia` (through the `/web` or `/react-native` LiveKit adapter),
35
+ `conversations.*`, `messages.*`, `wrapUp`, `dispositions`, `supervisor.*`,
36
+ `push.register/unregister`, `on(event)`, `disconnect({ signOut })`.
37
+ - `AgentRestClient`: every `/agent/v1` endpoint, typed with the Agent API
38
+ contract (`AgentMeDto`, `AgentAcceptDto`, …, exported). Errors are
39
+ `HainivaAgentError` from the `{error:{code,message}}` envelope. A 401
40
+ `token_expired` refreshes once (single flight) and retries; a refused refresh
41
+ signs in again with a new assertion; `session_revoked` is final. Every POST
42
+ carries an `Idempotency-Key` (reused by the retry). Timeouts and
43
+ `AbortSignal`. Ids are checked before they go into a path.
44
+ - `/agent/v1/ws` bindings (`agentSocketBindings`): auth first frame with the
45
+ resume position, `token.refresh`, `session.expiring` (refresh at once),
46
+ `resync_required` (rings refetched, `resync` event), close codes 4001 / 4003
47
+ / 4009. `call.*` events drive the `CallStore`.
48
+ - `SessionManager.invalidate(refusedToken)` ignores a refusal of an older token;
49
+ `SessionManager.clear()` forgets a session the server already ended.
50
+ - `ReconnectingSocket` `closeCodes.fatal`: close codes that stop reconnecting.
51
+
52
+ ## [0.1.0]
53
+
54
+ Not published to npm; its contents shipped in 0.2.0.
55
+
56
+ - `SessionManager`: sign-in with a fresh assertion from your backend every
57
+ time, pluggable `SessionStore` (memory by default), single-flight refresh
58
+ before expiry, new sign-in when a refresh is refused.
59
+ - `ReconnectingSocket`: first-frame auth, exponential backoff with jitter,
60
+ heartbeat with dead-connection timeout, `resume {lastSeq}` hook, duplicate
61
+ frame suppression, revoked / token-expired close codes, pluggable frames.
62
+ - `CallStore` / `transition`: the agent call state machine (ringing,
63
+ connecting, active, held, transferring, ended; concurrent rings; ring expiry).
64
+ - `TypedEmitter`, `singleFlight`, `createTokenRefresher`, `assertSafeUrl`,
65
+ `Redactor`, `HainivaAgentError` (redacted).
66
+ - `/web`: `localStorageStore` (opt-in), `joinCallRoom` (LiveKit).
67
+ - `/react-native`: `secureStoreSessionStore`, `joinCallRoom` (LiveKit).
68
+ - `/embed`: placeholder, throws.
69
+
70
+ [Unreleased]: https://github.com/hainiva/agent-sdk/compare/v0.3.0...HEAD
71
+ [0.3.0]: https://github.com/hainiva/agent-sdk/compare/v0.2.0...v0.3.0
72
+ [0.2.0]: https://github.com/hainiva/agent-sdk/compare/v0.1.0...v0.2.0
73
+ [0.1.0]: https://github.com/hainiva/agent-sdk/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ePacific Telecom (HAINIVA)
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 CHANGED
@@ -1,3 +1,279 @@
1
- # Temporary Holding Version
1
+ # @hainiva/agent-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build **your own staff app** on [HAINIVA AICC](https://hainiva.com): your staff
4
+ sign in with an assertion from your backend, receive their rings and
5
+ conversations over one reconnecting socket, and take calls with LiveKit — in a
6
+ browser, in React Native, or in Node.
7
+
8
+ > **Status: 0.x preview.** The API may change in a minor release until 1.0.
9
+
10
+ | Import | What | Needs |
11
+ | --- | --- | --- |
12
+ | `@hainiva/agent-sdk` | `HainivaAgent`, the REST client, the socket, the call state machine, redaction | nothing (no LiveKit, DOM or React Native) |
13
+ | `@hainiva/agent-sdk/web` | `joinCallRoom` (LiveKit), `localStorageStore` (opt-in) | `livekit-client` |
14
+ | `@hainiva/agent-sdk/react-native` | `joinCallRoom`, `secureStoreSessionStore`, `<HainivaDesk/>` | `livekit-client`, `@livekit/react-native`, `expo-secure-store`; `react-native-webview` for `<HainivaDesk/>` |
15
+ | `@hainiva/agent-sdk/embed` | `mountDesk()`: the full HAINIVA desk inside your web page | a browser |
16
+
17
+ Native ringing on phones (CallKit / Android call notification) is
18
+ [`@hainiva/react-native-call`](https://github.com/hainiva/react-native-call).
19
+ Samples: [`examples/`](./examples) — a Node assertion signer, an Expo staff
20
+ app and a web softphone.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ npm install @hainiva/agent-sdk livekit-client
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ ```ts
31
+ import { HainivaAgent } from "@hainiva/agent-sdk";
32
+ import { joinCallRoom } from "@hainiva/agent-sdk/web";
33
+
34
+ const agent = new HainivaAgent({
35
+ baseUrl: "https://api.hainiva.com",
36
+ tenantId: "YOUR_ORGANISATION_ID",
37
+ // Ask YOUR backend for a fresh assertion — every time (see below).
38
+ getAssertion: () => fetch("/my-backend/hainiva-assertion", { method: "POST", credentials: "include" })
39
+ .then((r) => r.json()).then((j) => j.assertion),
40
+ media: joinCallRoom,
41
+ });
42
+
43
+ agent.on("call.ringing", (r) => showRing(r.callId, r.contactName ?? r.callerNumber));
44
+ agent.on("calls", (state) => render(state)); // the call state machine
45
+ agent.on("wrapup.required", (w) => askForWrapUp(w.callId));
46
+ agent.on("revoked", () => showSignIn()); // HAINIVA ended the session
47
+
48
+ const me = await agent.connect(); // sign in, me, socket, rings waiting
49
+ await agent.setStatus("online");
50
+
51
+ await agent.calls.accept(callId); // answers and joins the room
52
+ await agent.calls.hold(callId, true);
53
+ await agent.calls.transfer(callId, { kind: "queue", queueId, name: "Support" });
54
+ await agent.calls.end(callId);
55
+ await agent.wrapUp(callId, { dispositionCode: "resolved", notes: "…" });
56
+
57
+ await agent.disconnect({ signOut: true });
58
+ ```
59
+
60
+ Everything else is on the same object: `conversations.list/get/assign/release/
61
+ transfer/close`, `messages.list/send/typing`, `dispositions()`,
62
+ `supervisor.liveCalls/agents/supervise/setMode`, `push.register/unregister`,
63
+ `rings.pending()`, `joinMedia(callId)` (re-join with a fresh LiveKit token).
64
+ `agent.rest` is the typed REST client for every `/agent/v1` endpoint, and the
65
+ response types (`AgentMeDto`, `AgentConversationDto`, …) are exported.
66
+
67
+ ## Sign-in: your backend vouches for your staff member
68
+
69
+ Your backend holds the agent app's **private key** (HAINIVA stores only the
70
+ public key, or reads your `jwksUrl`). After it has authenticated the staff
71
+ member with **your own** login, it signs a short assertion:
72
+
73
+ | | |
74
+ | --- | --- |
75
+ | header | `alg: "ES256"` (or `RS256`), `kid`: the key id you registered. `HS256` works only for a test app on a sandbox organisation. |
76
+ | `iss` | your agent app id |
77
+ | `sub` | the member's `externalId` (`PUT /v1/members/by-external-id/:externalId`) |
78
+ | `aud` | `hainiva-agent` |
79
+ | `jti` | unique: each assertion works once |
80
+ | `iat` / `exp` | now / at most 5 minutes ahead |
81
+
82
+ Roles never come from the assertion — HAINIVA uses the role set on the member.
83
+ The SDK calls `getAssertion` every time it needs a new session and never caches
84
+ or stores the assertion. See [`examples/node-signer`](./examples/node-signer)
85
+ for key generation and a complete signer.
86
+
87
+ **Sessions.** An access token lasts 15 minutes and is refreshed before it
88
+ expires (one refresh however many requests are waiting; the refresh token
89
+ rotates). A request answered `401 token_expired` refreshes and is retried once.
90
+ A refused refresh (expired, revoked, reused) signs in again with a new
91
+ assertion. `401 session_revoked` (signed out elsewhere, sessions revoked,
92
+ member suspended, role changed, app paused) ends this session: the SDK forgets
93
+ it and emits `revoked`.
94
+
95
+ **Revoking sessions does not cut a member off.** The next `connect()`, or the
96
+ embedded desk asking for a token, signs in again with a fresh assertion, and
97
+ your backend still vouches for the member, so it succeeds. To stop someone
98
+ working, suspend or delete the member (`PATCH /v1/members/:id
99
+ {"status":"suspended"}`, `DELETE /v1/members/:id`) or pause the agent app; then
100
+ sign-in is refused (`401 session_refused`, `403 agent_app_paused`). Stop
101
+ signing assertions for them on your side too.
102
+
103
+ ## Events
104
+
105
+ `agent.on(name, fn)` — from the event socket (only this member's events,
106
+ filtered by HAINIVA):
107
+
108
+ | Event | Data |
109
+ | --- | --- |
110
+ | `call.ringing` | `{callId, conversationId, path: direct\|queue\|transfer, channel, line, contactName, callerNumber, transferMode?, expiresAt}` |
111
+ | `call.ended` | `{callId \| null, conversationId, reason: ended\|failed\|answered_elsewhere\|declined\|expired\|transfer_ended}` |
112
+ | `conversation.assigned` | `{conversationId, callId, channel, contactName}` |
113
+ | `message.created` | `{conversationId, interactionId, channel, preview, contentType, senderName}` |
114
+ | `wrapup.required` | `{callId, conversationId, deadlineAt}` |
115
+ | `presence.changed` | `{callId, participantId, role, presence}` |
116
+ | `supervisor.alert` | the alert (supervisors) |
117
+ | `event.unknown` | an event name newer than this SDK |
118
+
119
+ And from the SDK itself: `connection` (socket state), `ready`, `calls` (the
120
+ call state), `callEffect` (stop a native ring, leave media — media is handled
121
+ for you), `resync` (events were missed beyond what HAINIVA keeps: rings are
122
+ refetched, refetch your own lists), `session.expiring`, `revoked`, `error`.
123
+
124
+ The socket authenticates in its **first frame** (the token is never in the
125
+ URL), resumes from the last event after a reconnect, sends a refreshed token
126
+ without reconnecting, and reconnects with backoff — except when HAINIVA revoked
127
+ the session or the member has too many sockets open.
128
+
129
+ ## Calls
130
+
131
+ `agent.callsState` / the `calls` event is the HAINIVA Agent app's call state
132
+ machine: one call at a time, concurrent rings, ring expiry, decline, taken
133
+ elsewhere, caller hanging up while you answer, hold, mute, warm and blind
134
+ transfer. `CallStore` and the pure `transition(state, event)` are exported for
135
+ Redux, Zustand or tests.
136
+
137
+ ## Push: rings with YOUR app's own credentials
138
+
139
+ Phones ring through **your** app with **your** Firebase / APNs credentials.
140
+
141
+ 1. **Upload them** on the agent app — Organization → Developer → Agent apps →
142
+ the bell, or `PUT /v1/agent-apps/:id/push-credentials` (key scope
143
+ `agent_apps:write`):
144
+ ```jsonc
145
+ {
146
+ "fcm": { "serviceAccount": { /* the service-account key file */ }, "expiresAt": "2027-11-01T00:00:00Z" },
147
+ "apns": { "keyP8": "<contents of AuthKey_XXXXXXXXXX.p8>", "keyId": "XXXXXXXXXX", "teamId": "YYYYYYYYYY",
148
+ "topic": "com.example.agentapp", "environment": "production" }
149
+ }
150
+ ```
151
+ HAINIVA checks each with Google / Apple before saving (a refusal names the
152
+ problem: `fcm_auth_failed`, `apns_auth_failed`, `apns_topic_not_allowed`, …),
153
+ stores it encrypted and never returns it — only a fingerprint, the status
154
+ and the expiry you declare (your admins are alerted 14 days before, and at
155
+ once if Google or Apple start refusing it). The APNs `topic` must be one of
156
+ the agent app's `allowedBundleIds`. `null` removes a platform.
157
+ 2. **Register each phone** after sign-in:
158
+ ```ts
159
+ await agent.push.register({ platform: "android", token: fcmToken, kind: "voip", bundleId: "com.example.agentapp", deviceId });
160
+ await agent.push.register({ platform: "ios", token: pushKitToken, kind: "voip", bundleId: "com.example.agentapp", deviceId });
161
+ ```
162
+ A phone stops ringing when its sign-in ends; call `push.unregister(token)`
163
+ before signing out anyway.
164
+ 3. **Show the ring** with `@hainiva/react-native-call` — Android: the FCM data
165
+ message `{type: "call", tenantId, interactionId, conversationId, path,
166
+ caller, line, expiresAt, transferMode?}` (all strings; `expiresAt` in Unix
167
+ milliseconds) and `{type: "call_cancel", tenantId, interactionId, reason}`;
168
+ iPhone: a VoIP push to `<topic>.voip` with the same fields, reported to
169
+ CallKit natively (ring ends come over the socket, not as a push). See
170
+ [`examples/expo-agent-app`](./examples/expo-agent-app).
171
+
172
+ ## Web
173
+
174
+ Add your page's exact origin to the agent app's `allowedOrigins`. HAINIVA
175
+ answers the browser's CORS preflight only for origins an agent app lists, and
176
+ checks every request against the origins of the app its session belongs to
177
+ (`403 origin_not_allowed`). A page on an origin that no app lists is refused at
178
+ the preflight, before any sign-in: `connect()` fails with a
179
+ `HainivaAgentError` of status `0` (`network_error`), which in the browser's
180
+ network panel is a CORS error.
181
+
182
+ The session is kept **in memory** by default: a reload signs in again with a
183
+ fresh assertion. `localStorageStore()` keeps it across reloads, but **any
184
+ script on your origin can read localStorage, refresh token included** — use it
185
+ only with a strict Content-Security-Policy and no third-party scripts.
186
+
187
+ ## React Native
188
+
189
+ ```ts
190
+ import * as SecureStore from "expo-secure-store";
191
+ import { joinCallRoom, secureStoreSessionStore } from "@hainiva/agent-sdk/react-native";
192
+ const agent = new HainivaAgent({ …, store: secureStoreSessionStore(SecureStore), media: joinCallRoom });
193
+ ```
194
+
195
+ The session lives in the iOS Keychain (readable after first unlock, so a ring
196
+ that wakes a locked phone finds it; never migrated to another device) or the
197
+ Android Keystore. There is no fallback to a plain store. Call
198
+ `registerGlobals()` from `@livekit/react-native` once at startup.
199
+
200
+ ## The embedded desk
201
+
202
+ Show the whole HAINIVA desk inside your product, signed in as your staff
203
+ member with **your** agent session — no HAINIVA login screen, no password.
204
+
205
+ ```ts
206
+ import { mountDesk } from "@hainiva/agent-sdk/embed";
207
+
208
+ await agent.connect();
209
+ const desk = mountDesk(document.getElementById("desk")!, {
210
+ agent, // your connected HainivaAgent
211
+ appId: "YOUR_AGENT_APP_ID",
212
+ baseUrl: "https://app.hainiva.com", // the HAINIVA desk
213
+ theme: "light", locale: "vi",
214
+ onEnded: (reason) => showSignedOut(reason),
215
+ });
216
+ desk.setTheme("dark");
217
+ desk.destroy();
218
+ ```
219
+
220
+ - **Your page's origin must be in the agent app's `allowedOrigins`.** That list
221
+ is also the desk's CSP `frame-ancestors`: any other site gets a blank frame
222
+ (and `onEnded("load_failed")`). Adding an origin lets that site embed the desk
223
+ for this app's staff — list only sites you control.
224
+ - The SDK exchanges your agent session for a 15-minute **desk token**
225
+ (`POST /agent/v1/desk-session`, `agent.desk.session()`) and hands it to the
226
+ desk over `postMessage` — never in the URL, never stored. The desk asks for a
227
+ new one before it expires. When your session ends for good (sign-out,
228
+ member suspended or deleted, app paused) the desk shows "ended" at once and
229
+ `onEnded` fires. Revoking sessions alone does not end the desk: the SDK signs
230
+ in again with a fresh assertion (see Sessions above).
231
+ - The desk token works only for the desk's own work (conversations, calls,
232
+ contacts, tickets…). Settings, team, billing, developer and admin pages are
233
+ refused, and it can never switch organisation.
234
+ - The iframe is created with
235
+ `sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals"`
236
+ (never top navigation: the desk cannot navigate your page) and
237
+ `allow="microphone; camera; autoplay; clipboard-write"`. On the web **the desk
238
+ carries calls itself (WebRTC)**; if your own iframe or Permissions-Policy does
239
+ not delegate the microphone, or the person refuses it, `onMediaError(name)`
240
+ tells you.
241
+ - Messages are read only from that iframe's window **and** the desk origin, and
242
+ only in the documented shapes; everything is sent to the desk origin only.
243
+
244
+ React Native — calls stay native in your app (`@hainiva/react-native-call`),
245
+ the desk hands them to you:
246
+
247
+ ```tsx
248
+ import { WebView } from "react-native-webview";
249
+ import { HainivaDesk } from "@hainiva/agent-sdk/react-native";
250
+
251
+ <HainivaDesk agent={agent} appId={APP_ID} baseUrl="https://app.hainiva.com" WebView={WebView}
252
+ onDial={(number, contactId) => placeCall(number, contactId)}
253
+ onDialer={(prefill) => openDialer(prefill)}
254
+ onOpenUrl={(url) => Linking.openURL(url)}
255
+ onEnded={(reason) => signOut(reason)} />
256
+ ```
257
+
258
+ The WebView stays on the desk origin (`originWhitelist`, every other URL goes
259
+ to `onOpenUrl`), opens no windows, and gets its bridge before the desk's
260
+ scripts, on the desk origin only.
261
+
262
+ ## Security
263
+
264
+ - **No tokens in URLs or logs.** Tokens travel in the `Authorization` header or
265
+ the socket's first frame; configured URLs are checked (TLS only, no
266
+ `user:pass@`, no credential-like query parameter). Errors and the `log`
267
+ callback are redacted (JWTs, Bearer values, token parameters, HAINIVA key
268
+ prefixes and the exact tokens the SDK holds). The SDK never writes to the
269
+ console.
270
+ - **Never put the private key in an app.** Only your backend signs assertions.
271
+ Rotate keys with `POST /v1/agent-apps/:id/rotate-credential` (the old key
272
+ keeps working 24 h; `immediate: true` when it leaked) and push credentials by
273
+ uploading new ones.
274
+ - Every POST carries an `Idempotency-Key`, so a retry never acts twice.
275
+ - See [SECURITY.md](./SECURITY.md).
276
+
277
+ ## License
278
+
279
+ [MIT](./LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,33 @@
1
+ # Security policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please **do not open a public issue**. Email **security@epacific.com.vn** with a
6
+ description, the affected version and steps to reproduce. We acknowledge
7
+ reports within 2 business days and keep you informed until a fix ships.
8
+
9
+ ## Supported versions
10
+
11
+ The latest minor release receives security fixes.
12
+
13
+ ## Using the SDK safely
14
+
15
+ - **Never put the agent app's private key (or a test secret) in the app.**
16
+ Only your backend signs staff assertions, after it has authenticated the
17
+ staff member itself. Keep assertions short-lived (≤ 5 minutes) with a new
18
+ `jti` each time; live apps use ES256/RS256.
19
+ - `getAssertion` must fetch a new assertion every time — do not cache one.
20
+ - Web: keep the default in-memory session store unless you need reloads to stay
21
+ signed in; `localStorageStore` exposes the refresh token to any script on
22
+ your origin.
23
+ - React Native: use `secureStoreSessionStore`; never AsyncStorage for tokens.
24
+ - Push credentials (Firebase service account, APNs .p8) are uploaded to
25
+ HAINIVA once, by your admin or your backend — never shipped in the app, never
26
+ committed. HAINIVA stores them encrypted and shows only fingerprints; upload
27
+ new ones to rotate, and remove them (`null`) if they leak.
28
+ - Unregister a phone's push token before signing out on it.
29
+ - Do not log raw socket frames or API bodies in production: they carry
30
+ customer data. The SDK's own `log` callback is already redacted.
31
+ - Every release on npm is published from GitHub Actions with
32
+ [provenance](https://docs.npmjs.com/generating-provenance-statements); check it
33
+ with `npm audit signatures`.