@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.
- package/cjs/AiLiveAssist.d.ts +588 -6
- package/cjs/AiLiveAssist.js +14892 -1240
- package/cjs/client.d.ts +71 -10
- package/cjs/client.js +316 -58
- package/cjs/context.d.ts +15 -5
- package/cjs/context.js +568 -73
- package/cjs/control.d.ts +72 -7
- package/cjs/control.js +594 -71
- package/cjs/dialogFocusScope.d.ts +37 -0
- package/cjs/dialogFocusScope.js +227 -0
- package/cjs/experienceState.js +1 -1
- package/cjs/index.d.ts +1 -1
- package/cjs/index.js +2 -2
- package/cjs/media.d.ts +14 -3
- package/cjs/media.js +128 -19
- package/cjs/messages.js +26 -3
- package/cjs/messengerLoader.d.ts +3 -0
- package/cjs/messengerLoader.js +156 -0
- package/cjs/pageAssistancePreference.d.ts +78 -0
- package/cjs/pageAssistancePreference.js +488 -0
- package/cjs/preview.d.ts +33 -0
- package/cjs/preview.js +567 -0
- package/cjs/tabLock.d.ts +54 -3
- package/cjs/tabLock.js +752 -72
- package/cjs/types.d.ts +152 -4
- package/cjs/ui.d.ts +200 -10
- package/cjs/ui.js +6946 -523
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/cjs/voicePresenceUi.d.ts +48 -9
- package/cjs/voicePresenceUi.js +863 -125
- package/lib/AiLiveAssist.d.ts +588 -6
- package/lib/AiLiveAssist.js +14897 -1248
- package/lib/client.d.ts +71 -10
- package/lib/client.js +313 -57
- package/lib/context.d.ts +15 -5
- package/lib/context.js +566 -72
- package/lib/control.d.ts +72 -7
- package/lib/control.js +594 -71
- package/lib/dialogFocusScope.d.ts +37 -0
- package/lib/dialogFocusScope.js +223 -0
- package/lib/experienceState.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -2
- package/lib/media.d.ts +14 -3
- package/lib/media.js +128 -19
- package/lib/messages.js +26 -3
- package/lib/messengerLoader.d.ts +3 -0
- package/lib/messengerLoader.js +151 -0
- package/lib/pageAssistancePreference.d.ts +78 -0
- package/lib/pageAssistancePreference.js +482 -0
- package/lib/preview.d.ts +33 -0
- package/lib/preview.js +561 -0
- package/lib/tabLock.d.ts +54 -3
- package/lib/tabLock.js +752 -72
- package/lib/types.d.ts +152 -4
- package/lib/ui.d.ts +200 -10
- package/lib/ui.js +6947 -524
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/voicePresenceUi.d.ts +48 -9
- package/lib/voicePresenceUi.js +863 -125
- package/package.json +8 -3
- package/cjs/recordingPolicy.d.ts +0 -2
- package/cjs/recordingPolicy.js +0 -12
- package/lib/recordingPolicy.d.ts +0 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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 (![
|
|
23
|
-
throw new Error(
|
|
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(
|
|
30
|
-
url.searchParams.set(
|
|
59
|
+
url.searchParams.set("siteKey", siteKey);
|
|
60
|
+
url.searchParams.set("path", path);
|
|
31
61
|
const response = await fetch(url, {
|
|
32
|
-
method:
|
|
33
|
-
mode:
|
|
34
|
-
credentials:
|
|
35
|
-
cache:
|
|
62
|
+
method: "GET",
|
|
63
|
+
mode: "cors",
|
|
64
|
+
credentials: "omit",
|
|
65
|
+
cache: "no-store",
|
|
36
66
|
headers: {
|
|
37
|
-
Accept:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 !==
|
|
74
|
-
audioPlayback: typeof HTMLAudioElement !==
|
|
75
|
-
webAudio: typeof AudioContext !==
|
|
76
|
-
typeof window
|
|
77
|
-
|
|
78
|
-
|
|
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 !==
|
|
141
|
+
webrtc: typeof RTCPeerConnection !== "undefined",
|
|
91
142
|
reducedMotion: input.reducedMotion,
|
|
92
143
|
},
|
|
93
144
|
};
|
|
94
|
-
return this.postJson(
|
|
95
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
109
|
-
mode:
|
|
110
|
-
credentials:
|
|
111
|
-
cache:
|
|
222
|
+
method: "POST",
|
|
223
|
+
mode: "cors",
|
|
224
|
+
credentials: "omit",
|
|
225
|
+
cache: "no-store",
|
|
112
226
|
headers: {
|
|
113
|
-
Accept:
|
|
227
|
+
Accept: "application/sdp",
|
|
114
228
|
Authorization: `Bearer ${result.launchCredential}`,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(
|
|
239
|
+
const fencingToken = Number(response.headers.get("X-Oxvo-Fencing-Token"));
|
|
126
240
|
if (!Number.isSafeInteger(fencingToken) || fencingToken < 1) {
|
|
127
|
-
throw new RuntimeClientError(
|
|
241
|
+
throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid control lease.", false, null, 502);
|
|
128
242
|
}
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
152
|
-
mode:
|
|
153
|
-
credentials:
|
|
154
|
-
cache:
|
|
403
|
+
method: "POST",
|
|
404
|
+
mode: "cors",
|
|
405
|
+
credentials: "omit",
|
|
406
|
+
cache: "no-store",
|
|
155
407
|
headers: {
|
|
156
|
-
Accept:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
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 ===
|
|
446
|
+
throw new RuntimeClientError(typeof error.code === "string" ? error.code : "RUNTIME_UNAVAILABLE", typeof error.message === "string"
|
|
189
447
|
? error.message
|
|
190
|
-
:
|
|
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;
|