@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,172 @@
1
+ /**
2
+ * The REST client for the Agent API (`/agent/v1/*`).
3
+ *
4
+ * - `baseUrl` is checked with assertSafeUrl (HTTPS only, no credential in it);
5
+ * - the access token goes in the Authorization header only — never in a URL;
6
+ * - a 401 `token_expired` (or `token_invalid`) refreshes ONCE (single flight
7
+ * across every caller: SessionManager) and retries the request once; a
8
+ * refused refresh signs in again with a NEW assertion from `getAssertion`;
9
+ * a 401 `session_revoked` is final (the facade emits `revoked`);
10
+ * - every POST carries an `Idempotency-Key` (the retry after a refresh reuses
11
+ * it, so the server never does the action twice);
12
+ * - each request has a timeout (default 15 s) and honours an AbortSignal;
13
+ * - any non-2xx becomes a HainivaAgentError {status, code, body} from the
14
+ * server's `{error:{code,message}}` envelope, redacted; non-JSON or
15
+ * non-object bodies are handled defensively.
16
+ */
17
+ import type { AgentAcceptDto, AgentCallDto, AgentConversationDto, AgentDispositionDto, AgentMeDto, AgentMediaTokenDto, AgentMessageDto, AgentPage, AgentPresenceStatus, AgentPushSubscriptionDto, AgentPushSubscriptionInput, AgentRingDto, AgentDeskSessionDto, AgentSessionDto, SupervisorAgentDto, SupervisorLiveCallDto, SupervisorSuperviseDto } from "./contract.cjs";
18
+ import { HainivaAgentError } from "./errors.cjs";
19
+ import { type Logger } from "./log.cjs";
20
+ import { Redactor } from "./redact.cjs";
21
+ import type { AgentSession, SessionTransport } from "./session.cjs";
22
+ export interface AgentRestOptions {
23
+ /** e.g. https://api.hainiva.com (HTTPS only). */
24
+ baseUrl: string;
25
+ /** Your HAINIVA organisation id: sign-in and refresh name it. */
26
+ tenantId: string;
27
+ fetch?: typeof fetch;
28
+ /** Per request, default 15 000 ms. */
29
+ timeoutMs?: number;
30
+ /** Local development only: http://localhost. */
31
+ allowInsecureLocalhost?: boolean;
32
+ /** Device shown to the organisation's admins in the session list. */
33
+ device?: {
34
+ label?: string;
35
+ platform?: "ios" | "android" | "web" | "other";
36
+ };
37
+ redactor?: Redactor;
38
+ log?: Logger;
39
+ }
40
+ /** Where the client gets its access token, and how it reports a refused one. */
41
+ export interface TokenSource {
42
+ accessToken(): Promise<string>;
43
+ /** The server refused this token: refresh before the next request. */
44
+ invalidate(refused?: string): void;
45
+ }
46
+ export interface RequestOptions {
47
+ body?: unknown;
48
+ query?: Record<string, string | number | undefined>;
49
+ signal?: AbortSignal;
50
+ /** Overrides the client's timeout for this request. */
51
+ timeoutMs?: number;
52
+ }
53
+ /** LiveKit room credentials for a call. */
54
+ export type MediaCredentials = AgentMediaTokenDto;
55
+ export type AgentStatus = AgentPresenceStatus;
56
+ /** Where a call is transferred. */
57
+ export type CallTransferTarget = {
58
+ agentId: string;
59
+ mode?: "warm" | "blind";
60
+ } | {
61
+ queueId: string;
62
+ } | {
63
+ number: string;
64
+ mode?: "warm" | "blind";
65
+ };
66
+ export interface ConversationListQuery {
67
+ scope?: "all" | "mine";
68
+ status?: "open" | "closed" | "all";
69
+ cursor?: string;
70
+ limit?: number;
71
+ }
72
+ /** A random UUID v4: crypto.randomUUID where it exists (browsers, Node), else getRandomValues, else Math.random (Idempotency-Key only, not a secret). */
73
+ export declare function newIdempotencyKey(): string;
74
+ /** Pure: the error a non-2xx answer becomes. Exported for tests. */
75
+ export declare function errorFromResponse(status: number, data: unknown, redactor?: Redactor, retryAfter?: string | null): HainivaAgentError;
76
+ /**
77
+ * The Agent API over fetch. `tokens` is normally the SessionManager; the
78
+ * client's own `sessions` transport is what that SessionManager signs in with.
79
+ */
80
+ export declare class AgentRestClient {
81
+ private readonly o;
82
+ readonly baseUrl: string;
83
+ private readonly fetchFn;
84
+ private readonly timeoutMs;
85
+ private readonly redactor;
86
+ private readonly log;
87
+ private tokens;
88
+ constructor(o: AgentRestOptions, tokens?: TokenSource);
89
+ /** Attach the token source after construction (the SessionManager needs `sessions` first). */
90
+ useTokens(tokens: TokenSource): void;
91
+ /** The wss:// URL of the event stream, derived from baseUrl (never from a server answer). */
92
+ get wsUrl(): string;
93
+ readonly sessions: SessionTransport<AgentSession & AgentSessionDto>;
94
+ /** An authenticated request: refresh-and-retry once on an expired token. */
95
+ request<T>(method: "GET" | "POST" | "PUT" | "DELETE", path: string, opts?: RequestOptions): Promise<T>;
96
+ private raw;
97
+ me(o?: RequestOptions): Promise<AgentMeDto>;
98
+ setStatus(status: AgentStatus, o?: RequestOptions): Promise<{
99
+ status: AgentStatus;
100
+ }>;
101
+ /** Rings for this member now (after a cold start, a missed push or a resync). */
102
+ pendingRings(o?: RequestOptions): Promise<AgentRingDto[]>;
103
+ getCall(callId: string, o?: RequestOptions): Promise<AgentCallDto>;
104
+ acceptCall(callId: string, o?: RequestOptions): Promise<AgentAcceptDto>;
105
+ declineCall(callId: string, o?: RequestOptions): Promise<{
106
+ callId: string;
107
+ declined: true;
108
+ }>;
109
+ endCall(callId: string, o?: RequestOptions): Promise<{
110
+ callId: string;
111
+ ended: true;
112
+ }>;
113
+ holdCall(callId: string, on: boolean, o?: RequestOptions): Promise<{
114
+ callId: string;
115
+ onHold: boolean;
116
+ }>;
117
+ transferCall(callId: string, target: CallTransferTarget, o?: RequestOptions): Promise<{
118
+ callId: string;
119
+ transferred: true;
120
+ }>;
121
+ /**
122
+ * A 15-minute desk token for the embedded HAINIVA desk (S4): give it to the
123
+ * desk over postMessage only (`@hainiva/agent-sdk/embed` does). Never put it
124
+ * in a URL or storage. Ends with this session.
125
+ */
126
+ deskSession(o?: RequestOptions): Promise<AgentDeskSessionDto>;
127
+ /** Re-mint the LiveKit token while still a participant (reconnect, token near expiry). */
128
+ mediaToken(callId: string, o?: RequestOptions): Promise<MediaCredentials>;
129
+ dispositions(channel?: string, o?: RequestOptions): Promise<AgentDispositionDto[]>;
130
+ wrapUp(callId: string, w?: {
131
+ dispositionCode?: string;
132
+ notes?: string;
133
+ }, o?: RequestOptions): Promise<{
134
+ callId: string;
135
+ wrappedUp: true;
136
+ }>;
137
+ listConversations(q?: ConversationListQuery, o?: RequestOptions): Promise<AgentPage<AgentConversationDto>>;
138
+ getConversation(conversationId: string, o?: RequestOptions): Promise<AgentConversationDto>;
139
+ /** Claim it (no agentId), or — a supervisor — assign it to a colleague. */
140
+ assignConversation(conversationId: string, agentId?: string, o?: RequestOptions): Promise<unknown>;
141
+ releaseConversation(conversationId: string, note?: string, o?: RequestOptions): Promise<{
142
+ conversationId: string;
143
+ released: true;
144
+ }>;
145
+ transferConversation(conversationId: string, to: {
146
+ agentId: string;
147
+ } | {
148
+ queueId: string;
149
+ note?: string;
150
+ }, o?: RequestOptions): Promise<unknown>;
151
+ closeConversation(conversationId: string, o?: RequestOptions): Promise<{
152
+ conversationId: string;
153
+ closed: true;
154
+ }>;
155
+ listMessages(conversationId: string, q?: {
156
+ after?: string;
157
+ limit?: number;
158
+ }, o?: RequestOptions): Promise<AgentPage<AgentMessageDto>>;
159
+ sendMessage(conversationId: string, text: string, o?: RequestOptions): Promise<Pick<AgentMessageDto, "id" | "conversationId" | "interactionId" | "createdAt">>;
160
+ typing(conversationId: string, o?: RequestOptions): Promise<{
161
+ ok: true;
162
+ }>;
163
+ liveCalls(o?: RequestOptions): Promise<SupervisorLiveCallDto[]>;
164
+ teamAgents(o?: RequestOptions): Promise<SupervisorAgentDto[]>;
165
+ supervise(callId: string, mode?: "monitor" | "whisper" | "barge", o?: RequestOptions): Promise<SupervisorSuperviseDto>;
166
+ setSuperviseMode(callId: string, mode: "monitor" | "whisper" | "barge", supervisorTrackSid?: string, o?: RequestOptions): Promise<{
167
+ callId: string;
168
+ mode: string;
169
+ }>;
170
+ registerPush(sub: AgentPushSubscriptionInput, o?: RequestOptions): Promise<AgentPushSubscriptionDto>;
171
+ unregisterPush(token: string, o?: RequestOptions): Promise<void>;
172
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionManager = void 0;
4
+ exports.memoryStore = memoryStore;
5
+ const emitter_js_1 = require("./emitter.cjs");
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 singleFlight_js_1 = require("./singleFlight.cjs");
10
+ /** Keeps the session for the life of the page / JS context only. The default. */
11
+ function memoryStore() {
12
+ let v = null;
13
+ return { get: () => v, set: (x) => { v = x; }, remove: () => { v = null; } };
14
+ }
15
+ const isSession = (v) => {
16
+ const s = v;
17
+ return !!s && typeof s === "object" && typeof s["accessToken"] === "string" && typeof s["refreshToken"] === "string"
18
+ && typeof s["accessExpiresAt"] === "string" && typeof s["refreshExpiresAt"] === "string";
19
+ };
20
+ /**
21
+ * The staff session: sign in with a fresh assertion, keep it in the store,
22
+ * refresh before expiry (single flight — one refresh however many callers),
23
+ * sign in again with a NEW assertion when the refresh is refused.
24
+ */
25
+ class SessionManager {
26
+ constructor(o) {
27
+ this.o = o;
28
+ this.events = new emitter_js_1.TypedEmitter();
29
+ this.session = null;
30
+ this.forceNext = false;
31
+ this.freshSignIn = (0, singleFlight_js_1.singleFlight)(async () => {
32
+ // Always a new assertion: never cached, never stored, never reused.
33
+ let assertion;
34
+ try {
35
+ assertion = await this.o.getAssertion();
36
+ }
37
+ catch (e) {
38
+ throw errors_js_1.HainivaAgentError.from(e, "assertion_failed", this.redactor);
39
+ }
40
+ if (typeof assertion !== "string" || assertion.split(".").length !== 3) {
41
+ throw new errors_js_1.HainivaAgentError(0, "invalid_assertion", { hint: "getAssertion must resolve to a signed JWT from your backend" });
42
+ }
43
+ this.redactor.add(assertion);
44
+ try {
45
+ const s = await this.o.transport.exchange(assertion);
46
+ if (!isSession(s))
47
+ throw new errors_js_1.HainivaAgentError(0, "invalid_session", {}, this.redactor);
48
+ await this.keep(s);
49
+ return s;
50
+ }
51
+ catch (e) {
52
+ throw errors_js_1.HainivaAgentError.from(e, "sign_in_failed", this.redactor);
53
+ }
54
+ finally {
55
+ this.redactor.remove(assertion);
56
+ }
57
+ });
58
+ this.store = o.store ?? memoryStore();
59
+ this.skew = o.refreshSkewMs ?? 60000;
60
+ this.now = o.now ?? Date.now;
61
+ this.redactor = o.redactor ?? new redact_js_1.Redactor();
62
+ this.log = (0, log_js_1.safeLogger)(o.log, this.redactor);
63
+ this.refreshOnce = (0, singleFlight_js_1.singleFlight)(() => this.doRefresh());
64
+ }
65
+ get current() { return this.session; }
66
+ /** Restore the stored session if it can still be refreshed, else sign in afresh. */
67
+ async signIn() {
68
+ const saved = await Promise.resolve().then(() => this.store.get()).catch((e) => { this.log("store_read_failed", e); return null; });
69
+ if (saved) {
70
+ try {
71
+ const s = JSON.parse(saved);
72
+ if (isSession(s) && Date.parse(s.refreshExpiresAt) > this.now() + this.skew) {
73
+ this.adopt(s);
74
+ return await this.fresh();
75
+ }
76
+ }
77
+ catch { /* corrupt or refused: sign in afresh */ }
78
+ await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
79
+ }
80
+ return this.freshSignIn();
81
+ }
82
+ /** A valid access token, refreshing first when it is about to expire. */
83
+ async accessToken() {
84
+ return (await this.fresh()).accessToken;
85
+ }
86
+ /**
87
+ * The server refused the access token (401 / socket 4001): the next
88
+ * accessToken() refreshes. Pass the token that was refused: a refusal of an
89
+ * OLDER token (a request that was in flight while another caller already
90
+ * refreshed) is ignored, so ten parallel 401s cause one refresh, not ten.
91
+ */
92
+ invalidate(refused) {
93
+ if (refused && this.session && this.session.accessToken !== refused)
94
+ return;
95
+ this.forceNext = true;
96
+ }
97
+ /** Forget the session here only (the server already ended it): no revoke call. */
98
+ async clear() {
99
+ this.adopt(null);
100
+ await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
101
+ }
102
+ /** End the session here and (best effort) on the server. */
103
+ async signOut() {
104
+ const s = this.session;
105
+ this.adopt(null);
106
+ await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
107
+ if (s && this.o.transport.revoke)
108
+ await this.o.transport.revoke(s).catch((e) => this.log("revoke_failed", e));
109
+ this.redactor.clear();
110
+ }
111
+ async fresh() {
112
+ if (!this.session)
113
+ return this.freshSignIn();
114
+ const left = Date.parse(this.session.accessExpiresAt) - this.now();
115
+ if (!this.forceNext && left > this.skew)
116
+ return this.session;
117
+ return this.refreshOnce();
118
+ }
119
+ async doRefresh() {
120
+ const s = this.session;
121
+ if (!s)
122
+ return this.freshSignIn();
123
+ this.forceNext = false;
124
+ try {
125
+ const r = await this.o.transport.refresh(s.refreshToken);
126
+ if (!r || typeof r.accessToken !== "string" || !r.accessToken)
127
+ throw new errors_js_1.HainivaAgentError(0, "refresh_failed", {}, this.redactor);
128
+ const next = { ...s, ...r };
129
+ await this.keep(next);
130
+ return next;
131
+ }
132
+ catch (e) {
133
+ // Refused (expired, revoked, reused): a new session from a NEW assertion.
134
+ if (e instanceof errors_js_1.HainivaAgentError && (e.status === 401 || e.status === 403)) {
135
+ this.log("refresh_refused", { status: e.status });
136
+ await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
137
+ this.adopt(null);
138
+ return this.freshSignIn();
139
+ }
140
+ this.forceNext = true;
141
+ throw errors_js_1.HainivaAgentError.from(e, "refresh_failed", this.redactor);
142
+ }
143
+ }
144
+ async keep(s) {
145
+ this.adopt(s);
146
+ await Promise.resolve().then(() => this.store.set(JSON.stringify(s))).catch((e) => this.log("store_write_failed", e));
147
+ }
148
+ adopt(s) {
149
+ const prev = this.session;
150
+ this.session = s;
151
+ // Old tokens stay redacted: they may still appear in an in-flight error.
152
+ if (s)
153
+ this.redactor.add(s.accessToken, s.refreshToken);
154
+ this.events.emit("session", s);
155
+ if (s && s.accessToken !== prev?.accessToken)
156
+ this.events.emit("token", s.accessToken);
157
+ }
158
+ }
159
+ exports.SessionManager = SessionManager;
@@ -0,0 +1,101 @@
1
+ import { TypedEmitter } from "./emitter.cjs";
2
+ import { type Logger } from "./log.cjs";
3
+ import { Redactor } from "./redact.cjs";
4
+ /**
5
+ * Where the session is kept between launches. Same shape as @hainiva/app-sdk's.
6
+ * The default is in memory (nothing persisted). On the web, `localStorageStore`
7
+ * (`@hainiva/agent-sdk/web`) is opt-in; in React Native use
8
+ * `secureStoreSessionStore` (`@hainiva/agent-sdk/react-native`) — the refresh
9
+ * token then lives only in the Keychain / Android Keystore.
10
+ */
11
+ export interface SessionStore {
12
+ get(): Promise<string | null> | string | null;
13
+ set(value: string): Promise<void> | void;
14
+ remove(): Promise<void> | void;
15
+ }
16
+ /** Keeps the session for the life of the page / JS context only. The default. */
17
+ export declare function memoryStore(): SessionStore;
18
+ /** A staff session. Fields beyond these are kept as the server sent them. */
19
+ export interface AgentSession {
20
+ accessToken: string;
21
+ /** ISO time. */
22
+ accessExpiresAt: string;
23
+ refreshToken: string;
24
+ /** ISO time. */
25
+ refreshExpiresAt: string;
26
+ [field: string]: unknown;
27
+ }
28
+ /**
29
+ * Ask YOUR backend for a fresh staff assertion (a short-lived JWT it signs with
30
+ * the agent app's private key). Called every time a new session is needed —
31
+ * the SDK never caches or stores an assertion, and you must not either: each
32
+ * one carries a single-use `jti` and expires in minutes.
33
+ */
34
+ export type GetAssertion = () => Promise<string>;
35
+ /**
36
+ * How sessions are created and refreshed — bound by the REST client.
37
+ * TODO(S3b): implemented against `/agent/v1/sessions` once the S2 contract is final.
38
+ */
39
+ export interface SessionTransport<S extends AgentSession = AgentSession> {
40
+ /** Exchange an assertion for a session (POST /agent/v1/sessions). */
41
+ exchange(assertion: string): Promise<S>;
42
+ /** Rotate: a new access token and (rotating) refresh token. */
43
+ refresh(refreshToken: string): Promise<Pick<S, "accessToken" | "accessExpiresAt"> & Partial<S>>;
44
+ /** End the session on the server (best effort). */
45
+ revoke?(session: S): Promise<void>;
46
+ }
47
+ export interface SessionManagerOptions<S extends AgentSession> {
48
+ getAssertion: GetAssertion;
49
+ transport: SessionTransport<S>;
50
+ /** Default: memoryStore(). */
51
+ store?: SessionStore;
52
+ /** Refresh this long before the access token expires. Default 60 s. */
53
+ refreshSkewMs?: number;
54
+ now?: () => number;
55
+ redactor?: Redactor;
56
+ log?: Logger;
57
+ }
58
+ export interface SessionEvents<S extends AgentSession> {
59
+ /** A new access token (after sign-in or refresh): e.g. send it on the socket. */
60
+ token: string;
61
+ session: S | null;
62
+ }
63
+ /**
64
+ * The staff session: sign in with a fresh assertion, keep it in the store,
65
+ * refresh before expiry (single flight — one refresh however many callers),
66
+ * sign in again with a NEW assertion when the refresh is refused.
67
+ */
68
+ export declare class SessionManager<S extends AgentSession = AgentSession> {
69
+ private readonly o;
70
+ readonly events: TypedEmitter<SessionEvents<S>>;
71
+ private session;
72
+ private readonly store;
73
+ private readonly skew;
74
+ private readonly now;
75
+ readonly redactor: Redactor;
76
+ private readonly log;
77
+ private readonly refreshOnce;
78
+ private forceNext;
79
+ constructor(o: SessionManagerOptions<S>);
80
+ get current(): S | null;
81
+ /** Restore the stored session if it can still be refreshed, else sign in afresh. */
82
+ signIn(): Promise<S>;
83
+ /** A valid access token, refreshing first when it is about to expire. */
84
+ accessToken(): Promise<string>;
85
+ /**
86
+ * The server refused the access token (401 / socket 4001): the next
87
+ * accessToken() refreshes. Pass the token that was refused: a refusal of an
88
+ * OLDER token (a request that was in flight while another caller already
89
+ * refreshed) is ignored, so ten parallel 401s cause one refresh, not ten.
90
+ */
91
+ invalidate(refused?: string): void;
92
+ /** Forget the session here only (the server already ended it): no revoke call. */
93
+ clear(): Promise<void>;
94
+ /** End the session here and (best effort) on the server. */
95
+ signOut(): Promise<void>;
96
+ private fresh;
97
+ private doRefresh;
98
+ private readonly freshSignIn;
99
+ private keep;
100
+ private adopt;
101
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.singleFlight = singleFlight;
4
+ exports.createTokenRefresher = createTokenRefresher;
5
+ /**
6
+ * Calls `fn` at most once at a time: callers that arrive while it runs share its
7
+ * result (or its error). The next call after it settles runs `fn` again.
8
+ */
9
+ function singleFlight(fn) {
10
+ let inFlight = null;
11
+ return () => {
12
+ inFlight ?? (inFlight = (async () => {
13
+ try {
14
+ return await fn();
15
+ }
16
+ finally {
17
+ inFlight = null;
18
+ }
19
+ })());
20
+ return inFlight;
21
+ };
22
+ }
23
+ /**
24
+ * Single-flight token refresh with a pluggable refresh function. Ten requests that
25
+ * find the token expired cause one refresh, not ten — which also keeps a
26
+ * rotating refresh token from being spent twice (reuse detection would then
27
+ * revoke the whole session family).
28
+ */
29
+ function createTokenRefresher(o) {
30
+ const skew = o.skewMs ?? 60000;
31
+ const now = o.now ?? Date.now;
32
+ let current = null;
33
+ const run = singleFlight(async () => {
34
+ const t = await o.refresh();
35
+ if (!t || typeof t.token !== "string" || !t.token)
36
+ throw new Error("refresh returned no token");
37
+ current = t;
38
+ return t.token;
39
+ });
40
+ return {
41
+ get valid() { return current !== null && current.expiresAt - now() > skew; },
42
+ async get() {
43
+ if (current && current.expiresAt - now() > skew)
44
+ return current.token;
45
+ return run();
46
+ },
47
+ invalidate() { current = null; },
48
+ };
49
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Calls `fn` at most once at a time: callers that arrive while it runs share its
3
+ * result (or its error). The next call after it settles runs `fn` again.
4
+ */
5
+ export declare function singleFlight<T>(fn: () => Promise<T>): () => Promise<T>;
6
+ export interface RefreshedToken {
7
+ token: string;
8
+ /** Epoch ms. */
9
+ expiresAt: number;
10
+ }
11
+ export interface TokenRefresherOptions {
12
+ /** Fetch a new token. Called once however many callers wait for it. */
13
+ refresh: () => Promise<RefreshedToken>;
14
+ /** Refresh this long before expiry. Default 60 s. */
15
+ skewMs?: number;
16
+ now?: () => number;
17
+ }
18
+ export interface TokenRefresher {
19
+ /** A token valid for at least `skewMs`, refreshing (once, shared) when needed. */
20
+ get(): Promise<string>;
21
+ /** Forget the current token (the server refused it): the next get() refreshes. */
22
+ invalidate(): void;
23
+ /** Whether a token is held that is still valid beyond the skew. */
24
+ readonly valid: boolean;
25
+ }
26
+ /**
27
+ * Single-flight token refresh with a pluggable refresh function. Ten requests that
28
+ * find the token expired cause one refresh, not ten — which also keeps a
29
+ * rotating refresh token from being spent twice (reuse detection would then
30
+ * revoke the whole session family).
31
+ */
32
+ export declare function createTokenRefresher(o: TokenRefresherOptions): TokenRefresher;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertSafeUrl = assertSafeUrl;
4
+ const errors_js_1 = require("./errors.cjs");
5
+ const LOCAL = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
6
+ /** Query parameter names that would carry a credential. */
7
+ const CREDENTIAL_PARAM = /token|assertion|secret|password|passw|key|auth|jwt|session|signature|ticket/i;
8
+ /**
9
+ * A base / socket / media URL a partner configured, checked before use: TLS only,
10
+ * no user:password@, and no credential in the query string. Tokens travel in
11
+ * headers or in the socket's first frame — never in a URL, which ends up in
12
+ * access logs, proxies and browser history.
13
+ */
14
+ function assertSafeUrl(raw, rules) {
15
+ let u;
16
+ try {
17
+ u = new URL(raw);
18
+ }
19
+ catch {
20
+ throw new errors_js_1.HainivaAgentError(0, "invalid_url", { kind: rules.kind });
21
+ }
22
+ const secure = rules.kind === "http" ? "https:" : "wss:";
23
+ const insecure = rules.kind === "http" ? "http:" : "ws:";
24
+ const local = LOCAL.has(u.hostname);
25
+ if (u.protocol !== secure && !(u.protocol === insecure && local && rules.allowInsecureLocalhost)) {
26
+ throw new errors_js_1.HainivaAgentError(0, "insecure_url", { expected: secure, got: u.protocol, host: u.host });
27
+ }
28
+ if (u.username || u.password)
29
+ throw new errors_js_1.HainivaAgentError(0, "credentials_in_url", { host: u.host });
30
+ let param = null;
31
+ u.searchParams.forEach((_v, name) => { if (!param && CREDENTIAL_PARAM.test(name))
32
+ param = name; });
33
+ if (param)
34
+ throw new errors_js_1.HainivaAgentError(0, "credentials_in_url", { host: u.host, param });
35
+ if (/eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/.test(u.search + u.hash + u.pathname)) {
36
+ throw new errors_js_1.HainivaAgentError(0, "credentials_in_url", { host: u.host });
37
+ }
38
+ return u;
39
+ }
@@ -0,0 +1,13 @@
1
+ export interface UrlRules {
2
+ /** "http" for REST (https required), "ws" for sockets (wss required). */
3
+ kind: "http" | "ws";
4
+ /** Allow http:// / ws:// to localhost (local development only). Default false. */
5
+ allowInsecureLocalhost?: boolean;
6
+ }
7
+ /**
8
+ * A base / socket / media URL a partner configured, checked before use: TLS only,
9
+ * no user:password@, and no credential in the query string. Tokens travel in
10
+ * headers or in the socket's first frame — never in a URL, which ends up in
11
+ * access logs, proxies and browser history.
12
+ */
13
+ export declare function assertSafeUrl(raw: string, rules: UrlRules): URL;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.joinCallRoom = exports.memoryStore = void 0;
4
+ exports.localStorageStore = localStorageStore;
5
+ var session_js_1 = require("./session.cjs");
6
+ Object.defineProperty(exports, "memoryStore", { enumerable: true, get: function () { return session_js_1.memoryStore; } });
7
+ var livekit_js_1 = require("./livekit.cjs");
8
+ Object.defineProperty(exports, "joinCallRoom", { enumerable: true, get: function () { return livekit_js_1.joinCallRoom; } });
9
+ /**
10
+ * OPT-IN: keep the session in localStorage so a reload stays signed in.
11
+ *
12
+ * ⚠ Any script running on your origin (an XSS, a compromised third-party script)
13
+ * can read localStorage — including the 30-day refresh token. Use it only with a
14
+ * strict Content-Security-Policy and no third-party scripts on the page. The
15
+ * default (memoryStore) keeps nothing: a reload signs in again with a fresh
16
+ * assertion from your backend, which is the safer choice.
17
+ */
18
+ function localStorageStore(o = {}) {
19
+ const key = o.key ?? "hainiva.agent.session";
20
+ const storage = o.storage ?? globalThis.localStorage;
21
+ if (!storage)
22
+ throw new Error("localStorage is not available here; use memoryStore()");
23
+ return {
24
+ get: () => storage.getItem(key),
25
+ set: (v) => storage.setItem(key, v),
26
+ remove: () => storage.removeItem(key),
27
+ };
28
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @hainiva/agent-sdk/web — browser adapters: an opt-in localStorage session store
3
+ * and LiveKit call media (peer dependency `livekit-client`).
4
+ */
5
+ import type { SessionStore } from "./session.cjs";
6
+ export { memoryStore } from "./session.cjs";
7
+ export { joinCallRoom, type JoinOptions, type MediaCall, type MediaJoin } from "./livekit.cjs";
8
+ export interface LocalStorageStoreOptions {
9
+ /** Default "hainiva.agent.session". */
10
+ key?: string;
11
+ /** Default globalThis.localStorage. */
12
+ storage?: Pick<Storage, "getItem" | "setItem" | "removeItem">;
13
+ }
14
+ /**
15
+ * OPT-IN: keep the session in localStorage so a reload stays signed in.
16
+ *
17
+ * ⚠ Any script running on your origin (an XSS, a compromised third-party script)
18
+ * can read localStorage — including the 30-day refresh token. Use it only with a
19
+ * strict Content-Security-Policy and no third-party scripts on the page. The
20
+ * default (memoryStore) keeps nothing: a reload signs in again with a fresh
21
+ * assertion from your backend, which is the safer choice.
22
+ */
23
+ export declare function localStorageStore(o?: LocalStorageStoreOptions): SessionStore;