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