@lazyingart/agent-web 0.1.40
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/LICENSE +22 -0
- package/README.md +438 -0
- package/docs/architecture.md +503 -0
- package/package.json +43 -0
- package/src/aginti-adapter.js +602 -0
- package/src/chat-context.js +1020 -0
- package/src/chat-migrations.js +947 -0
- package/src/chat-store.js +3308 -0
- package/src/cli.js +134 -0
- package/src/cloud-server.js +2043 -0
- package/src/contracts.js +103 -0
- package/src/deterministic-context-summarizer.js +254 -0
- package/src/direct-chat-capability-limits.js +66 -0
- package/src/direct-chat-contract.js +3 -0
- package/src/errors.js +50 -0
- package/src/http-contract.js +592 -0
- package/src/index.js +88 -0
- package/src/localllm-connector.js +667 -0
- package/src/migrations.js +231 -0
- package/src/operator-health.js +184 -0
- package/src/password-verifier.js +131 -0
- package/src/service-config.js +547 -0
- package/src/service.js +408 -0
- package/src/sqlite-health.js +83 -0
- package/src/storage-path.js +130 -0
- package/src/store.js +914 -0
- package/src/validation.js +181 -0
- package/src/vision-attachment.js +404 -0
- package/src/web/aginti-client.js +552 -0
- package/src/web/aginti-protocol.js +1146 -0
- package/src/web/asset-map.js +462 -0
- package/src/web/browser-app.js +6491 -0
- package/src/web/cloud-session-client.js +427 -0
- package/src/web/direct-chat-client.js +1482 -0
- package/src/web/index.js +10 -0
- package/src/web/presentation-state.js +107 -0
- package/src/web/pwa-assets.js +854 -0
- package/src/web/pwa-update-handoff-store.js +179 -0
- package/src/web/safe-rendering.js +836 -0
- package/src/web/vision-image-client.js +546 -0
- package/src/web/vision-image-sanitizer.js +168 -0
- package/src/web/web-release.js +28 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addWebReleaseHeader,
|
|
3
|
+
inspectWebReleaseResponse,
|
|
4
|
+
optionalWebRelease,
|
|
5
|
+
} from "./web-release.js";
|
|
6
|
+
|
|
7
|
+
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
8
|
+
const JSON_RESPONSE_LIMIT = 32 * 1024;
|
|
9
|
+
const ERROR_RESPONSE_LIMIT = 16 * 1024;
|
|
10
|
+
const COOKIE_LIMIT = 4_096;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
12
|
+
|
|
13
|
+
export const CLOUD_SESSION_ROUTES = Object.freeze({
|
|
14
|
+
login: "/api/login",
|
|
15
|
+
session: "/api/session",
|
|
16
|
+
logout: "/api/logout",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const CLOUD_CSRF_COOKIE_NAME = "__Host-lazying_csrf";
|
|
20
|
+
export const CLOUD_CSRF_HEADER_NAME = "x-csrf-token";
|
|
21
|
+
|
|
22
|
+
const ERROR_CODE = /^[a-z][a-z0-9_]{0,79}$/u;
|
|
23
|
+
const CSRF_TOKEN = /^[A-Za-z0-9_-]{32,128}$/u;
|
|
24
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
|
|
27
|
+
export class CloudBrowserProtocolError extends Error {
|
|
28
|
+
constructor(message, { code = "cloud_protocol_error" } = {}) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "CloudBrowserProtocolError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.status = 502;
|
|
33
|
+
this.retryable = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class CloudBrowserTransportError extends Error {
|
|
38
|
+
constructor(message, {
|
|
39
|
+
code = "cloud_unavailable",
|
|
40
|
+
status = 503,
|
|
41
|
+
retryable = true,
|
|
42
|
+
serverRelease,
|
|
43
|
+
} = {}) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "CloudBrowserTransportError";
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.status = status;
|
|
48
|
+
this.retryable = retryable;
|
|
49
|
+
if (serverRelease !== undefined) this.serverRelease = optionalWebRelease(serverRelease);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function exactObject(value, allowed, required, label) {
|
|
54
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
throw new CloudBrowserProtocolError(`${label} must be a plain object`);
|
|
56
|
+
}
|
|
57
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
59
|
+
throw new CloudBrowserProtocolError(`${label} must be a plain object`);
|
|
60
|
+
}
|
|
61
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
62
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
63
|
+
if (typeof key !== "string" || !allowed.includes(key)) {
|
|
64
|
+
throw new CloudBrowserProtocolError(`${label} contains an unsupported field`);
|
|
65
|
+
}
|
|
66
|
+
if (!descriptors[key].enumerable || !Object.hasOwn(descriptors[key], "value")) {
|
|
67
|
+
throw new CloudBrowserProtocolError(`${label} contains an accessor`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const key of required) {
|
|
71
|
+
if (!Object.hasOwn(value, key)) throw new CloudBrowserProtocolError(`${label}.${key} is required`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function utf8Length(value) {
|
|
77
|
+
return encoder.encode(value).byteLength;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function boundedText(value, label, { minimum = 1, maximum, controls = false } = {}) {
|
|
81
|
+
if (typeof value !== "string") throw new TypeError(`${label} must be a string`);
|
|
82
|
+
const bytes = utf8Length(value);
|
|
83
|
+
if (bytes < minimum || bytes > maximum || (!controls && CONTROL.test(value))) {
|
|
84
|
+
throw new TypeError(`${label} is invalid`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizedBaseOrigin(value) {
|
|
90
|
+
const base = value ?? globalThis.location?.href;
|
|
91
|
+
if (typeof base !== "string") throw new TypeError("baseUrl is required outside a browser");
|
|
92
|
+
const parsed = new URL(base);
|
|
93
|
+
if (!/^https?:$/u.test(parsed.protocol) || parsed.username || parsed.password || parsed.origin === "null") {
|
|
94
|
+
throw new TypeError("baseUrl must be an HTTP(S) URL without credentials");
|
|
95
|
+
}
|
|
96
|
+
return parsed.origin;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cookieReader(source) {
|
|
100
|
+
if (source === undefined) return () => globalThis.document?.cookie ?? "";
|
|
101
|
+
if (typeof source === "function") return source;
|
|
102
|
+
if (typeof source === "string") return () => source;
|
|
103
|
+
throw new TypeError("cookieSource must be a function or string");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function readCloudCsrfCookie(source = () => globalThis.document?.cookie ?? "") {
|
|
107
|
+
const raw = typeof source === "function" ? source() : source;
|
|
108
|
+
if (typeof raw !== "string" || utf8Length(raw) > COOKIE_LIMIT || CONTROL.test(raw)) {
|
|
109
|
+
throw new CloudBrowserProtocolError("browser cookies are invalid", { code: "invalid_cookie" });
|
|
110
|
+
}
|
|
111
|
+
const matches = [];
|
|
112
|
+
for (const component of raw.split(";")) {
|
|
113
|
+
const part = component.trim();
|
|
114
|
+
if (!part) continue;
|
|
115
|
+
const separator = part.indexOf("=");
|
|
116
|
+
if (separator < 1) continue;
|
|
117
|
+
const name = part.slice(0, separator).trim();
|
|
118
|
+
if (name === CLOUD_CSRF_COOKIE_NAME) matches.push(part.slice(separator + 1).trim());
|
|
119
|
+
}
|
|
120
|
+
if (matches.length > 1) {
|
|
121
|
+
throw new CloudBrowserProtocolError("the CSRF cookie is duplicated", { code: "invalid_cookie" });
|
|
122
|
+
}
|
|
123
|
+
if (matches.length === 0) return undefined;
|
|
124
|
+
if (!CSRF_TOKEN.test(matches[0])) {
|
|
125
|
+
throw new CloudBrowserProtocolError("the CSRF cookie is invalid", { code: "invalid_cookie" });
|
|
126
|
+
}
|
|
127
|
+
return matches[0];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function timeoutSignal(signal, timeoutMs) {
|
|
131
|
+
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError("signal must be an AbortSignal");
|
|
132
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 120_000) {
|
|
133
|
+
throw new TypeError("timeoutMs is invalid");
|
|
134
|
+
}
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const forward = () => controller.abort(signal.reason ?? new DOMException("request aborted", "AbortError"));
|
|
137
|
+
if (signal?.aborted) forward();
|
|
138
|
+
else signal?.addEventListener("abort", forward, { once: true });
|
|
139
|
+
const timer = setTimeout(
|
|
140
|
+
() => controller.abort(new DOMException("request timed out", "TimeoutError")),
|
|
141
|
+
timeoutMs,
|
|
142
|
+
);
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
signal: controller.signal,
|
|
145
|
+
dispose() {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
signal?.removeEventListener("abort", forward);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mediaType(response) {
|
|
153
|
+
return String(response.headers?.get?.("content-type") ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function requireResponse(value) {
|
|
157
|
+
if (value === null || typeof value !== "object" || !Number.isSafeInteger(value.status)
|
|
158
|
+
|| value.status < 100 || value.status > 599 || typeof value.headers?.get !== "function") {
|
|
159
|
+
throw new CloudBrowserProtocolError("cloud transport returned an invalid response");
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function requireNoStore(response) {
|
|
165
|
+
const directives = String(response.headers?.get?.("cache-control") ?? "")
|
|
166
|
+
.toLowerCase()
|
|
167
|
+
.split(",")
|
|
168
|
+
.map((value) => value.trim());
|
|
169
|
+
if (!directives.includes("no-store")) {
|
|
170
|
+
throw new CloudBrowserProtocolError("cloud response is missing its no-store policy");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function readBoundedText(response, maximum) {
|
|
175
|
+
const advertised = response.headers?.get?.("content-length");
|
|
176
|
+
if (advertised !== null && advertised !== undefined
|
|
177
|
+
&& (!/^\d+$/u.test(advertised) || Number(advertised) > maximum)) {
|
|
178
|
+
throw new CloudBrowserProtocolError("cloud response exceeded its size limit");
|
|
179
|
+
}
|
|
180
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
181
|
+
const value = await response.text();
|
|
182
|
+
if (utf8Length(value) > maximum) throw new CloudBrowserProtocolError("cloud response exceeded its size limit");
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
const reader = response.body.getReader();
|
|
186
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
187
|
+
let bytes = 0;
|
|
188
|
+
let result = "";
|
|
189
|
+
try {
|
|
190
|
+
while (true) {
|
|
191
|
+
const { done, value } = await reader.read();
|
|
192
|
+
if (done) break;
|
|
193
|
+
if (!(value instanceof Uint8Array)) throw new CloudBrowserProtocolError("cloud response returned a non-byte chunk");
|
|
194
|
+
bytes += value.byteLength;
|
|
195
|
+
if (bytes > maximum) throw new CloudBrowserProtocolError("cloud response exceeded its size limit");
|
|
196
|
+
result += decoder.decode(value, { stream: true });
|
|
197
|
+
}
|
|
198
|
+
result += decoder.decode();
|
|
199
|
+
return result;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error instanceof CloudBrowserProtocolError) throw error;
|
|
202
|
+
throw new CloudBrowserProtocolError("cloud response is not valid UTF-8");
|
|
203
|
+
} finally {
|
|
204
|
+
reader.releaseLock?.();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function responseMatchesRoute(response, endpoint) {
|
|
209
|
+
if (response?.redirected === true || response?.type === "opaqueredirect") return false;
|
|
210
|
+
if (typeof response?.url !== "string" || response.url === "") return true;
|
|
211
|
+
try { return new URL(response.url).href === endpoint; }
|
|
212
|
+
catch { return false; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function safeErrorCode(value) {
|
|
216
|
+
return typeof value === "string" && ERROR_CODE.test(value) ? value : "request_failed";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function responseFailure(response, action) {
|
|
220
|
+
let code = "request_failed";
|
|
221
|
+
if (mediaType(response) === "application/json") {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(await readBoundedText(response, ERROR_RESPONSE_LIMIT));
|
|
224
|
+
const envelope = exactObject(parsed, ["error"], ["error"], "error response");
|
|
225
|
+
const error = exactObject(envelope.error, ["code", "message"], ["code", "message"], "error");
|
|
226
|
+
boundedText(error.message, "error.message", { maximum: 512 });
|
|
227
|
+
code = safeErrorCode(error.code);
|
|
228
|
+
} catch {
|
|
229
|
+
code = "request_failed";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return new CloudBrowserTransportError(`${action} request was not accepted.`, {
|
|
233
|
+
code,
|
|
234
|
+
status: Number.isSafeInteger(response?.status) ? response.status : 503,
|
|
235
|
+
retryable: [408, 425, 429].includes(response?.status) || response?.status >= 500,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function requirePinnedRelease(response, releaseId, action) {
|
|
240
|
+
const proof = inspectWebReleaseResponse(response, releaseId);
|
|
241
|
+
if (proof.kind === "unpinned" || proof.kind === "match") return;
|
|
242
|
+
if (proof.kind === "mismatch") {
|
|
243
|
+
throw new CloudBrowserTransportError(`${action} request requires the current browser app release.`, {
|
|
244
|
+
code: "client_release_mismatch",
|
|
245
|
+
status: 409,
|
|
246
|
+
retryable: false,
|
|
247
|
+
serverRelease: proof.releaseId,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
throw new CloudBrowserProtocolError("cloud response is missing its release identity");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function networkFailure(error, action, signal) {
|
|
254
|
+
if (error instanceof CloudBrowserProtocolError || error instanceof CloudBrowserTransportError) return error;
|
|
255
|
+
const reason = signal?.aborted ? signal.reason : error;
|
|
256
|
+
if (reason?.name === "AbortError" || reason?.name === "TimeoutError") {
|
|
257
|
+
return new CloudBrowserTransportError(`${action} request was interrupted.`, {
|
|
258
|
+
code: reason.name === "TimeoutError" ? "request_timeout" : "request_aborted",
|
|
259
|
+
status: reason.name === "TimeoutError" ? 504 : 499,
|
|
260
|
+
retryable: reason.name === "TimeoutError",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return new CloudBrowserTransportError(`${action} service is unavailable.`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function sessionEnvelope(value) {
|
|
267
|
+
const session = exactObject(value, ["authenticated", "username", "csrfToken"], ["authenticated"], "session response");
|
|
268
|
+
if (typeof session.authenticated !== "boolean") {
|
|
269
|
+
throw new CloudBrowserProtocolError("session response authenticated flag is invalid");
|
|
270
|
+
}
|
|
271
|
+
if (!session.authenticated) {
|
|
272
|
+
if (Object.keys(session).length !== 1) throw new CloudBrowserProtocolError("signed-out session contains unsupported state");
|
|
273
|
+
return Object.freeze({ authenticated: false });
|
|
274
|
+
}
|
|
275
|
+
boundedText(session.username, "session.username", { maximum: 128 });
|
|
276
|
+
if (typeof session.csrfToken !== "string" || !CSRF_TOKEN.test(session.csrfToken)) {
|
|
277
|
+
throw new CloudBrowserProtocolError("session response CSRF token is invalid");
|
|
278
|
+
}
|
|
279
|
+
return Object.freeze({ authenticated: true, username: session.username, csrfToken: session.csrfToken });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function logoutEnvelope(value) {
|
|
283
|
+
const result = exactObject(
|
|
284
|
+
value,
|
|
285
|
+
["signedOut", "agentCancellationPending"],
|
|
286
|
+
["signedOut", "agentCancellationPending"],
|
|
287
|
+
"logout response",
|
|
288
|
+
);
|
|
289
|
+
if (result.signedOut !== true || typeof result.agentCancellationPending !== "boolean") {
|
|
290
|
+
throw new CloudBrowserProtocolError("logout response is invalid");
|
|
291
|
+
}
|
|
292
|
+
return Object.freeze({ signedOut: true, agentCancellationPending: result.agentCancellationPending });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function loginRequest(value) {
|
|
296
|
+
const request = exactObject(value, ["username", "password", "remember"], ["username", "password", "remember"], "login request");
|
|
297
|
+
boundedText(request.username, "username", { maximum: 128 });
|
|
298
|
+
boundedText(request.password, "password", { maximum: 1_024, controls: true });
|
|
299
|
+
if (typeof request.remember !== "boolean") throw new TypeError("remember must be boolean");
|
|
300
|
+
return Object.freeze({ username: request.username, password: request.password, remember: request.remember });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export class CloudSessionClient {
|
|
304
|
+
constructor(options = {}) {
|
|
305
|
+
const config = exactObject(
|
|
306
|
+
options,
|
|
307
|
+
["baseUrl", "fetchImpl", "cookieSource", "timeoutMs", "releaseId"],
|
|
308
|
+
[],
|
|
309
|
+
"session client options",
|
|
310
|
+
);
|
|
311
|
+
const baseUrl = config.baseUrl;
|
|
312
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
313
|
+
const cookieSource = config.cookieSource;
|
|
314
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
315
|
+
if (typeof fetchImpl !== "function") throw new TypeError("fetchImpl must be a function");
|
|
316
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 120_000) {
|
|
317
|
+
throw new TypeError("timeoutMs is invalid");
|
|
318
|
+
}
|
|
319
|
+
this.baseOrigin = normalizedBaseOrigin(baseUrl);
|
|
320
|
+
this.fetch = fetchImpl === globalThis.fetch ? fetchImpl.bind(globalThis) : fetchImpl;
|
|
321
|
+
this.readCookie = cookieReader(cookieSource);
|
|
322
|
+
this.timeoutMs = timeoutMs;
|
|
323
|
+
this.releaseId = optionalWebRelease(config.releaseId);
|
|
324
|
+
this.validatedCsrf = undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
csrfToken() {
|
|
328
|
+
const cookie = readCloudCsrfCookie(this.readCookie);
|
|
329
|
+
return cookie ?? this.validatedCsrf;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async #post(route, body, { signal, csrf, expectedStatus, action }) {
|
|
333
|
+
const endpoint = `${this.baseOrigin}${route}`;
|
|
334
|
+
const deadline = timeoutSignal(signal, this.timeoutMs);
|
|
335
|
+
const headers = addWebReleaseHeader(new Headers({
|
|
336
|
+
accept: "application/json",
|
|
337
|
+
"content-type": JSON_CONTENT_TYPE,
|
|
338
|
+
}), this.releaseId);
|
|
339
|
+
if (csrf !== undefined) headers.set(CLOUD_CSRF_HEADER_NAME, csrf);
|
|
340
|
+
let response;
|
|
341
|
+
try {
|
|
342
|
+
response = requireResponse(await this.fetch(endpoint, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
credentials: "same-origin",
|
|
345
|
+
cache: "no-store",
|
|
346
|
+
redirect: "error",
|
|
347
|
+
referrerPolicy: "same-origin",
|
|
348
|
+
headers,
|
|
349
|
+
body: JSON.stringify(body),
|
|
350
|
+
signal: deadline.signal,
|
|
351
|
+
}));
|
|
352
|
+
if (!responseMatchesRoute(response, endpoint)) {
|
|
353
|
+
throw new CloudBrowserProtocolError("cloud response came from an unexpected URL");
|
|
354
|
+
}
|
|
355
|
+
requirePinnedRelease(response, this.releaseId, action);
|
|
356
|
+
requireNoStore(response);
|
|
357
|
+
if (response.status !== expectedStatus) throw await responseFailure(response, action);
|
|
358
|
+
if (mediaType(response) !== "application/json") {
|
|
359
|
+
throw new CloudBrowserProtocolError("cloud response content type is invalid");
|
|
360
|
+
}
|
|
361
|
+
let value;
|
|
362
|
+
try { value = JSON.parse(await readBoundedText(response, JSON_RESPONSE_LIMIT)); }
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (error instanceof CloudBrowserProtocolError) throw error;
|
|
365
|
+
throw new CloudBrowserProtocolError("cloud response is not valid JSON");
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw networkFailure(error, action, deadline.signal);
|
|
370
|
+
} finally {
|
|
371
|
+
deadline.dispose();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async restore(options = {}) {
|
|
376
|
+
const { signal } = exactObject(options, ["signal"], [], "session restore options");
|
|
377
|
+
const csrf = this.csrfToken();
|
|
378
|
+
const session = sessionEnvelope(await this.#post(CLOUD_SESSION_ROUTES.session, {}, {
|
|
379
|
+
signal,
|
|
380
|
+
csrf,
|
|
381
|
+
expectedStatus: 200,
|
|
382
|
+
action: "Session restore",
|
|
383
|
+
}));
|
|
384
|
+
if (session.authenticated && (csrf === undefined || session.csrfToken !== csrf)) {
|
|
385
|
+
throw new CloudBrowserProtocolError("restored session is not bound to the browser CSRF cookie");
|
|
386
|
+
}
|
|
387
|
+
this.validatedCsrf = session.authenticated ? session.csrfToken : undefined;
|
|
388
|
+
return session;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async login(value, options = {}) {
|
|
392
|
+
const { signal } = exactObject(options, ["signal"], [], "sign-in options");
|
|
393
|
+
const request = loginRequest(value);
|
|
394
|
+
const session = sessionEnvelope(await this.#post(CLOUD_SESSION_ROUTES.login, request, {
|
|
395
|
+
signal,
|
|
396
|
+
expectedStatus: 200,
|
|
397
|
+
action: "Sign-in",
|
|
398
|
+
}));
|
|
399
|
+
if (!session.authenticated) throw new CloudBrowserProtocolError("sign-in returned a signed-out session");
|
|
400
|
+
const csrf = readCloudCsrfCookie(this.readCookie);
|
|
401
|
+
if (csrf !== undefined && csrf !== session.csrfToken) {
|
|
402
|
+
throw new CloudBrowserProtocolError("signed-in session is not bound to the browser CSRF cookie");
|
|
403
|
+
}
|
|
404
|
+
this.validatedCsrf = session.csrfToken;
|
|
405
|
+
return session;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async logout(options = {}) {
|
|
409
|
+
const { signal } = exactObject(options, ["signal"], [], "sign-out options");
|
|
410
|
+
const csrf = this.csrfToken();
|
|
411
|
+
if (csrf === undefined) {
|
|
412
|
+
throw new CloudBrowserTransportError("Sign-out request was not accepted.", {
|
|
413
|
+
code: "authentication_required",
|
|
414
|
+
status: 401,
|
|
415
|
+
retryable: false,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const result = logoutEnvelope(await this.#post(CLOUD_SESSION_ROUTES.logout, {}, {
|
|
419
|
+
signal,
|
|
420
|
+
csrf,
|
|
421
|
+
expectedStatus: 200,
|
|
422
|
+
action: "Sign-out",
|
|
423
|
+
}));
|
|
424
|
+
this.validatedCsrf = undefined;
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
}
|