@oxvo/ai-live-assist 7.3.4 → 7.4.23

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 (67) hide show
  1. package/cjs/AiLiveAssist.d.ts +588 -6
  2. package/cjs/AiLiveAssist.js +14892 -1240
  3. package/cjs/client.d.ts +71 -10
  4. package/cjs/client.js +316 -58
  5. package/cjs/context.d.ts +15 -5
  6. package/cjs/context.js +568 -73
  7. package/cjs/control.d.ts +72 -7
  8. package/cjs/control.js +594 -71
  9. package/cjs/dialogFocusScope.d.ts +37 -0
  10. package/cjs/dialogFocusScope.js +227 -0
  11. package/cjs/experienceState.js +1 -1
  12. package/cjs/index.d.ts +1 -1
  13. package/cjs/index.js +2 -2
  14. package/cjs/media.d.ts +14 -3
  15. package/cjs/media.js +128 -19
  16. package/cjs/messages.js +26 -3
  17. package/cjs/messengerLoader.d.ts +3 -0
  18. package/cjs/messengerLoader.js +156 -0
  19. package/cjs/pageAssistancePreference.d.ts +78 -0
  20. package/cjs/pageAssistancePreference.js +488 -0
  21. package/cjs/preview.d.ts +33 -0
  22. package/cjs/preview.js +567 -0
  23. package/cjs/tabLock.d.ts +54 -3
  24. package/cjs/tabLock.js +752 -72
  25. package/cjs/types.d.ts +152 -4
  26. package/cjs/ui.d.ts +200 -10
  27. package/cjs/ui.js +6946 -523
  28. package/cjs/version.d.ts +1 -1
  29. package/cjs/version.js +1 -1
  30. package/cjs/voicePresenceUi.d.ts +48 -9
  31. package/cjs/voicePresenceUi.js +863 -125
  32. package/lib/AiLiveAssist.d.ts +588 -6
  33. package/lib/AiLiveAssist.js +14897 -1248
  34. package/lib/client.d.ts +71 -10
  35. package/lib/client.js +313 -57
  36. package/lib/context.d.ts +15 -5
  37. package/lib/context.js +566 -72
  38. package/lib/control.d.ts +72 -7
  39. package/lib/control.js +594 -71
  40. package/lib/dialogFocusScope.d.ts +37 -0
  41. package/lib/dialogFocusScope.js +223 -0
  42. package/lib/experienceState.js +1 -1
  43. package/lib/index.d.ts +1 -1
  44. package/lib/index.js +2 -2
  45. package/lib/media.d.ts +14 -3
  46. package/lib/media.js +128 -19
  47. package/lib/messages.js +26 -3
  48. package/lib/messengerLoader.d.ts +3 -0
  49. package/lib/messengerLoader.js +151 -0
  50. package/lib/pageAssistancePreference.d.ts +78 -0
  51. package/lib/pageAssistancePreference.js +482 -0
  52. package/lib/preview.d.ts +33 -0
  53. package/lib/preview.js +561 -0
  54. package/lib/tabLock.d.ts +54 -3
  55. package/lib/tabLock.js +752 -72
  56. package/lib/types.d.ts +152 -4
  57. package/lib/ui.d.ts +200 -10
  58. package/lib/ui.js +6947 -524
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/lib/voicePresenceUi.d.ts +48 -9
  62. package/lib/voicePresenceUi.js +863 -125
  63. package/package.json +8 -3
  64. package/cjs/recordingPolicy.d.ts +0 -2
  65. package/cjs/recordingPolicy.js +0 -12
  66. package/lib/recordingPolicy.d.ts +0 -2
  67. package/lib/recordingPolicy.js +0 -8
