@happyrobot-ai/sdk 0.1.16 → 0.1.17

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/README.md CHANGED
@@ -789,6 +789,8 @@ const client = new HappyRobotClient({ apiKey: process.env.HAPPYROBOT_API_KEY });
789
789
  app.post("/api/chat-token", async (req, res) => {
790
790
  const { token, expires_at } = await client.chat.createToken({
791
791
  workflow_id: "your-workflow-id",
792
+ // Optional. Token lifetime in seconds. Default 3600 (1 hour), min 60, max 86400.
793
+ // ttl_seconds: 1800,
792
794
  });
793
795
  res.json({ token, expires_at });
794
796
  });
@@ -825,8 +827,17 @@ const connection = chat.connect(session_id, {
825
827
  onSessionClosed: (event) => {
826
828
  // Session ended: event.status, event.reason, event.duration
827
829
  },
830
+ // Auto-refresh: ~30s before the token expires the SDK calls this and
831
+ // forwards the result over the WebSocket. The connection stays open.
832
+ getToken: async () => {
833
+ const { token } = await fetch("/api/chat-token", { method: "POST" }).then(
834
+ (r) => r.json()
835
+ );
836
+ return token;
837
+ },
828
838
  onTokenExpired: () => {
829
- // Token expired fetch a new token from your server and reconnect
839
+ // Backstop: only fires if no valid refresh arrived in time
840
+ // (e.g. getToken not provided, network failure). Reconnect from scratch.
830
841
  },
831
842
  });
832
843
 
