@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
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deskBridgeScript = deskBridgeScript;
4
+ exports.deliverScript = deliverScript;
5
+ exports.HainivaDesk = HainivaDesk;
6
+ /**
7
+ * `<HainivaDesk/>` — the HAINIVA desk inside your React Native app.
8
+ *
9
+ * import { WebView } from "react-native-webview";
10
+ * import { HainivaDesk } from "@hainiva/agent-sdk/react-native";
11
+ *
12
+ * <HainivaDesk agent={agent} appId={APP_ID} baseUrl="https://app.hainiva.com"
13
+ * onDial={(number, contactId) => placeNativeCall(number, contactId)}
14
+ * onEnded={(reason) => goToSignIn(reason)} />
15
+ *
16
+ * The split is the HAINIVA Agent app's: YOUR app rings and carries call audio
17
+ * natively (@hainiva/react-native-call + this SDK's CallStore); the desk shows
18
+ * the work and hands calls to you (`onDial`, `onDialer`).
19
+ *
20
+ * Security:
21
+ * - `originWhitelist` and `onShouldStartLoadWithRequest` keep the WebView on
22
+ * the HAINIVA desk origin; any other URL is refused there (and offered to
23
+ * `onOpenUrl`, if you pass one);
24
+ * - no window.open passthrough (`setSupportMultipleWindows={false}`,
25
+ * `javaScriptCanOpenWindowsAutomatically={false}`);
26
+ * - the bridge is installed before the desk's scripts and ONLY when the page
27
+ * is on the desk origin; the desk token goes in by injectJavaScript, never
28
+ * in the URL, and is not written to storage.
29
+ */
30
+ const React = require("react");
31
+ const embed_js_1 = require("./embed.cjs");
32
+ /** Runs before the desk's scripts, on the desk origin only: the partner-rn bridge the desk looks for. */
33
+ function deskBridgeScript(deskOrigin, appId) {
34
+ return `(function(){if(location.origin!==${JSON.stringify(deskOrigin)})return;`
35
+ + `window.__HAINIVA_EMBED__={host:"partner-rn",appId:${JSON.stringify(appId.toLowerCase())},q:[],deliver:function(s){this.q.push(String(s));}};})();true;`;
36
+ }
37
+ /** A message to the desk, as a script: JSON twice (a string literal of the JSON), nothing interpolated raw. */
38
+ function deliverScript(m) {
39
+ return `window.__HAINIVA_EMBED__&&window.__HAINIVA_EMBED__.deliver(${JSON.stringify(JSON.stringify(m))});true;`;
40
+ }
41
+ function defaultWebView() {
42
+ const mod = require("react-native-webview");
43
+ const W = mod.WebView ?? mod.default;
44
+ if (!W)
45
+ throw new Error("@hainiva/agent-sdk: install react-native-webview (or pass the WebView prop) to use <HainivaDesk/>");
46
+ return W;
47
+ }
48
+ function HainivaDesk(props) {
49
+ const url = React.useMemo(() => (0, embed_js_1.deskUrl)(props.baseUrl, props.appId, props.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {}), [props.baseUrl, props.appId, props.allowInsecureLocalhost]);
50
+ const origin = url.origin;
51
+ const web = React.useRef(null);
52
+ const latest = React.useRef(props);
53
+ latest.current = props;
54
+ const host = React.useMemo(() => new embed_js_1.DeskHost(props.appId, props.agent, (m) => web.current?.injectJavaScript(deliverScript(m)), {
55
+ onDial: (n, c) => latest.current.onDial?.(n, c),
56
+ onDialer: (p) => latest.current.onDialer?.(p),
57
+ onEnded: (r) => latest.current.onEnded?.(r),
58
+ onOpenUrl: (u) => latest.current.onOpenUrl?.(u),
59
+ onMediaError: (n) => latest.current.onMediaError?.(n),
60
+ }),
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ [props.appId, props.agent, origin]);
63
+ React.useEffect(() => { if (props.theme && host.isReady)
64
+ host.setTheme(props.theme); }, [props.theme, host]);
65
+ React.useEffect(() => { if (props.locale && host.isReady)
66
+ host.setLocale(props.locale); }, [props.locale, host]);
67
+ // Unmounted: the desk drops its token.
68
+ React.useEffect(() => () => { if (!host.isEnded)
69
+ web.current?.injectJavaScript(deliverScript({ v: 1, type: "hainiva:end" })); }, [host]);
70
+ const W = props.WebView ?? defaultWebView();
71
+ return React.createElement(W, {
72
+ ref: web,
73
+ source: { uri: url.href },
74
+ style: props.style,
75
+ originWhitelist: [origin],
76
+ injectedJavaScriptBeforeContentLoaded: deskBridgeScript(origin, props.appId),
77
+ onMessage: (e) => {
78
+ // Only from the desk origin (a page elsewhere never gets the bridge, and is refused below anyway).
79
+ const from = e.nativeEvent.url;
80
+ if (typeof from === "string" && !from.startsWith(`${origin}/`))
81
+ return;
82
+ const wasReady = host.isReady;
83
+ host.receive(e.nativeEvent.data);
84
+ if (!wasReady && host.isReady) {
85
+ if (latest.current.theme)
86
+ host.setTheme(latest.current.theme);
87
+ if (latest.current.locale)
88
+ host.setLocale(latest.current.locale);
89
+ }
90
+ },
91
+ onShouldStartLoadWithRequest: (r) => {
92
+ if (r.url === "about:blank" || r.url.startsWith(`${origin}/`) || r.url.startsWith("blob:") || r.url.startsWith("data:"))
93
+ return true;
94
+ latest.current.onOpenUrl?.(r.url);
95
+ return false;
96
+ },
97
+ setSupportMultipleWindows: false,
98
+ javaScriptCanOpenWindowsAutomatically: false,
99
+ allowFileAccess: false,
100
+ allowUniversalAccessFromFileURLs: false,
101
+ mixedContentMode: "never",
102
+ domStorageEnabled: true,
103
+ sharedCookiesEnabled: false,
104
+ thirdPartyCookiesEnabled: false,
105
+ mediaPlaybackRequiresUserAction: false,
106
+ });
107
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `<HainivaDesk/>` — the HAINIVA desk inside your React Native app.
3
+ *
4
+ * import { WebView } from "react-native-webview";
5
+ * import { HainivaDesk } from "@hainiva/agent-sdk/react-native";
6
+ *
7
+ * <HainivaDesk agent={agent} appId={APP_ID} baseUrl="https://app.hainiva.com"
8
+ * onDial={(number, contactId) => placeNativeCall(number, contactId)}
9
+ * onEnded={(reason) => goToSignIn(reason)} />
10
+ *
11
+ * The split is the HAINIVA Agent app's: YOUR app rings and carries call audio
12
+ * natively (@hainiva/react-native-call + this SDK's CallStore); the desk shows
13
+ * the work and hands calls to you (`onDial`, `onDialer`).
14
+ *
15
+ * Security:
16
+ * - `originWhitelist` and `onShouldStartLoadWithRequest` keep the WebView on
17
+ * the HAINIVA desk origin; any other URL is refused there (and offered to
18
+ * `onOpenUrl`, if you pass one);
19
+ * - no window.open passthrough (`setSupportMultipleWindows={false}`,
20
+ * `javaScriptCanOpenWindowsAutomatically={false}`);
21
+ * - the bridge is installed before the desk's scripts and ONLY when the page
22
+ * is on the desk origin; the desk token goes in by injectJavaScript, never
23
+ * in the URL, and is not written to storage.
24
+ */
25
+ import * as React from "react";
26
+ import { type DeskCallbacks, type DeskTokenSource, type HostToDesk } from "./embed.cjs";
27
+ /** The parts of react-native-webview's WebView this component uses. */
28
+ export interface WebViewLike {
29
+ injectJavaScript(script: string): void;
30
+ }
31
+ type WebViewComponent = React.ComponentType<Record<string, unknown> & {
32
+ ref?: React.Ref<WebViewLike>;
33
+ }>;
34
+ export interface HainivaDeskProps extends DeskCallbacks {
35
+ agent: DeskTokenSource;
36
+ appId: string;
37
+ /** The HAINIVA desk, e.g. https://app.hainiva.com. */
38
+ baseUrl: string;
39
+ theme?: "light" | "dark";
40
+ locale?: "vi" | "en";
41
+ /** react-native-webview's WebView. Default: required from "react-native-webview". */
42
+ WebView?: WebViewComponent;
43
+ /** Passed to the WebView. */
44
+ style?: unknown;
45
+ /** Local development only (http://localhost desk). */
46
+ allowInsecureLocalhost?: boolean;
47
+ }
48
+ /** Runs before the desk's scripts, on the desk origin only: the partner-rn bridge the desk looks for. */
49
+ export declare function deskBridgeScript(deskOrigin: string, appId: string): string;
50
+ /** A message to the desk, as a script: JSON twice (a string literal of the JSON), nothing interpolated raw. */
51
+ export declare function deliverScript(m: HostToDesk): string;
52
+ export declare function HainivaDesk(props: HainivaDeskProps): React.ReactElement;
53
+ export {};
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ /**
3
+ * Redaction: no token, assertion or secret may leave the SDK in an error message,
4
+ * a log line or an event. Two layers:
5
+ * - patterns that catch credentials by shape (JWTs, Bearer headers, token query
6
+ * parameters, HAINIVA key prefixes), always on;
7
+ * - a Redactor that also knows the exact secrets this client holds right now
8
+ * (the current access and refresh tokens), so even an opaque token is caught.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.Redactor = void 0;
12
+ exports.redactString = redactString;
13
+ exports.redactValue = redactValue;
14
+ const PATTERNS = [
15
+ // JWT / JWS / JWE (assertions, access tokens, LiveKit tokens).
16
+ [/eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}(?:\.[A-Za-z0-9_-]*){1,3}/g, "[redacted-jwt]"],
17
+ // Authorization header values.
18
+ [/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [redacted]"],
19
+ // Credentials in a query string.
20
+ [/([?&#](?:access_token|refresh_token|id_token|token|assertion|ticket|api_key|apikey|key|secret|signature|sig|password)=)[^&#\s"']+/gi, "$1[redacted]"],
21
+ // HAINIVA API keys and refresh tokens by prefix.
22
+ [/\b(aicc_(?:live|test)_|hnv_|agr_|apr_)[A-Za-z0-9_-]{6,}/g, "$1[redacted]"],
23
+ ];
24
+ /** Object keys whose values are never shown. */
25
+ const SENSITIVE_KEY = /token|assertion|secret|password|passwd|authori[sz]ation|cookie|refresh|jwt|signature|api[-_]?key|private[-_]?key|credential/i;
26
+ /** Redact credential-shaped substrings. */
27
+ function redactString(s, known = []) {
28
+ let out = s;
29
+ for (const k of known)
30
+ if (k && k.length >= 8)
31
+ out = out.split(k).join("[redacted]");
32
+ for (const [re, by] of PATTERNS)
33
+ out = out.replace(re, by);
34
+ return out;
35
+ }
36
+ /** A deep copy with credentials removed: sensitive keys blanked, strings scrubbed, Errors flattened. */
37
+ function redactValue(v, known = [], depth = 0) {
38
+ const list = [...known];
39
+ if (typeof v === "string")
40
+ return redactString(v, list);
41
+ if (v === null || typeof v !== "object")
42
+ return v;
43
+ if (depth > 6)
44
+ return "[truncated]";
45
+ if (v instanceof Error) {
46
+ return { name: v.name, message: redactString(v.message, list), ...(v.stack ? { stack: redactString(v.stack, list) } : {}) };
47
+ }
48
+ if (Array.isArray(v))
49
+ return v.slice(0, 100).map((x) => redactValue(x, list, depth + 1));
50
+ const out = {};
51
+ for (const [k, x] of Object.entries(v)) {
52
+ out[k] = SENSITIVE_KEY.test(k) && x !== null && x !== undefined && x !== "" ? "[redacted]" : redactValue(x, list, depth + 1);
53
+ }
54
+ return out;
55
+ }
56
+ /** Knows the secrets one client holds right now, on top of the patterns. */
57
+ class Redactor {
58
+ constructor() {
59
+ this.secrets = new Set();
60
+ }
61
+ add(...secrets) { for (const s of secrets)
62
+ if (s)
63
+ this.secrets.add(s); }
64
+ remove(...secrets) { for (const s of secrets)
65
+ if (s)
66
+ this.secrets.delete(s); }
67
+ clear() { this.secrets.clear(); }
68
+ string(s) { return redactString(s, this.secrets); }
69
+ value(v) { return redactValue(v, this.secrets); }
70
+ }
71
+ exports.Redactor = Redactor;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Redaction: no token, assertion or secret may leave the SDK in an error message,
3
+ * a log line or an event. Two layers:
4
+ * - patterns that catch credentials by shape (JWTs, Bearer headers, token query
5
+ * parameters, HAINIVA key prefixes), always on;
6
+ * - a Redactor that also knows the exact secrets this client holds right now
7
+ * (the current access and refresh tokens), so even an opaque token is caught.
8
+ */
9
+ /** Redact credential-shaped substrings. */
10
+ export declare function redactString(s: string, known?: Iterable<string>): string;
11
+ /** A deep copy with credentials removed: sensitive keys blanked, strings scrubbed, Errors flattened. */
12
+ export declare function redactValue(v: unknown, known?: Iterable<string>, depth?: number): unknown;
13
+ /** Knows the secrets one client holds right now, on top of the patterns. */
14
+ export declare class Redactor {
15
+ private readonly secrets;
16
+ add(...secrets: (string | null | undefined)[]): void;
17
+ remove(...secrets: (string | null | undefined)[]): void;
18
+ clear(): void;
19
+ string(s: string): string;
20
+ value(v: unknown): unknown;
21
+ }
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentRestClient = void 0;
4
+ exports.newIdempotencyKey = newIdempotencyKey;
5
+ exports.errorFromResponse = errorFromResponse;
6
+ const errors_js_1 = require("./errors.cjs");
7
+ const log_js_1 = require("./log.cjs");
8
+ const redact_js_1 = require("./redact.cjs");
9
+ const url_js_1 = require("./url.cjs");
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ const TENANT = /^[A-Za-z0-9_-]{1,64}$/;
12
+ /** A random UUID v4: crypto.randomUUID where it exists (browsers, Node), else getRandomValues, else Math.random (Idempotency-Key only, not a secret). */
13
+ function newIdempotencyKey() {
14
+ const c = globalThis.crypto;
15
+ if (c?.randomUUID)
16
+ return c.randomUUID();
17
+ const b = new Uint8Array(16);
18
+ if (c?.getRandomValues)
19
+ c.getRandomValues(b);
20
+ else
21
+ for (let i = 0; i < 16; i++)
22
+ b[i] = Math.floor(Math.random() * 256);
23
+ b[6] = (b[6] & 0x0f) | 0x40;
24
+ b[8] = (b[8] & 0x3f) | 0x80;
25
+ const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
26
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
27
+ }
28
+ /** An id that goes into a path: a UUID, or the request is refused before it leaves (no path tricks). */
29
+ function id(v, what) {
30
+ if (typeof v !== "string" || !UUID.test(v))
31
+ throw new errors_js_1.HainivaAgentError(0, "invalid_id", { field: what });
32
+ return v.toLowerCase();
33
+ }
34
+ function isObject(v) {
35
+ return !!v && typeof v === "object" && !Array.isArray(v);
36
+ }
37
+ /** Pure: the error a non-2xx answer becomes. Exported for tests. */
38
+ function errorFromResponse(status, data, redactor, retryAfter) {
39
+ const e = isObject(data) ? data["error"] : undefined;
40
+ let code = status === 401 ? "unauthorized" : status === 403 ? "forbidden" : status === 404 ? "not_found" : status === 409 ? "conflict" : status === 429 ? "rate_limited" : status >= 500 ? "server_error" : "invalid_request";
41
+ let message;
42
+ if (isObject(e)) {
43
+ if (typeof e["code"] === "string" && e["code"])
44
+ code = e["code"].slice(0, 80);
45
+ if (typeof e["message"] === "string")
46
+ message = e["message"].slice(0, 500);
47
+ }
48
+ else if (typeof e === "string") {
49
+ message = e.slice(0, 500);
50
+ }
51
+ const body = {};
52
+ if (message)
53
+ body["message"] = message;
54
+ if (retryAfter && /^\d{1,6}$/.test(retryAfter))
55
+ body["retryAfterSec"] = Number(retryAfter);
56
+ return new errors_js_1.HainivaAgentError(status, code, body, redactor);
57
+ }
58
+ /**
59
+ * The Agent API over fetch. `tokens` is normally the SessionManager; the
60
+ * client's own `sessions` transport is what that SessionManager signs in with.
61
+ */
62
+ class AgentRestClient {
63
+ constructor(o, tokens) {
64
+ this.o = o;
65
+ // ── Sessions (the SessionManager's transport) ───────────────────────────────
66
+ this.sessions = {
67
+ exchange: (assertion) => this.raw("POST", "/agent/v1/sessions", {
68
+ body: { tenantId: this.o.tenantId, assertion, ...(this.o.device ? { device: this.o.device } : {}) },
69
+ }, null).then((s) => s),
70
+ refresh: (refreshToken) => this.raw("POST", "/agent/v1/sessions/refresh", {
71
+ body: { tenantId: this.o.tenantId, refreshToken },
72
+ }, null).then((s) => s),
73
+ revoke: async (session) => {
74
+ await this.raw("DELETE", "/agent/v1/sessions/current", {}, session.accessToken);
75
+ },
76
+ };
77
+ const u = (0, url_js_1.assertSafeUrl)(o.baseUrl, { kind: "http", ...(o.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {}) });
78
+ if (u.search || u.hash)
79
+ throw new errors_js_1.HainivaAgentError(0, "invalid_url", { hint: "baseUrl takes no query or fragment" });
80
+ if (typeof o.tenantId !== "string" || !TENANT.test(o.tenantId))
81
+ throw new errors_js_1.HainivaAgentError(0, "invalid_tenant_id");
82
+ this.baseUrl = u.origin + u.pathname.replace(/\/+$/, "");
83
+ const f = o.fetch ?? globalThis.fetch;
84
+ if (!f)
85
+ throw new errors_js_1.HainivaAgentError(0, "no_fetch", { hint: "pass options.fetch" });
86
+ this.fetchFn = f.bind(globalThis);
87
+ this.timeoutMs = o.timeoutMs ?? 15000;
88
+ this.redactor = o.redactor ?? new redact_js_1.Redactor();
89
+ this.log = (0, log_js_1.safeLogger)(o.log, this.redactor);
90
+ this.tokens = tokens ?? null;
91
+ }
92
+ /** Attach the token source after construction (the SessionManager needs `sessions` first). */
93
+ useTokens(tokens) { this.tokens = tokens; }
94
+ /** The wss:// URL of the event stream, derived from baseUrl (never from a server answer). */
95
+ get wsUrl() {
96
+ return `${this.baseUrl.replace(/^http/, "ws")}/agent/v1/ws`;
97
+ }
98
+ // ── The generic request ─────────────────────────────────────────────────────
99
+ /** An authenticated request: refresh-and-retry once on an expired token. */
100
+ async request(method, path, opts = {}) {
101
+ if (!this.tokens)
102
+ throw new errors_js_1.HainivaAgentError(0, "not_signed_in");
103
+ const idem = method === "POST" ? newIdempotencyKey() : undefined;
104
+ const token = await this.tokens.accessToken();
105
+ try {
106
+ return await this.raw(method, path, opts, token, idem);
107
+ }
108
+ catch (e) {
109
+ if (!(e instanceof errors_js_1.HainivaAgentError) || e.status !== 401 || (e.code !== "token_expired" && e.code !== "token_invalid"))
110
+ throw e;
111
+ this.log("access_token_refused", { code: e.code });
112
+ this.tokens.invalidate(token);
113
+ const next = await this.tokens.accessToken();
114
+ return this.raw(method, path, opts, next, idem);
115
+ }
116
+ }
117
+ async raw(method, path, opts, token, idem) {
118
+ let url = `${this.baseUrl}${path}`;
119
+ if (opts.query) {
120
+ const q = new URLSearchParams();
121
+ for (const [k, v] of Object.entries(opts.query))
122
+ if (v !== undefined && v !== "")
123
+ q.set(k, String(v));
124
+ const s = q.toString();
125
+ if (s)
126
+ url += `?${s}`;
127
+ }
128
+ const headers = { accept: "application/json" };
129
+ if (token) {
130
+ this.redactor.add(token);
131
+ headers["authorization"] = `Bearer ${token}`;
132
+ }
133
+ if (idem)
134
+ headers["idempotency-key"] = idem;
135
+ let body;
136
+ if (opts.body !== undefined) {
137
+ headers["content-type"] = "application/json";
138
+ body = JSON.stringify(opts.body);
139
+ }
140
+ const ctl = new AbortController();
141
+ const timeout = opts.timeoutMs ?? this.timeoutMs;
142
+ let timedOut = false;
143
+ const timer = setTimeout(() => { timedOut = true; ctl.abort(); }, timeout);
144
+ const onAbort = () => ctl.abort();
145
+ if (opts.signal) {
146
+ if (opts.signal.aborted) {
147
+ clearTimeout(timer);
148
+ throw new errors_js_1.HainivaAgentError(0, "aborted");
149
+ }
150
+ opts.signal.addEventListener("abort", onAbort, { once: true });
151
+ }
152
+ let res;
153
+ try {
154
+ res = await this.fetchFn(url, { method, headers, ...(body !== undefined ? { body } : {}), signal: ctl.signal });
155
+ }
156
+ catch (e) {
157
+ if (timedOut)
158
+ throw new errors_js_1.HainivaAgentError(0, "timeout", { timeoutMs: timeout });
159
+ if (opts.signal?.aborted)
160
+ throw new errors_js_1.HainivaAgentError(0, "aborted");
161
+ throw errors_js_1.HainivaAgentError.from(e, "network_error", this.redactor);
162
+ }
163
+ finally {
164
+ clearTimeout(timer);
165
+ opts.signal?.removeEventListener("abort", onAbort);
166
+ }
167
+ if (res.status === 204)
168
+ return undefined;
169
+ let data = null;
170
+ const text = await res.text().catch(() => "");
171
+ if (text) {
172
+ try {
173
+ data = JSON.parse(text);
174
+ }
175
+ catch {
176
+ data = null;
177
+ }
178
+ }
179
+ if (!res.ok)
180
+ throw errorFromResponse(res.status, data, this.redactor, res.headers?.get?.("retry-after") ?? null);
181
+ if (data === null || typeof data !== "object")
182
+ throw new errors_js_1.HainivaAgentError(res.status, "invalid_response", {}, this.redactor);
183
+ return data;
184
+ }
185
+ // ── Me ──────────────────────────────────────────────────────────────────────
186
+ async me(o) { return this.request("GET", "/agent/v1/me", o); }
187
+ async setStatus(status, o) {
188
+ return this.request("PUT", "/agent/v1/me/status", { ...o, body: { status } });
189
+ }
190
+ /** Rings for this member now (after a cold start, a missed push or a resync). */
191
+ async pendingRings(o) {
192
+ const r = await this.request("GET", "/agent/v1/rings/pending", o);
193
+ return Array.isArray(r.data) ? r.data : [];
194
+ }
195
+ // ── Calls ───────────────────────────────────────────────────────────────────
196
+ async getCall(callId, o) { return this.request("GET", `/agent/v1/calls/${id(callId, "callId")}`, o); }
197
+ async acceptCall(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/accept`, { ...o, body: {} }); }
198
+ async declineCall(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/decline`, { ...o, body: {} }); }
199
+ async endCall(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/end`, { ...o, body: {} }); }
200
+ async holdCall(callId, on, o) {
201
+ return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/hold`, { ...o, body: { on } });
202
+ }
203
+ async transferCall(callId, target, o) {
204
+ let body;
205
+ if ("agentId" in target)
206
+ body = { targetAgentId: id(target.agentId, "agentId"), mode: target.mode ?? "warm" };
207
+ else if ("queueId" in target)
208
+ body = { queueId: id(target.queueId, "queueId") };
209
+ else
210
+ body = { number: target.number, mode: target.mode ?? "warm" };
211
+ return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/transfer`, { ...o, body });
212
+ }
213
+ /**
214
+ * A 15-minute desk token for the embedded HAINIVA desk (S4): give it to the
215
+ * desk over postMessage only (`@hainiva/agent-sdk/embed` does). Never put it
216
+ * in a URL or storage. Ends with this session.
217
+ */
218
+ async deskSession(o) { return this.request("POST", "/agent/v1/desk-session", { ...o, body: {} }); }
219
+ /** Re-mint the LiveKit token while still a participant (reconnect, token near expiry). */
220
+ async mediaToken(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/media-token`, { ...o, body: {} }); }
221
+ // ── Wrap-up ─────────────────────────────────────────────────────────────────
222
+ async dispositions(channel, o) {
223
+ const r = await this.request("GET", "/agent/v1/dispositions", { ...o, ...(channel ? { query: { channel } } : {}) });
224
+ return Array.isArray(r.data) ? r.data : [];
225
+ }
226
+ async wrapUp(callId, w = {}, o) {
227
+ return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/wrapup`, { ...o, body: w });
228
+ }
229
+ // ── Conversations and messages ──────────────────────────────────────────────
230
+ async listConversations(q = {}, o) {
231
+ return this.request("GET", "/agent/v1/conversations", { ...o, query: { scope: q.scope, status: q.status, cursor: q.cursor, limit: q.limit } }).then((page));
232
+ }
233
+ async getConversation(conversationId, o) {
234
+ return this.request("GET", `/agent/v1/conversations/${id(conversationId, "conversationId")}`, o);
235
+ }
236
+ /** Claim it (no agentId), or — a supervisor — assign it to a colleague. */
237
+ async assignConversation(conversationId, agentId, o) {
238
+ return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/assign`, { ...o, body: agentId ? { agentId: id(agentId, "agentId") } : {} });
239
+ }
240
+ async releaseConversation(conversationId, note, o) {
241
+ return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/release`, { ...o, body: note ? { note } : {} });
242
+ }
243
+ async transferConversation(conversationId, to, o) {
244
+ const body = "agentId" in to ? { agentId: id(to.agentId, "agentId") } : { queueId: id(to.queueId, "queueId"), ...(to.note ? { note: to.note } : {}) };
245
+ return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/transfer`, { ...o, body });
246
+ }
247
+ async closeConversation(conversationId, o) {
248
+ return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/close`, { ...o, body: {} });
249
+ }
250
+ async listMessages(conversationId, q = {}, o) {
251
+ return this.request("GET", `/agent/v1/conversations/${id(conversationId, "conversationId")}/messages`, { ...o, query: { after: q.after, limit: q.limit } }).then((page));
252
+ }
253
+ async sendMessage(conversationId, text, o) {
254
+ return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/messages`, { ...o, body: { text } });
255
+ }
256
+ async typing(conversationId, o) {
257
+ return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/typing`, { ...o, body: {} });
258
+ }
259
+ // ── Supervisor ──────────────────────────────────────────────────────────────
260
+ async liveCalls(o) {
261
+ const r = await this.request("GET", "/agent/v1/supervisor/live-calls", o);
262
+ return Array.isArray(r.data) ? r.data : [];
263
+ }
264
+ async teamAgents(o) {
265
+ const r = await this.request("GET", "/agent/v1/supervisor/agents", o);
266
+ return Array.isArray(r.data) ? r.data : [];
267
+ }
268
+ async supervise(callId, mode = "monitor", o) {
269
+ return this.request("POST", `/agent/v1/supervisor/calls/${id(callId, "callId")}/supervise`, { ...o, body: { mode } });
270
+ }
271
+ async setSuperviseMode(callId, mode, supervisorTrackSid, o) {
272
+ return this.request("PUT", `/agent/v1/supervisor/calls/${id(callId, "callId")}/supervise-mode`, { ...o, body: { mode, ...(supervisorTrackSid ? { supervisorTrackSid } : {}) } });
273
+ }
274
+ // ── Push (your app's own FCM / APNs credentials, uploaded by your admin) ─────
275
+ async registerPush(sub, o) {
276
+ return this.request("POST", "/agent/v1/push/subscriptions", { ...o, body: sub });
277
+ }
278
+ async unregisterPush(token, o) {
279
+ await this.request("DELETE", "/agent/v1/push/subscriptions", { ...o, body: { token } });
280
+ }
281
+ }
282
+ exports.AgentRestClient = AgentRestClient;
283
+ function page(r) {
284
+ const x = isObject(r) ? r : {};
285
+ return { data: Array.isArray(x["data"]) ? x["data"] : [], nextCursor: typeof x["nextCursor"] === "string" ? x["nextCursor"] : null };
286
+ }