package/cjs/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AssistMode, BootstrapConfig, ConsentScope, LaunchResult, ExperienceMode } from './types.js';
1
+ import type { AssistMode, AiGuideCompanionState, BootstrapConfig, ConsentScope, HandoffRecoveryResult, LaunchResult, ExperienceMode, MessengerConversationState } from "./types.js";
2
2
  export declare class RuntimeClientError extends Error {
3
3
  readonly code: string;
4
4
  readonly retryable: boolean;
@@ -6,17 +6,22 @@ export declare class RuntimeClientError extends Error {
6
6
  readonly status: number;
7
7
  constructor(code: string, message: string, retryable: boolean, retryAfterMs: number | null, status: number);
8
8
  }
9
- type LaunchInput = {
9
+ export declare const MAX_ASSIST_SESSION_GRANT_LENGTH = 4096;
10
+ export declare const isValidReplaySessionId: (value: unknown) => value is string;
11
+ export declare const isValidAssistSessionGrant: (value: unknown) => value is string;
12
+ export type ReplayLaunchContext = {
13
+ sessionId: string;
14
+ tabId: string;
15
+ grant: string;
16
+ };
17
+ export type LaunchInput = {
10
18
  siteKey: string;
11
19
  configRevision: number;
12
20
  path: string;
13
21
  tabId: string;
14
22
  mode: AssistMode;
15
23
  anonymousId: string;
16
- replay?: {
17
- sessionId: string;
18
- tabId: string;
19
- };
24
+ replay: ReplayLaunchContext;
20
25
  disclosureChecksum: string;
21
26
  scopes: ConsentScope[];
22
27
  locale: string;
@@ -26,6 +31,55 @@ type LaunchInput = {
26
31
  experienceMode: ExperienceMode;
27
32
  fallbackIntent?: "standard_assist" | "human_handoff";
28
33
  lowPower: boolean;
34
+ idempotencyKey: string;
35
+ };
36
+ export type ModeUpgradeResumeInput = {
37
+ sessionId: string;
38
+ resumeCredential: string;
39
+ path: string;
40
+ protocolVersion: 1 | 2;
41
+ requestId: string;
42
+ idempotencyKey: string;
43
+ };
44
+ export type ModeUpgradeRecoveryResult = {
45
+ schemaVersion: 1;
46
+ status: "voice_committed";
47
+ resume: LaunchResult;
48
+ } | {
49
+ schemaVersion: 1;
50
+ status: "terminalized";
51
+ sessionId: string;
52
+ state: "ended" | "failed" | "revoked";
53
+ };
54
+ export type RuntimeSessionState = "created" | "authorizing" | "connecting" | "active_text" | "active_voice" | "paused" | "reconnecting" | "handoff_pending" | "human_controlled" | "ending" | "ended" | "failed" | "revoked";
55
+ export type PauseCommandIdentity = {
56
+ operationId: string;
57
+ clientSequence: number;
58
+ };
59
+ export type PauseCommandResult = {
60
+ schemaVersion: 1;
61
+ operationId: string;
62
+ clientSequence: number;
63
+ fencingToken: number;
64
+ action: "pause" | "resume";
65
+ status: "applied" | "replayed" | "superseded";
66
+ state: RuntimeSessionState;
67
+ };
68
+ export type CompanionSurfaceSwitchInput = {
69
+ linkId: string;
70
+ switchGrant: string;
71
+ visitorId: string;
72
+ expectedOwnerEpoch: number;
73
+ targetSurface: "ai" | "messenger";
74
+ lastAiMode: AssistMode;
75
+ currentAiSessionId?: string;
76
+ idempotencyKey: string;
77
+ };
78
+ export type CompanionSurfaceSwitchResult = {
79
+ schemaVersion: 1;
80
+ status: "switched" | "replayed" | "stale";
81
+ companion: AiGuideCompanionState;
82
+ messengerConversation: MessengerConversationState;
29
83
  };
30
84
  export declare const idempotencyKey: (prefix: string) => string;
31
85
  export declare class RuntimeClient {
@@ -35,19 +89,26 @@ export declare class RuntimeClient {
35
89
  bootstrap(siteKey: string, path: string, anonymousId: string, signal: AbortSignal): Promise<BootstrapConfig>;
36
90
  launch(input: LaunchInput, signal: AbortSignal): Promise<LaunchResult>;
37
91
  resume(sessionId: string, credential: string, path: string, protocolVersion: 1 | 2, signal: AbortSignal): Promise<LaunchResult>;
38
- offer(result: LaunchResult, offerSdp: string, signal: AbortSignal): Promise<{
92
+ resumePendingHandoff(sessionId: string, credential: string, path: string, protocolVersion: 1 | 2, messengerBindingChallengeId: string, signal: AbortSignal): Promise<HandoffRecoveryResult>;
93
+ resumeModeUpgrade(input: ModeUpgradeResumeInput, signal: AbortSignal): Promise<LaunchResult>;
94
+ reconcileModeUpgrade(input: ModeUpgradeResumeInput, signal?: AbortSignal, keepalive?: boolean): Promise<ModeUpgradeRecoveryResult>;
95
+ offer(result: LaunchResult, offerSdp: string, signal: AbortSignal, offerIdempotencyKey?: string): Promise<{
39
96
  answerSdp: string;
40
97
  fencingToken: number;
98
+ sessionState: "active_text" | "active_voice" | null;
41
99
  }>;
42
100
  end(result: LaunchResult, fencingToken: number, signal?: AbortSignal): Promise<void>;
43
- pauseOrResume(result: LaunchResult, fencingToken: number, operation: 'pause' | 'resume', signal?: AbortSignal): Promise<void>;
101
+ recoverAndEnd(sessionId: string, resumeCredential: string, protocolVersion: 1 | 2, signal?: AbortSignal): Promise<void>;
102
+ pauseOrResume(result: LaunchResult, fencingToken: number, operation: "pause" | "resume", identity: PauseCommandIdentity, signal?: AbortSignal): Promise<PauseCommandResult>;
103
+ switchCompanionSurface(input: CompanionSurfaceSwitchInput, signal?: AbortSignal): Promise<CompanionSurfaceSwitchResult>;
44
104
  feedback(result: LaunchResult, payload: {
45
105
  rating: number | null;
46
- outcome: 'resolved' | 'partially_resolved' | 'not_resolved' | 'handoff';
106
+ outcome: "resolved" | "partially_resolved" | "not_resolved" | "handoff";
47
107
  }, signal?: AbortSignal): Promise<void>;
48
108
  private postEmpty;
109
+ private parseCompanionState;
110
+ private parseMessengerConversation;
49
111
  private postJson;
50
112
  private parseJson;
51
113
  private throwResponse;
52
114
  }
53
- export {};
package/cjs/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RuntimeClient = exports.idempotencyKey = exports.RuntimeClientError = void 0;
3
+ exports.RuntimeClient = exports.idempotencyKey = exports.isValidAssistSessionGrant = exports.isValidReplaySessionId = exports.MAX_ASSIST_SESSION_GRANT_LENGTH = exports.RuntimeClientError = void 0;
4
4
  class RuntimeClientError extends Error {
5
5
  constructor(code, message, retryable, retryAfterMs, status) {
6
6
  super(message);
@@ -8,10 +8,40 @@ class RuntimeClientError extends Error {
8
8
  this.retryable = retryable;
9
9
  this.retryAfterMs = retryAfterMs;
10
10
  this.status = status;
11
- this.name = 'RuntimeClientError';
11
+ this.name = "RuntimeClientError";
12
12
  }
13
13
  }
14
14
  exports.RuntimeClientError = RuntimeClientError;
15
+ const MAX_SIGNED_INT64 = "9223372036854775807";
16
+ const CONTROL_OPERATION_ID_PATTERN = /^[A-Za-z0-9_.:-]{8,128}$/;
17
+ exports.MAX_ASSIST_SESSION_GRANT_LENGTH = 4096;
18
+ const isValidReplaySessionId = (value) => {
19
+ if (typeof value !== "string" || !/^[1-9]\d{0,18}$/.test(value)) {
20
+ return false;
21
+ }
22
+ return value.length < MAX_SIGNED_INT64.length || value <= MAX_SIGNED_INT64;
23
+ };
24
+ exports.isValidReplaySessionId = isValidReplaySessionId;
25
+ const isValidAssistSessionGrant = (value) => typeof value === "string" &&
26
+ value.length > 0 &&
27
+ value.length <= exports.MAX_ASSIST_SESSION_GRANT_LENGTH &&
28
+ value.trim() === value;
29
+ exports.isValidAssistSessionGrant = isValidAssistSessionGrant;
30
+ const RUNTIME_SESSION_STATES = new Set([
31
+ "created",
32
+ "authorizing",
33
+ "connecting",
34
+ "active_text",
35
+ "active_voice",
36
+ "paused",
37
+ "reconnecting",
38
+ "handoff_pending",
39
+ "human_controlled",
40
+ "ending",
41
+ "ended",
42
+ "failed",
43
+ "revoked",
44
+ ]);
15
45
  const requestId = () => `req_${crypto.randomUUID()}`;
16
46
  const idempotencyKey = (prefix) => `${prefix}_${crypto.randomUUID()}`;
17
47
  exports.idempotencyKey = idempotencyKey;
@@ -19,31 +49,53 @@ class RuntimeClient {
19
49
  constructor(runtimeUrl, clientVersion) {
20
50
  this.clientVersion = clientVersion;
21
51
  const url = new URL(runtimeUrl, window.location.href);
22
- if (!['http:', 'https:'].includes(url.protocol)) {
23
- throw new Error('AI Guide runtime URL must use HTTP or HTTPS.');
52
+ if (!["http:", "https:"].includes(url.protocol)) {
53
+ throw new Error("AI Guide runtime URL must use HTTP or HTTPS.");
24
54
  }
25
- this.baseUrl = url.origin + url.pathname.replace(/\/$/, '');
55
+ this.baseUrl = url.origin + url.pathname.replace(/\/$/, "");
26
56
  }
27
57
  async bootstrap(siteKey, path, anonymousId, signal) {
28
58
  const url = new URL(`${this.baseUrl}/ai-live-assist/v1/public/bootstrap`);
29
- url.searchParams.set('siteKey', siteKey);
30
- url.searchParams.set('path', path);
59
+ url.searchParams.set("siteKey", siteKey);
60
+ url.searchParams.set("path", path);
31
61
  const response = await fetch(url, {
32
- method: 'GET',
33
- mode: 'cors',
34
- credentials: 'omit',
35
- cache: 'no-store',
62
+ method: "GET",
63
+ mode: "cors",
64
+ credentials: "omit",
65
+ cache: "no-store",
36
66
  headers: {
37
- Accept: 'application/json',
38
- 'X-Oxvo-Ai-Client-Version': this.clientVersion,
39
- 'X-Oxvo-Ai-Visitor-Id': anonymousId,
40
- 'X-Request-Id': requestId(),
67
+ Accept: "application/json",
68
+ "X-Oxvo-Ai-Client-Version": this.clientVersion,
69
+ "X-Oxvo-Ai-Visitor-Id": anonymousId,
70
+ "X-Request-Id": requestId(),
41
71
  },
42
72
  signal,
43
73
  });
44
74
  return this.parseJson(response);
45
75
  }
46
76
  async launch(input, signal) {
77
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(input.idempotencyKey)) {
78
+ throw new Error("Invalid AI Guide launch idempotency key.");
79
+ }
80
+ const replaySessionId = input.replay?.sessionId;
81
+ const replayTabId = input.replay?.tabId;
82
+ const replayGrant = input.replay?.grant;
83
+ if (!(0, exports.isValidReplaySessionId)(replaySessionId)) {
84
+ throw new Error("Invalid AI Guide replay session ID.");
85
+ }
86
+ if (typeof replayTabId !== "string" ||
87
+ replayTabId !== input.tabId ||
88
+ !/^[A-Za-z0-9_.:-]{1,128}$/.test(replayTabId)) {
89
+ throw new Error("Invalid AI Guide replay tab ID.");
90
+ }
91
+ if (!(0, exports.isValidAssistSessionGrant)(replayGrant)) {
92
+ throw new Error("Invalid AI Guide replay grant.");
93
+ }
94
+ const replay = {
95
+ sessionId: replaySessionId,
96
+ tabId: replayTabId,
97
+ grant: replayGrant,
98
+ };
47
99
  const shared = {
48
100
  clientVersion: this.clientVersion,
49
101
  siteKey: input.siteKey,
@@ -52,7 +104,7 @@ class RuntimeClient {
52
104
  tabId: input.tabId,
53
105
  mode: input.mode,
54
106
  visitor: { anonymousId: input.anonymousId },
55
- ...(input.replay ? { replay: input.replay } : {}),
107
+ replay,
56
108
  consent: {
57
109
  disclosureChecksum: input.disclosureChecksum,
58
110
  scopes: input.scopes,
@@ -70,13 +122,12 @@ class RuntimeClient {
70
122
  ...shared,
71
123
  capabilitySummary: {
72
124
  microphone: input.microphone,
73
- webrtc: typeof RTCPeerConnection !== 'undefined',
74
- audioPlayback: typeof HTMLAudioElement !== 'undefined',
75
- webAudio: typeof AudioContext !== 'undefined' ||
76
- typeof window
77
- .webkitAudioContext !== 'undefined',
78
- svgOverlay: typeof SVGElement !== 'undefined',
79
- visualViewport: typeof window.visualViewport !== 'undefined',
125
+ webrtc: typeof RTCPeerConnection !== "undefined",
126
+ audioPlayback: typeof HTMLAudioElement !== "undefined",
127
+ webAudio: typeof AudioContext !== "undefined" ||
128
+ typeof window.webkitAudioContext !== "undefined",
129
+ svgOverlay: typeof SVGElement !== "undefined",
130
+ visualViewport: typeof window.visualViewport !== "undefined",
80
131
  reducedMotion: input.reducedMotion,
81
132
  lowPower: input.lowPower,
82
133
  },
@@ -87,83 +138,290 @@ class RuntimeClient {
87
138
  ...shared,
88
139
  capabilitySummary: {
89
140
  microphone: input.microphone,
90
- webrtc: typeof RTCPeerConnection !== 'undefined',
141
+ webrtc: typeof RTCPeerConnection !== "undefined",
91
142
  reducedMotion: input.reducedMotion,
92
143
  },
93
144
  };
94
- return this.postJson('/ai-live-assist/v1/public/launch', body, {
95
- 'Idempotency-Key': (0, exports.idempotencyKey)('launch'),
145
+ return this.postJson("/ai-live-assist/v1/public/launch", body, {
146
+ "Idempotency-Key": input.idempotencyKey,
96
147
  }, signal);
97
148
  }
98
149
  async resume(sessionId, credential, path, protocolVersion, signal) {
99
- return this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/resume`, {
150
+ const request = this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/resume`, {
100
151
  schemaVersion: protocolVersion,
101
152
  protocolVersion,
102
153
  path,
103
- reason: 'navigation',
154
+ reason: "navigation",
104
155
  }, { Authorization: `Bearer ${credential}` }, signal);
156
+ try {
157
+ return await request;
158
+ }
159
+ catch (error) {
160
+ if (error instanceof TypeError) {
161
+ throw new RuntimeClientError("RUNTIME_UNAVAILABLE", "AI Guide is temporarily unavailable.", true, null, 0);
162
+ }
163
+ throw error;
164
+ }
165
+ }
166
+ async resumePendingHandoff(sessionId, credential, path, protocolVersion, messengerBindingChallengeId, signal) {
167
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(messengerBindingChallengeId)) {
168
+ throw new Error("Invalid Messenger handoff recovery identity.");
169
+ }
170
+ try {
171
+ return await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/resume`, {
172
+ schemaVersion: protocolVersion,
173
+ protocolVersion,
174
+ path,
175
+ reason: "handoff_recovery",
176
+ messengerBindingChallengeId,
177
+ }, { Authorization: `Bearer ${credential}` }, signal);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof TypeError) {
181
+ throw new RuntimeClientError("RUNTIME_UNAVAILABLE", "AI Guide is temporarily unavailable.", true, null, 0);
182
+ }
183
+ throw error;
184
+ }
185
+ }
186
+ async resumeModeUpgrade(input, signal) {
187
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(input.requestId) ||
188
+ !/^[A-Za-z0-9_.:-]{8,128}$/.test(input.idempotencyKey)) {
189
+ throw new Error("Invalid AI Guide mode-upgrade identity.");
190
+ }
191
+ return this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(input.sessionId)}/resume`, {
192
+ schemaVersion: input.protocolVersion,
193
+ protocolVersion: input.protocolVersion,
194
+ path: input.path,
195
+ reason: "mode_upgrade",
196
+ modeUpgradeRequestId: input.requestId,
197
+ }, {
198
+ Authorization: `Bearer ${input.resumeCredential}`,
199
+ "Idempotency-Key": input.idempotencyKey,
200
+ }, signal);
201
+ }
202
+ async reconcileModeUpgrade(input, signal, keepalive = false) {
203
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(input.requestId) ||
204
+ !/^[A-Za-z0-9_.:-]{8,128}$/.test(input.idempotencyKey)) {
205
+ throw new Error("Invalid AI Guide mode-upgrade recovery identity.");
206
+ }
207
+ return this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(input.sessionId)}/mode-upgrade/reconcile`, {
208
+ schemaVersion: input.protocolVersion,
209
+ protocolVersion: input.protocolVersion,
210
+ path: input.path,
211
+ modeUpgradeRequestId: input.requestId,
212
+ }, {
213
+ Authorization: `Bearer ${input.resumeCredential}`,
214
+ "Idempotency-Key": input.idempotencyKey,
215
+ }, signal, keepalive);
105
216
  }
106
- async offer(result, offerSdp, signal) {
217
+ async offer(result, offerSdp, signal, offerIdempotencyKey = (0, exports.idempotencyKey)("offer")) {
218
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(offerIdempotencyKey)) {
219
+ throw new Error("Invalid AI Guide realtime-offer idempotency key.");
220
+ }
107
221
  const response = await fetch(result.realtimeOfferUrl, {
108
- method: 'POST',
109
- mode: 'cors',
110
- credentials: 'omit',
111
- cache: 'no-store',
222
+ method: "POST",
223
+ mode: "cors",
224
+ credentials: "omit",
225
+ cache: "no-store",
112
226
  headers: {
113
- Accept: 'application/sdp',
227
+ Accept: "application/sdp",
114
228
  Authorization: `Bearer ${result.launchCredential}`,
115
- 'Content-Type': 'application/sdp',
116
- 'Idempotency-Key': (0, exports.idempotencyKey)('offer'),
117
- 'X-Oxvo-Ai-Client-Version': this.clientVersion,
118
- 'X-Request-Id': requestId(),
229
+ "Content-Type": "application/sdp",
230
+ "Idempotency-Key": offerIdempotencyKey,
231
+ "X-Oxvo-Ai-Client-Version": this.clientVersion,
232
+ "X-Request-Id": requestId(),
119
233
  },
120
234
  body: offerSdp,
121
235
  signal,
122
236
  });
123
237
  if (!response.ok)
124
238
  await this.throwResponse(response);
125
- const fencingToken = Number(response.headers.get('X-Oxvo-Fencing-Token'));
239
+ const fencingToken = Number(response.headers.get("X-Oxvo-Fencing-Token"));
126
240
  if (!Number.isSafeInteger(fencingToken) || fencingToken < 1) {
127
- throw new RuntimeClientError('PROTOCOL_INVALID', 'The runtime returned an invalid control lease.', false, null, 502);
241
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid control lease.", false, null, 502);
128
242
  }
129
- return { answerSdp: await response.text(), fencingToken };
243
+ const sessionStateHeader = response.headers.get("X-Oxvo-Session-State");
244
+ const sessionState = sessionStateHeader === "active_text" ||
245
+ sessionStateHeader === "active_voice"
246
+ ? sessionStateHeader
247
+ : null;
248
+ return {
249
+ answerSdp: await response.text(),
250
+ fencingToken,
251
+ sessionState,
252
+ };
130
253
  }
131
254
  async end(result, fencingToken, signal) {
132
- await this.postEmpty(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/end`, result.controlCredential, fencingToken, result.protocolVersion, signal);
255
+ await this.postEmpty(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/end`, result.controlCredential, fencingToken, result.protocolVersion, signal, true);
256
+ }
257
+ async recoverAndEnd(sessionId, resumeCredential, protocolVersion, signal) {
258
+ await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/end`, {
259
+ schemaVersion: protocolVersion,
260
+ protocolVersion,
261
+ fencingToken: 0,
262
+ terminalRecovery: true,
263
+ }, { Authorization: `Bearer ${resumeCredential}` }, signal, true, true);
133
264
  }
134
- async pauseOrResume(result, fencingToken, operation, signal) {
135
- await this.postEmpty(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/${operation}`, result.controlCredential, fencingToken, result.protocolVersion, signal);
265
+ async pauseOrResume(result, fencingToken, operation, identity, signal) {
266
+ if (!CONTROL_OPERATION_ID_PATTERN.test(identity.operationId) ||
267
+ !Number.isSafeInteger(identity.clientSequence) ||
268
+ identity.clientSequence < 1) {
269
+ throw new Error("Invalid AI Guide pause command identity.");
270
+ }
271
+ const response = await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/actions/${operation}`, {
272
+ schemaVersion: result.protocolVersion,
273
+ protocolVersion: result.protocolVersion,
274
+ fencingToken,
275
+ operationId: identity.operationId,
276
+ clientSequence: identity.clientSequence,
277
+ }, { Authorization: `Bearer ${result.controlCredential}` }, signal);
278
+ if (!response ||
279
+ typeof response !== "object" ||
280
+ response.schemaVersion !== 1 ||
281
+ response.operationId !== identity.operationId ||
282
+ response.clientSequence !== identity.clientSequence ||
283
+ response.fencingToken !== fencingToken ||
284
+ response.action !== operation ||
285
+ !["applied", "replayed", "superseded"].includes(response.status) ||
286
+ !RUNTIME_SESSION_STATES.has(response.state)) {
287
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid pause command result.", false, null, 502);
288
+ }
289
+ return response;
290
+ }
291
+ async switchCompanionSurface(input, signal) {
292
+ if (!CONTROL_OPERATION_ID_PATTERN.test(input.linkId) ||
293
+ !(0, exports.isValidAssistSessionGrant)(input.switchGrant) ||
294
+ !CONTROL_OPERATION_ID_PATTERN.test(input.visitorId) ||
295
+ !Number.isSafeInteger(input.expectedOwnerEpoch) ||
296
+ input.expectedOwnerEpoch < 0 ||
297
+ !["ai", "messenger"].includes(input.targetSurface) ||
298
+ !["text", "voice"].includes(input.lastAiMode) ||
299
+ !CONTROL_OPERATION_ID_PATTERN.test(input.idempotencyKey) ||
300
+ (input.currentAiSessionId !== undefined &&
301
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.currentAiSessionId))) {
302
+ throw new Error("Invalid AI Guide companion switch request.");
303
+ }
304
+ const response = await this.postJson(`/ai-live-assist/v1/public/companion-links/${encodeURIComponent(input.linkId)}/surface`, {
305
+ schemaVersion: 1,
306
+ expectedOwnerEpoch: input.expectedOwnerEpoch,
307
+ targetSurface: input.targetSurface,
308
+ lastAiMode: input.lastAiMode,
309
+ ...(input.currentAiSessionId
310
+ ? { currentAiSessionId: input.currentAiSessionId }
311
+ : {}),
312
+ idempotencyKey: input.idempotencyKey,
313
+ }, {
314
+ Authorization: `Bearer ${input.switchGrant}`,
315
+ "X-Oxvo-Ai-Visitor-Id": input.visitorId,
316
+ }, signal);
317
+ const companion = this.parseCompanionState(response.companion, response.switchGrant);
318
+ const messengerConversation = this.parseMessengerConversation(response.messengerConversation);
319
+ if (response.schemaVersion !== 1 ||
320
+ (response.status !== "switched" &&
321
+ response.status !== "replayed" &&
322
+ response.status !== "stale") ||
323
+ !companion ||
324
+ !messengerConversation ||
325
+ companion.linkId !== input.linkId) {
326
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid companion switch result.", false, null, 502);
327
+ }
328
+ return {
329
+ schemaVersion: 1,
330
+ status: response.status,
331
+ companion,
332
+ messengerConversation,
333
+ };
136
334
  }
137
335
  async feedback(result, payload, signal) {
138
336
  await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/feedback`, payload, {
139
337
  Authorization: `Bearer ${result.feedbackCredential}`,
140
338
  }, signal);
141
339
  }
142
- async postEmpty(path, credential, fencingToken, protocolVersion, signal) {
340
+ async postEmpty(path, credential, fencingToken, protocolVersion, signal, keepalive = false) {
143
341
  await this.postJson(path, {
144
342
  schemaVersion: protocolVersion,
145
343
  protocolVersion,
146
344
  fencingToken,
147
- }, { Authorization: `Bearer ${credential}` }, signal);
345
+ }, { Authorization: `Bearer ${credential}` }, signal, keepalive);
346
+ }
347
+ parseCompanionState(value, switchGrant) {
348
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
349
+ return null;
350
+ }
351
+ const candidate = value;
352
+ const currentAiSessionId = candidate.currentAiSessionId;
353
+ const expiresAt = typeof candidate.expiresAt === "string"
354
+ ? Date.parse(candidate.expiresAt)
355
+ : Number.NaN;
356
+ if (candidate.schemaVersion !== 1 ||
357
+ typeof candidate.linkId !== "string" ||
358
+ !CONTROL_OPERATION_ID_PATTERN.test(candidate.linkId) ||
359
+ (candidate.activeSurface !== "ai" &&
360
+ candidate.activeSurface !== "messenger") ||
361
+ (candidate.lastAiMode !== "text" && candidate.lastAiMode !== "voice") ||
362
+ !Number.isSafeInteger(candidate.ownerEpoch) ||
363
+ candidate.ownerEpoch < 0 ||
364
+ (currentAiSessionId !== null &&
365
+ (typeof currentAiSessionId !== "string" ||
366
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(currentAiSessionId))) ||
367
+ !Number.isFinite(expiresAt) ||
368
+ expiresAt <= Date.now() ||
369
+ !(0, exports.isValidAssistSessionGrant)(switchGrant)) {
370
+ return null;
371
+ }
372
+ return {
373
+ schemaVersion: 1,
374
+ linkId: candidate.linkId,
375
+ activeSurface: candidate.activeSurface,
376
+ lastAiMode: candidate.lastAiMode,
377
+ ownerEpoch: candidate.ownerEpoch,
378
+ currentAiSessionId: currentAiSessionId,
379
+ expiresAt: candidate.expiresAt,
380
+ switchGrant,
381
+ };
148
382
  }
149
- async postJson(path, body, extraHeaders, signal) {
383
+ parseMessengerConversation(value) {
384
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
385
+ return null;
386
+ }
387
+ const candidate = value;
388
+ if (candidate.state !== "connected" ||
389
+ typeof candidate.conversationDisplayId !== "string" ||
390
+ !/^[1-9]\d{0,18}$/.test(candidate.conversationDisplayId) ||
391
+ typeof candidate.inboxId !== "string" ||
392
+ !/^[1-9]\d{0,18}$/.test(candidate.inboxId)) {
393
+ return null;
394
+ }
395
+ return {
396
+ state: "connected",
397
+ conversationDisplayId: candidate.conversationDisplayId,
398
+ inboxId: candidate.inboxId,
399
+ };
400
+ }
401
+ async postJson(path, body, extraHeaders, signal, keepalive = false, requireNoContent = false) {
150
402
  const response = await fetch(`${this.baseUrl}${path}`, {
151
- method: 'POST',
152
- mode: 'cors',
153
- credentials: 'omit',
154
- cache: 'no-store',
403
+ method: "POST",
404
+ mode: "cors",
405
+ credentials: "omit",
406
+ cache: "no-store",
155
407
  headers: {
156
- Accept: 'application/json',
157
- 'Content-Type': 'application/json',
158
- 'X-Oxvo-Ai-Client-Version': this.clientVersion,
159
- 'X-Request-Id': requestId(),
408
+ Accept: "application/json",
409
+ "Content-Type": "application/json",
410
+ "X-Oxvo-Ai-Client-Version": this.clientVersion,
411
+ "X-Request-Id": requestId(),
160
412
  ...extraHeaders,
161
413
  },
162
414
  body: JSON.stringify(body),
415
+ ...(keepalive ? { keepalive: true } : {}),
163
416
  ...(signal ? { signal } : {}),
164
417
  });
165
418
  if (response.status === 204)
166
419
  return undefined;
420
+ if (requireNoContent) {
421
+ if (!response.ok)
422
+ await this.throwResponse(response);
423
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid terminal response.", false, null, 502);
424
+ }
167
425
  return this.parseJson(response);
168
426
  }
169
427
  async parseJson(response) {
@@ -173,7 +431,7 @@ class RuntimeClient {
173
431
  return (await response.json());
174
432
  }
175
433
  catch {
176
- throw new RuntimeClientError('PROTOCOL_INVALID', 'The runtime returned an invalid response.', false, null, response.status);
434
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid response.", false, null, response.status);
177
435
  }
178
436
  }
179
437
  async throwResponse(response) {
@@ -185,9 +443,9 @@ class RuntimeClient {
185
443
  catch {
186
444
  // Keep the public fallback generic; response bodies may come from a proxy.
187
445
  }
188
- throw new RuntimeClientError(typeof error.code === 'string' ? error.code : 'RUNTIME_UNAVAILABLE', typeof error.message === 'string'
446
+ throw new RuntimeClientError(typeof error.code === "string" ? error.code : "RUNTIME_UNAVAILABLE", typeof error.message === "string"
189
447
  ? error.message
190
- : 'AI Guide is temporarily unavailable.', error.retryable === true, typeof error.retryAfterMs === 'number' ? error.retryAfterMs : null, response.status);
448
+ : "AI Guide is temporarily unavailable.", error.retryable === true, typeof error.retryAfterMs === "number" ? error.retryAfterMs : null, response.status);
191
449
  }
192
450
  }
193
451
  exports.RuntimeClient = RuntimeClient;