@@ -890,9 +901,9 @@ await connection.sendMessage({
890
901
 
891
902
  ### `HappyRobotClient` (server-side)
892
903
 
893
- | Method | Description |
894
- | ------------------------------------------------ | -------------------------------------------- |
895
- | `client.chat.createToken({ workflow_id, env? })` | Create a scoped client token (1 hour expiry) |
904
+ | Method | Description |
905
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
906
+ | `client.chat.createToken({ workflow_id, env?, ttl_seconds? })` | Create a scoped client token. `ttl_seconds` defaults to 3600 (1 hour). |
896
907
 
897
908
  ### `HappyRobotChatClient` (browser-side)
898
909
 
@@ -918,23 +929,39 @@ await connection.sendMessage({
918
929
 
919
930
  **Server → Client:**
920
931
 
921
- | Event | Fields | Description |
922
- | ---------------- | --------------------------------------------------------- | ------------------------------------------ |
923
- | `connected` | `session_id` | Connection established |
924
- | `response-start` | `content` | AI started generating a response |
925
- | `response-chunk` | `content` | Partial response text |
926
- | `response-end` | `content` | Complete response text |
927
- | `session-closed` | `session_id`, `status`, `reason`, `duration`, `timestamp` | Session ended |
928
- | `token-expired` | — | JWT expired reconnect with a fresh token |
929
- | `message-ack` | `id`, `message` | Server confirmed the message was sent |
930
- | `message-error` | `id`, `error` | Server failed to send the message |
931
- | `heartbeat` | — | Keep-alive (every 15s) |
932
+ | Event | Fields | Description |
933
+ | ------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------------- |
934
+ | `connected` | `session_id` | Connection established |
935
+ | `response-start` | `content` | AI started generating a response |
936
+ | `response-chunk` | `content` | Partial response text |
937
+ | `response-end` | `content` | Complete response text |
938
+ | `session-closed` | `session_id`, `status`, `reason`, `duration`, `timestamp` | Session ended |
939
+ | `token-refresh-required` | — | Sent ~30s before exp. SDK handles automatically when `getToken` is provided |
940
+ | `token-refresh-ack` | `expires_at` | Server accepted the refreshed token; connection lifetime extended |
941
+ | `token-refresh-error` | `error` | Refresh token was invalid or scope-mismatched; previous token still in force |
942
+ | `token-expired` | — | Backstop: token expired without a valid refresh, connection is closing |
943
+ | `message-ack` | `id`, `message` | Server confirmed the message was sent |
944
+ | `message-error` | `id`, `error` | Server failed to send the message |
945
+ | `heartbeat` | — | Keep-alive (every 15s) |
932
946
 
933
947
  **Client → Server:**
934
948
 
935
- | Event | Fields | Description |
936
- | --------- | ------------------------------ | ----------------------------------------------------- |
937
- | `message` | `content`, `artifacts?`, `id?` | Send a user message. `id` is echoed back in ack/error |
949
+ | Event | Fields | Description |
950
+ | --------------- | ------------------------------ | ------------------------------------------------------------------------------------------------- |
951
+ | `message` | `content`, `artifacts?`, `id?` | Send a user message. `id` is echoed back in ack/error |
952
+ | `token-refresh` | `token` | Provide a fresh token to extend the connection. Sent automatically by the SDK when `getToken` set |
953
+
954
+ ### Token refresh
955
+
956
+ Tokens have a finite lifetime (default 1 hour, configurable via `ttl_seconds` on `createToken`). To keep long-lived connections alive without forcing the client to reconnect, the SDK supports in-band refresh:
957
+
958
+ 1. ~30s before the active token expires, the server sends `token-refresh-required`.
959
+ 2. The SDK calls your `getToken` handler to fetch a fresh token from your backend.
960
+ 3. The SDK sends `{ type: "token-refresh", token }` over the WebSocket.
961
+ 4. The server verifies the new token (scope must match: same `workflow_id`, `org`, `env`) and replies with `token-refresh-ack`. `onTokenRefreshed(expiresAt)` fires on the client.
962
+ 5. If no valid refresh arrives by exp, the server sends `token-expired` and closes the connection — `onTokenExpired` fires as a backstop.
963
+
964
+ If you don't provide `getToken`, refreshes are skipped and the connection will close at `exp` (legacy behavior). When the same token is shared across multiple concurrent connections, each connection refreshes independently — they diverge to per-connection tokens after the first refresh.
938
965
 
939
966
  ---
940
967
 
@@ -957,6 +984,10 @@ app.post("/api/voice-token", async (req, res) => {
957
984
  const result = await client.voice.createToken({
958
985
  workflow_id: "your-workflow-id",
959
986
  data: { customer_name: "John" }, // optional — passed to the agent
987
+ // Optional. LiveKit token lifetime in seconds.
988
+ // Default 21600 (6 hours), min 60, max 86400 (24 hours).
989
+ // LiveKit does not refresh tokens on an active call.
990
+ // ttl_seconds: 3600,
960
991
  });
961
992
  res.json(result); // { url, token, room_name, run_id }
962
993
  });
@@ -1008,9 +1039,9 @@ await connection.disconnect();
1008
1039
 
1009
1040
  ### `HappyRobotClient` (server-side)
1010
1041
 
1011
- | Method | Description |
1012
- | -------------------------------------------------------- | -------------------------------------- |
1013
- | `client.voice.createToken({ workflow_id, data?, env? })` | Create a LiveKit token for voice calls |
1042
+ | Method | Description |
1043
+ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
1044
+ | `client.voice.createToken({ workflow_id, data?, env?, ttl_seconds? })` | Create a LiveKit token for voice calls. `ttl_seconds` defaults to 21600 (min 60, max 86400). |
1014
1045
 
1015
1046
  ### `HappyRobotVoiceClient` (browser-side)
1016
1047
 
package/chat-client.d.ts CHANGED
@@ -35,7 +35,25 @@ export interface ChatConnectionHandlers {
35
35
  onSessionClosed?: (event: ChatWsSessionClosedEvent) => void;
36
36
  /** Connection established. */
37
37
  onConnected?: (sessionId: string) => void;
38
- /** Token expired — reconnect with a fresh token to continue. */
38
+ /**
39
+ * Called when the server requests a fresh token before the current one
40
+ * expires. Provide an async function that fetches a new token from your
41
+ * backend; the SDK will send it over the WebSocket automatically and the
42
+ * connection stays open. If omitted, the connection will be closed at
43
+ * exp with `onTokenExpired`.
44
+ *
45
+ * Note: when one token is shared across multiple concurrent connections,
46
+ * each connection will refresh independently — they diverge to per-connection
47
+ * tokens after the first refresh.
48
+ */
49
+ getToken?: () => Promise<string>;
50
+ /** Token was successfully refreshed in-band. */
51
+ onTokenRefreshed?: (expiresAt: string) => void;
52
+ /**
53
+ * Token expired and was not refreshed in time — reconnect with a fresh
54
+ * token to continue. Acts as a backstop if `getToken` is not provided or
55
+ * fails to deliver a valid token before expiry.
56
+ */
39
57
  onTokenExpired?: () => void;
40
58
  /** WebSocket error. */
41
59
  onError?: (error: Event) => void;
package/chat-client.js CHANGED
@@ -146,6 +146,29 @@ class HappyRobotChatClient {
146
146
  case "session-closed":
147
147
  handlers.onSessionClosed?.(data);
148
148
  break;
149
+ case "token-refresh-required": {
150
+ const getToken = handlers.getToken;
151
+ if (!getToken)
152
+ break;
153
+ getToken()
154
+ .then((newToken) => {
155
+ if (ws.readyState === WebSocket.OPEN) {
156
+ ws.send(JSON.stringify({ type: "token-refresh", token: newToken }));
157
+ }
158
+ })
159
+ .catch(() => {
160
+ // Server's expiry timer is the backstop — onTokenExpired
161
+ // will fire if no valid refresh arrives in time.
162
+ });
163
+ break;
164
+ }
165
+ case "token-refresh-ack":
166
+ handlers.onTokenRefreshed?.(data.expires_at);
167
+ break;
168
+ case "token-refresh-error":
169
+ // Connection stays alive on the previous token until exp; the
170
+ // backstop will close it if no valid refresh arrives in time.
171
+ break;
149
172
  case "token-expired":
150
173
  handlers.onTokenExpired?.();
151
174
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyrobot-ai/sdk",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "TypeScript SDK for the HappyRobot Public API",
5
5
  "main": "./index.js",
6
6
  "module": "./index.mjs",
@@ -2,12 +2,12 @@
2
2
  * Adversarial test types extracted from Zod schemas.
3
3
  */
4
4
  import type { z } from "zod";
5
+ import type { EffectiveScopeApiSchema } from "../../routes/adversarial-tests/effective-scope/get";
5
6
  import type { GetAdversarialTestByIdResponseSchema } from "../../routes/adversarial-tests/get";
6
7
  import type { PatchAdversarialTestBodySchema, PatchAdversarialTestResponseSchema } from "../../routes/adversarial-tests/patch";
7
8
  import type { RunAdversarialTestBodySchema, RunAdversarialTestResponseSchema } from "../../routes/adversarial-tests/run/post";
8
9
  import type { AdversarialTestRunMessageSchema, GetAdversarialTestRunMessagesResponseSchema } from "../../routes/adversarial-tests/runs/:run_id/messages/get";
9
10
  import type { AuditRemarkApiSchema, AdversarialTestRunApiSchema, GetAdversarialTestRunsResponseSchema } from "../../routes/adversarial-tests/runs/get";
10
- import type { EffectiveScopeApiSchema } from "../../routes/adversarial-tests/effective-scope/get";
11
11
  import type { AdversarialTestApiSchema } from "../../routes/nodes/:node_id/adversarial-tests/get";
12
12
  export type AdversarialTest = z.infer<typeof AdversarialTestApiSchema>;
13
13
  export type AdversarialTestEffectiveScope = z.infer<typeof EffectiveScopeApiSchema>;
@@ -3,6 +3,8 @@ export interface CreateChatTokenBody {
3
3
  workflow_id: string;
4
4
  data?: Record<string, unknown>;
5
5
  env?: "production" | "staging" | "development";
6
+ /** Token lifetime in seconds. Defaults to 3600 (1 hour). Min 60s, max 24h. */
7
+ ttl_seconds?: number;
6
8
  }
7
9
  /** POST /chat/tokens response. */
8
10
  export interface CreateChatTokenResponse {
@@ -129,6 +131,17 @@ export interface ChatWsHeartbeatEvent {
129
131
  export interface ChatWsTokenExpiredEvent {
130
132
  type: "token-expired";
131
133
  }
134
+ export interface ChatWsTokenRefreshRequiredEvent {
135
+ type: "token-refresh-required";
136
+ }
137
+ export interface ChatWsTokenRefreshAckEvent {
138
+ type: "token-refresh-ack";
139
+ expires_at: string;
140
+ }
141
+ export interface ChatWsTokenRefreshErrorEvent {
142
+ type: "token-refresh-error";
143
+ error: string;
144
+ }
132
145
  export interface ChatWsMessageAckEvent {
133
146
  type: "message-ack";
134
147
  id?: string;
@@ -145,4 +158,4 @@ export interface ChatWsMessageErrorEvent {
145
158
  id?: string;
146
159
  error: string;
147
160
  }
148
- export type ChatWsEvent = ChatWsConnectedEvent | ChatWsResponseStartEvent | ChatWsResponseChunkEvent | ChatWsResponseEndEvent | ChatWsSessionClosedEvent | ChatWsHeartbeatEvent | ChatWsTokenExpiredEvent | ChatWsMessageAckEvent | ChatWsMessageErrorEvent;
161
+ export type ChatWsEvent = ChatWsConnectedEvent | ChatWsResponseStartEvent | ChatWsResponseChunkEvent | ChatWsResponseEndEvent | ChatWsSessionClosedEvent | ChatWsHeartbeatEvent | ChatWsTokenExpiredEvent | ChatWsTokenRefreshRequiredEvent | ChatWsTokenRefreshAckEvent | ChatWsTokenRefreshErrorEvent | ChatWsMessageAckEvent | ChatWsMessageErrorEvent;
@@ -3,6 +3,13 @@ export interface CreateVoiceTokenBody {
3
3
  workflow_id: string;
4
4
  data?: Record<string, unknown>;
5
5
  env?: "production" | "staging" | "development";
6
+ /**
7
+ * LiveKit token lifetime in seconds. Defaults to 21600 (6 hours) to match
8
+ * LiveKit's default. Min 60s, max 86400 (24h). LiveKit does not refresh
9
+ * tokens on an active call — the browser must reconnect if the call
10
+ * outlives the TTL.
11
+ */
12
+ ttl_seconds?: number;
6
13
  }
7
14
  /** POST /voice/tokens response. */
8
15
  export interface CreateVoiceTokenResponse {